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 CPhysicsObj::AdjustPosition /// (acclient_2013_pseudo_c.txt:280009): resolve which cell actually /// contains a world point, given a seed cell. Indoor (objcell_id ≥ 0x100) /// delegates to (stab-list mode, /// retail arg5 = 1); outdoor snaps to the landcell under the point /// (retail LandDefs::adjust_to_outside). SmartBox::update_viewer /// uses it to seat the camera sweep's start cell at the head-pivot, and again /// as fallback 1 at the sought eye. /// public class PhysicsEngineAdjustPositionTests { private const uint FeetCellId = 0xA9B40174u; // indoor connector (the cellar lip) private const uint RoomCellId = 0xA9B40171u; // indoor room above, in the feet cell's stab list private const uint LandblockId = 0xA9B40000u; [Fact] public void Indoor_PivotInStabListSibling_ResolvesSiblingAndFound() { var engine = BuildIndoorEngine(); // Pivot at Y=8 is outside the feet cell (Y≤3) but inside the room cell (Y≥7). var (cellId, found) = engine.AdjustPosition(FeetCellId, new Vector3(0f, 8f, 0f)); Assert.True(found); Assert.Equal(RoomCellId, cellId); } [Fact] public void Indoor_PointInNoCell_NotFound() { var engine = BuildIndoorEngine(); // Y=5 is in the gap between the feet cell (Y≤3) and the room cell (Y≥7). var (_, found) = engine.AdjustPosition(FeetCellId, new Vector3(0f, 5f, 0f)); Assert.False(found); } [Fact] public void Outdoor_PointInLandblock_SnapsToLandcell() { var engine = BuildIndoorEngine(); // also registers the landblock // Outdoor seed (low byte < 0x100). A point inside the landblock snaps to a // prefixed landcell with found=true (retail LandDefs::adjust_to_outside). var (cellId, found) = engine.AdjustPosition(0xA9B40001u, new Vector3(12f, 12f, 50f)); Assert.True(found); Assert.Equal(LandblockId, cellId & 0xFFFF0000u); // correct landblock prefix Assert.True((cellId & 0xFFFFu) < 0x0100u); // an outdoor landcell low byte } // ── fixture ──────────────────────────────────────────────────────────── private static PhysicsEngine BuildIndoorEngine() { var cache = new PhysicsDataCache(); var engine = new PhysicsEngine { DataCache = cache }; cache.RegisterCellStructForTest(FeetCellId, MakeCell(InteriorYAtMost(3f), new uint[] { RoomCellId })); cache.RegisterCellStructForTest(RoomCellId, MakeCell(InteriorYAtLeast(7f), Array.Empty())); // A flat stub landblock so the outdoor branch has a grid to snap to. var heights = new byte[81]; var heightTable = new float[256]; engine.AddLandblock( landblockId: LandblockId, terrain: new TerrainSurface(heights, heightTable), cells: Array.Empty(), portals: Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f); return engine; } private static CellBSPNode InteriorYAtMost(float boundary) => new() { SplittingPlane = new Plane(new Vector3(0f, -1f, 0f), boundary), PosNode = new CellBSPNode { Type = BSPNodeType.Leaf }, }; private static CellBSPNode InteriorYAtLeast(float boundary) => new() { SplittingPlane = new Plane(new Vector3(0f, 1f, 0f), -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), }; }