using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; using AcDream.Core.Physics; using DatReaderWriter.Enums; using DatReaderWriter.Types; using Xunit; namespace AcDream.App.Tests.Rendering; /// /// Render Residual A — the verbatim SmartBox::update_viewer /// (acclient_2013_pseudo_c.txt:92761) orchestration in /// : seat the sweep's start /// cell at the head-PIVOT when indoor (via AdjustPosition → /// find_visible_child_cell), and snap the eye to the player when there is /// no start cell / the sweep fails. /// public class CameraCollisionUpdateViewerTests { private const uint FeetCellId = 0xA9B40174u; // cellar (low ≥ 0x0100 → indoor), interior Z ≤ 94 private const uint RoomCellId = 0xA9B40171u; // cottage floor above, interior Z ≥ 94, in feet cell's stab list private const uint LandblockId = 0xA9B40000u; /// /// The cellar-lip case (user point 3): the player's FEET are in the low cellar /// cell, but the head-PIVOT is up at cottage-floor level in a different cell. /// Retail seats the sweep at the pivot's cell (AdjustPosition, pc:92832), /// so the viewer cell is the room above — NOT the feet cell. Without the start-cell /// port the sweep stays in the feet cell. /// [Fact] public void SweepEye_IndoorPivotInCellAboveFeet_SeatsStartAtPivotCell() { var engine = BuildTwoCellEngine(); var probe = new PhysicsCameraCollisionProbe(engine); var feet = new Vector3(0f, 0f, 93f); // in the feet cell (Z ≤ 94) var pivot = new Vector3(0f, 0f, 94.5f); // head, up in the room cell (Z ≥ 94) var eye = new Vector3(0f, 3f, 95.5f); // behind + up, still in the room region, no wall var result = probe.SweepEye(pivot, eye, cellId: FeetCellId, selfEntityId: 0u, playerPos: feet); Assert.Equal(RoomCellId, result.ViewerCellId); } /// /// Retail update_viewer snaps the viewer to the player position when the /// player has no cell (pc:92775) and as fallback 2 when the sweep fails /// (pc:92886): set_viewer(player_pos); viewer_cell = null. /// [Fact] public void SweepEye_NoStartCell_SnapsToPlayer() { var probe = new PhysicsCameraCollisionProbe(new PhysicsEngine()); var player = new Vector3(7f, 8f, 9f); var result = probe.SweepEye( pivot: new Vector3(7f, 8f, 10.5f), desiredEye: new Vector3(7f, 13f, 11f), cellId: 0u, selfEntityId: 0u, playerPos: player); Assert.Equal(player, result.Eye); Assert.Equal(0u, result.ViewerCellId); } // ── fixture ──────────────────────────────────────────────────────────── private static PhysicsEngine BuildTwoCellEngine() { var cache = new PhysicsDataCache(); var engine = new PhysicsEngine { DataCache = cache }; // Feet cell: interior Z ≤ 94, in its stab list the room cell above. No portals // (so the collision sweep cannot transit to the room — the start cell is decisive). cache.RegisterCellStructForTest(FeetCellId, MakeCell(InteriorZAtMost(94f), new uint[] { RoomCellId })); // Room cell: interior Z ≥ 94, no walls, no portals. cache.RegisterCellStructForTest(RoomCellId, MakeCell(InteriorZAtLeast(94f), Array.Empty())); var heights = new byte[81]; var heightTable = new float[256]; for (int i = 0; i < 256; i++) heightTable[i] = -1000f; engine.AddLandblock( landblockId: LandblockId, terrain: new TerrainSurface(heights, heightTable), cells: Array.Empty(), portals: Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f); return engine; } private static CellBSPNode InteriorZAtMost(float boundary) => new() { SplittingPlane = new Plane(new Vector3(0f, 0f, -1f), boundary), // dist = boundary − Z ≥ 0 ⇔ Z ≤ boundary PosNode = new CellBSPNode { Type = BSPNodeType.Leaf }, }; private static CellBSPNode InteriorZAtLeast(float boundary) => new() { SplittingPlane = new Plane(new Vector3(0f, 0f, 1f), -boundary), // dist = Z − boundary ≥ 0 ⇔ Z ≥ 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), }; }