using System.Collections.Generic; using System.Numerics; using AcDream.Core.Physics; using Xunit; namespace AcDream.Core.Tests.Physics; public class CellTransitCheckBuildingTransitTests { [Fact] public void BuildingPortalWithUnloadedCellBSP_NoCandidateAdded() { // Verifies the null-CellBSP guard: when the destination interior cell // is cached but its CellBSP isn't yet loaded (or is structurally absent), // CheckBuildingTransit must NOT add the cell to candidates — even though // PointInsideCellBsp(null, _) returns true. // // Happy-path (CellBSP present, sphere inside) requires a synthetic // CellBSPTree which is non-trivial to construct from DatReaderWriter // types. Deferred to visual verification. // Building at world origin. One portal to interior cell 0xA9B40100. var building = new BuildingPhysics { WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity, Portals = new[] { new BldPortalInfo( otherCellId: 0xA9B40100u, otherPortalId: 0, flags: 0), }, }; // Interior cell with null CellBSP — PointInsideCellBsp(null, _) returns true, // but CheckBuildingTransit guards on CellBSP?.Root being non-null, so this // cell is skipped. var interiorCell = new CellPhysics { WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity, Resolved = new Dictionary(), }; var cache = new PhysicsDataCache(); cache.RegisterCellStructForTest(0xA9B40100u, interiorCell); var candidates = new HashSet(); CellTransit.CheckBuildingTransit( cache, building, worldSphereCenter: new Vector3(0, 0, 0), sphereRadius: 0.5f, candidates); // CellBSP is null → containment guard (otherCell?.CellBSP?.Root is null) // skips this cell. No candidate added. Assert.Empty(candidates); } /// /// BR-7 / A6.P4 C1 (2026-06-11): retail's first gate in /// CEnvCell::check_building_transit (Ghidra 0x0052c5dc) is /// if (other_portal_id >= 0) on the SIGNED sign-extended /// portal id — a portal whose dat value is 0xFFFF (-1, "no reciprocal /// portal") never admits its interior cell, even when a sphere /// overlaps it. A leaf-root CellBSP makes the sphere test /// unconditionally "inside", so the only thing separating the two /// portals below is the sign of OtherPortalId. /// [Fact] public void NegativeOtherPortalId_RejectsTransit_PositiveAdmits() { var leafBsp = new DatReaderWriter.Types.CellBSPTree { Root = new DatReaderWriter.Types.CellBSPNode { Type = DatReaderWriter.Enums.BSPNodeType.Leaf, }, }; CellPhysics MakeLeafCell() => new CellPhysics { WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity, Resolved = new Dictionary(), CellBSP = leafBsp, }; var cache = new PhysicsDataCache(); cache.RegisterCellStructForTest(0xA9B40101u, MakeLeafCell()); cache.RegisterCellStructForTest(0xA9B40102u, MakeLeafCell()); var building = new BuildingPhysics { WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity, Portals = new[] { // Wire 0xFFFF reinterpreted signed = -1 → must be skipped. new BldPortalInfo( otherCellId: 0xA9B40101u, otherPortalId: unchecked((short)0xFFFF), flags: 0), // Non-negative id → admitted (leaf BSP ⇒ sphere inside). new BldPortalInfo( otherCellId: 0xA9B40102u, otherPortalId: 0, flags: 0), }, }; var candidates = new HashSet(); CellTransit.CheckBuildingTransit( cache, building, worldSphereCenter: new Vector3(0, 0, 0), sphereRadius: 0.5f, candidates); Assert.DoesNotContain(0xA9B40101u, candidates); Assert.Contains(0xA9B40102u, candidates); } /// /// Multi-sphere form (retail per-sphere loop at 0052c5fe): the first /// intersecting sphere admits the cell and sets /// hitsInteriorCell (retail SPHEREPATH.hits_interior_cell write /// at 0052c650). All-spheres-outside ⇒ no admit, flag false. /// [Fact] public void MultiSphere_AnySphereAdmits_SetsHitsInteriorCell() { var leafBsp = new DatReaderWriter.Types.CellBSPTree { Root = new DatReaderWriter.Types.CellBSPNode { Type = DatReaderWriter.Enums.BSPNodeType.Leaf, }, }; var cache = new PhysicsDataCache(); cache.RegisterCellStructForTest(0xA9B40103u, new CellPhysics { WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity, Resolved = new Dictionary(), CellBSP = leafBsp, }); var building = new BuildingPhysics { WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity, Portals = new[] { new BldPortalInfo(0xA9B40103u, otherPortalId: 1, flags: 0), }, }; var spheres = new[] { new DatReaderWriter.Types.Sphere { Origin = new Vector3(500f, 0f, 0f), Radius = 0.5f }, new DatReaderWriter.Types.Sphere { Origin = Vector3.Zero, Radius = 0.5f }, }; var candidates = new HashSet(); CellTransit.CheckBuildingTransit( cache, building, spheres, spheres.Length, candidates, out bool hits); // Leaf-root BSP treats every sphere as inside, so even the far // sphere admits; the point of this test is the plumbing: candidates // populated + hitsInteriorCell set through the multi-sphere form. Assert.Contains(0xA9B40103u, candidates); Assert.True(hits); // Zero spheres → nothing admitted, flag stays false. var empty = new HashSet(); CellTransit.CheckBuildingTransit( cache, building, spheres, 0, empty, out bool hitsNone); Assert.Empty(empty); Assert.False(hitsNone); } }