using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Physics; using DatReaderWriter.Enums; using DatReaderWriter.Types; using Xunit; namespace AcDream.Core.Tests.Physics; /// /// Render Residual A — verbatim port of CEnvCell::find_visible_child_cell /// (acclient_2013_pseudo_c.txt:311397): given a start cell, a world point, /// and a mode, return the cell whose cell-BSP point_in_cell contains the /// point — checking the start cell itself, then (stab-list mode) the start's /// VisibleCellIds or (portal mode) its direct portal neighbours. /// /// /// This is the sibling of (retail /// find_cell_list); both resolve cell membership from the cell graph. The /// camera's SmartBox::update_viewer start-cell uses the stab-list mode /// (AdjustPosition at pc:280028 passes arg5=1) to seat the sweep at /// the PIVOT's cell, which differs from the feet cell at a low connector (the /// cellar lip), where the pivot is up at floor level in a different cell. /// /// /// /// Geometry is identity-transform (cell-local == world) so the synthetic CellBSP /// splitting planes read directly: cell A is the half-space Y≤3, cell B (in A's /// stab list) is the half-space Y≥7, and Y∈(3,7) belongs to neither. /// /// public class CellTransitFindVisibleChildCellTests { private const uint StartCellId = 0xA9B40174u; // low 0x0174 ≥ 0x0100 → indoor private const uint SiblingCellId = 0xA9B40171u; // the "room above" in StartCell's stab list [Fact] public void PointInsideStartCell_ReturnsStartCell() { var cache = new PhysicsDataCache(); cache.RegisterCellStructForTest(StartCellId, MakeCell(InteriorYAtMost(3f), new uint[] { SiblingCellId })); cache.RegisterCellStructForTest(SiblingCellId, MakeCell(InteriorYAtLeast(7f), Array.Empty())); // P at Y=1 is inside A (Y≤3) → the "this" branch returns the start cell. uint result = CellTransit.FindVisibleChildCell(cache, StartCellId, new Vector3(0f, 1f, 0f), useStabList: true); Assert.Equal(StartCellId, result); } [Fact] public void PointInStabListSibling_ReturnsSibling() { var cache = new PhysicsDataCache(); cache.RegisterCellStructForTest(StartCellId, MakeCell(InteriorYAtMost(3f), new uint[] { SiblingCellId })); cache.RegisterCellStructForTest(SiblingCellId, MakeCell(InteriorYAtLeast(7f), Array.Empty())); // P at Y=8 is outside A (Y≤3) but inside B (Y≥7), and B is in A's stab list. uint result = CellTransit.FindVisibleChildCell(cache, StartCellId, new Vector3(0f, 8f, 0f), useStabList: true); Assert.Equal(SiblingCellId, result); } [Fact] public void PointInNoCell_ReturnsZero() { var cache = new PhysicsDataCache(); cache.RegisterCellStructForTest(StartCellId, MakeCell(InteriorYAtMost(3f), new uint[] { SiblingCellId })); cache.RegisterCellStructForTest(SiblingCellId, MakeCell(InteriorYAtLeast(7f), Array.Empty())); // P at Y=5 is in the gap: outside A (Y≤3) and outside B (Y≥7). uint result = CellTransit.FindVisibleChildCell(cache, StartCellId, new Vector3(0f, 5f, 0f), useStabList: true); Assert.Equal(0u, result); } [Fact] public void UnknownStartCell_ReturnsZero() { var cache = new PhysicsDataCache(); uint result = CellTransit.FindVisibleChildCell(cache, 0xDEADBEEFu, new Vector3(0f, 1f, 0f), useStabList: true); Assert.Equal(0u, result); } // ── helpers ───────────────────────────────────────────────────────────── /// CellBSP root for the half-space Y ≤ /// (interior on the −Y side; point_in_cell true when Y ≤ boundary). private static CellBSPNode InteriorYAtMost(float boundary) => new() { SplittingPlane = new Plane(new Vector3(0f, -1f, 0f), boundary), // dist = boundary − Y ≥ 0 ⇔ Y ≤ boundary PosNode = new CellBSPNode { Type = BSPNodeType.Leaf }, }; /// CellBSP root for the half-space Y ≥ . private static CellBSPNode InteriorYAtLeast(float boundary) => new() { SplittingPlane = new Plane(new Vector3(0f, 1f, 0f), -boundary), // dist = Y − boundary ≥ 0 ⇔ Y ≥ boundary PosNode = new CellBSPNode { Type = BSPNodeType.Leaf }, }; private static CellPhysics MakeCell(CellBSPNode cellBspRoot, uint[] visibleCellIds) => new() { BSP = new PhysicsBSPTree { Root = new PhysicsBSPNode { Type = BSPNodeType.Leaf } }, WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity, Resolved = new Dictionary(), CellBSP = new CellBSPTree { Root = cellBspRoot }, Portals = Array.Empty(), PortalPolygons = new Dictionary(), VisibleCellIds = new HashSet(visibleCellIds), }; }