diff --git a/src/AcDream.Core/Physics/CellTransit.cs b/src/AcDream.Core/Physics/CellTransit.cs index 564a46f..bd62333 100644 --- a/src/AcDream.Core/Physics/CellTransit.cs +++ b/src/AcDream.Core/Physics/CellTransit.cs @@ -223,7 +223,8 @@ public static class CellTransit /// finds the cell whose contains /// the sphere center, and returns its full id (landblock-prefixed). /// Falls back to when no candidate - /// matches. + /// matches. The candidate set built internally is discarded; use + /// to recover it. /// /// /// @@ -238,7 +239,47 @@ public static class CellTransit float sphereRadius, uint currentCellId) { - var candidates = new HashSet(); + return BuildCellSetAndPickContaining( + cache, worldSphereCenter, sphereRadius, currentCellId, + out _); + } + + /// + /// Phase A4 (2026-05-20). Same portal-graph traversal as + /// but additionally returns the full + /// candidate set built during traversal. Used by + /// to iterate every cell + /// the sphere overlaps for per-cell BSP collision. + /// + /// + /// Retail oracle: CTransition::check_other_cells at + /// acclient_2013_pseudo_c.txt:272717-272798 calls + /// CObjCell::find_cell_list(&this->cell_array, &var_4c, ...) + /// which fills both the cell_array (set) and var_4c (containing cell). + /// + /// + public static uint FindCellSet( + PhysicsDataCache cache, + Vector3 worldSphereCenter, + float sphereRadius, + uint currentCellId, + out IReadOnlyCollection cellSet) + { + var containing = BuildCellSetAndPickContaining( + cache, worldSphereCenter, sphereRadius, currentCellId, + out var candidates); + cellSet = candidates; + return containing; + } + + private static uint BuildCellSetAndPickContaining( + PhysicsDataCache cache, + Vector3 worldSphereCenter, + float sphereRadius, + uint currentCellId, + out HashSet candidates) + { + candidates = new HashSet(); uint currentLow = currentCellId & 0xFFFFu; if (currentLow >= 0x0100u) diff --git a/tests/AcDream.Core.Tests/Physics/CellTransitFindCellSetTests.cs b/tests/AcDream.Core.Tests/Physics/CellTransitFindCellSetTests.cs new file mode 100644 index 0000000..76917e9 --- /dev/null +++ b/tests/AcDream.Core.Tests/Physics/CellTransitFindCellSetTests.cs @@ -0,0 +1,137 @@ +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using DatReaderWriter.Enums; +using DatReaderWriter.Types; +using AcDream.Core.Physics; +using Xunit; + +namespace AcDream.Core.Tests.Physics; + +public class CellTransitFindCellSetTests +{ + // ────────────────────────────────────────────────────────────────── + // Helpers — mirror CellTransitFindTransitCellsSphereTests.cs pattern + // ────────────────────────────────────────────────────────────────── + + private static CellPhysics MakeCellWithPortalAtRightWall( + Matrix4x4 worldTransform, uint otherCellId, ushort flags) + { + var portalPoly = new ResolvedPolygon + { + Vertices = new[] + { + new Vector3(2.5f, -2.5f, 0f), + new Vector3(2.5f, 2.5f, 0f), + new Vector3(2.5f, 2.5f, 5f), + new Vector3(2.5f, -2.5f, 5f), + }, + Plane = new Plane(new Vector3(1, 0, 0), -2.5f), // x = 2.5 + NumPoints = 4, + SidesType = CullMode.None, + }; + + Matrix4x4.Invert(worldTransform, out var inv); + return new CellPhysics + { + WorldTransform = worldTransform, + InverseWorldTransform = inv, + Resolved = new Dictionary(), + PortalPolygons = new Dictionary { [10] = portalPoly }, + Portals = new[] + { + new PortalInfo(otherCellId: (ushort)otherCellId, polygonId: 10, flags: flags), + }, + CellBSP = new CellBSPTree + { + Root = new CellBSPNode { Type = BSPNodeType.Leaf }, + } + }; + } + + // ────────────────────────────────────────────────────────────────── + // Tests + // ────────────────────────────────────────────────────────────────── + + [Fact] + public void Sphere_FullyInsidePrimaryCell_ReturnsOnlyPrimary() + { + var cellA = MakeCellWithPortalAtRightWall(Matrix4x4.Identity, otherCellId: 0x0101, flags: 0); + var cache = new PhysicsDataCache(); + cache.RegisterCellStructForTest(0xA9B40100u, cellA); + + // Sphere far from any portal — local x=-1, reach to x=-0.5; portal at x=2.5. + var sphereCenter = new Vector3(-1.0f, 0f, 2.5f); + + uint containing = CellTransit.FindCellSet( + cache, sphereCenter, sphereRadius: 0.5f, + currentCellId: 0xA9B40100u, + out var cellSet); + + Assert.Equal(0xA9B40100u, containing); + Assert.Single(cellSet); + Assert.Contains(0xA9B40100u, cellSet); + } + + [Fact] + public void Sphere_StraddlingPortal_ReturnsBothCells() + { + var cellA = MakeCellWithPortalAtRightWall(Matrix4x4.Identity, otherCellId: 0x0101, flags: 0); + var cellBT = Matrix4x4.CreateTranslation(new Vector3(5f, 0f, 0f)); + Matrix4x4.Invert(cellBT, out var cellBInv); + var cellB = new CellPhysics + { + WorldTransform = cellBT, + InverseWorldTransform = cellBInv, + Resolved = new Dictionary(), + CellBSP = new CellBSPTree + { + Root = new CellBSPNode { Type = BSPNodeType.Leaf }, + } + }; + + var cache = new PhysicsDataCache(); + cache.RegisterCellStructForTest(0xA9B40100u, cellA); + cache.RegisterCellStructForTest(0xA9B40101u, cellB); + + // Sphere center at local x=2.0, radius=0.5 → reaches x=2.5 = portal plane. + var sphereCenter = new Vector3(2.0f, 0f, 2.5f); + + uint containing = CellTransit.FindCellSet( + cache, sphereCenter, sphereRadius: 0.5f, + currentCellId: 0xA9B40100u, + out var cellSet); + + Assert.Contains(0xA9B40100u, cellSet); + Assert.Contains(0xA9B40101u, cellSet); + } + + [Fact] + public void FindCellSet_OutdoorSeed_IncludesNeighbourLandcells() + { + var cache = new PhysicsDataCache(); + // Outdoor seed near a cell boundary — expand to neighbours via + // AddAllOutsideCells. Landcells have no CellPhysics in cache, so + // they appear in the set but the containing-cell loop falls back + // to currentCellId. The point of this test: the SET captures + // them even though FindCellList's single-uint return cannot. + // + // World coords for landblock 0xA9B4FFFF: origin at + // (0xA9*192, 0xB4*192) = (32448, 34560). Cell grid(0,0) covers + // world XY in [(32448,34560), (32472,34584)). Place the sphere + // center near the east boundary of grid(0,0) so AddAllOutsideCells + // adds the east neighbour grid(1,0). + uint lbPrefix = 0xA9B40000u; + float lbX = ((lbPrefix >> 24) & 0xFFu) * 192f; + float lbY = ((lbPrefix >> 16) & 0xFFu) * 192f; + var sphereCenter = new Vector3(lbX + 23.8f, lbY + 12f, 0f); + + uint containing = CellTransit.FindCellSet( + cache, sphereCenter, sphereRadius: 0.5f, + currentCellId: 0xA9B40001u, // outdoor cell, low byte < 0x100 + out var cellSet); + + Assert.Equal(0xA9B40001u, containing); + Assert.True(cellSet.Count >= 2, $"Expected ≥2 cells in set (primary + east neighbour), got {cellSet.Count}"); + } +}