From d23d1f40dc76a012ee9e8aa68cafcdbdde0e734e Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 2 Jun 2026 14:09:42 +0200 Subject: [PATCH] =?UTF-8?q?Revert=20"feat(core):=20UCG=20W2=20Task=203=20?= =?UTF-8?q?=E2=80=94=20stab-list=20doorway=20hysteresis=20in=20ResolveCell?= =?UTF-8?q?Id"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 2acd8f9e1d19ec301c8d0bda645f39ad73c93218. --- src/AcDream.Core/Physics/PhysicsEngine.cs | 43 ---- .../Physics/CellGraphMembershipTests.cs | 203 ------------------ 2 files changed, 246 deletions(-) diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index 08f062a..5d68e23 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Numerics; namespace AcDream.Core.Physics; @@ -52,27 +51,6 @@ public sealed class PhysicsEngine float WorldOffsetX, float WorldOffsetY); - /// - /// UCG W2b doorway-hysteresis band (metres) added to the foot-sphere radius when deciding - /// whether to HOLD the previous indoor cell at the indoor→outdoor seam. Must exceed the - /// ~10 cm push-back oscillation at the threshold (handoff §5: front-door 0170↔0031) while - /// still releasing on a genuine step-out. The single value the visual gate may tune. - /// - private const float DoorwayHoldMargin = 0.2f; - - /// - /// UCG W2b: does the foot-sphere (at , inflated to - /// ) overlap 's containment BSP? Returns false - /// when the cell has no containment BSP (can't test ⇒ release, never hold forever). - /// Retail: CCellStruct::sphere_intersects_cell (acclient_2013_pseudo_c.txt:317666). - /// - private static bool SphereOverlapsEnvCell(World.Cells.EnvCell cell, Vector3 worldPos, float radius) - { - if (cell.ContainmentBsp?.Root is null) return false; - var local = Vector3.Transform(worldPos, cell.InverseWorldTransform); - return BSPQuery.SphereIntersectsCellBsp(cell.ContainmentBsp.Root, local, radius); - } - /// /// Register a landblock with its terrain surface, indoor cells, portal /// planes, and world-space origin offset. @@ -399,27 +377,6 @@ public sealed class PhysicsEngine } } - // UCG W2b — retail find_cell_list do_not_load_cells prune - // (acclient_2013_pseudo_c.txt:308829-308867). Retail drops from the candidate - // cell-array any cell that is neither the current cell nor in its stab list - // (VisibleCells). Adapted here as doorway hysteresis at the indoor→outdoor seam: - // if the previous membership answer is an indoor cell and the outdoor candidate - // is NOT in its stab list (outdoor landcells never are), HOLD the indoor cell - // while the foot-sphere still overlaps that cell's containment BSP expanded by - // DoorwayHoldMargin. The strict overlap check at the indoor branch (~line 340) - // just released (the center was pushed past the wall), but the expanded test holds - // us across the ~10 cm push-back oscillation at the threshold (handoff §5: - // 0170↔0031). Releases on a genuine step-out — by then the bare overlap is also - // false, so CheckBuildingTransit won't re-grab the cell next tick. Anti-ping-pong - // the #98 saga never had; fires only at the front-door seam (the cellar has no - // exit portal → it never falls through here → #98 cellar-up is untouched). - if (DataCache?.CellGraph.CurrCell is World.Cells.EnvCell prevCell - && !prevCell.StabList.Contains(outdoorCellId) - && SphereOverlapsEnvCell(prevCell, worldPos, sphereRadius + DoorwayHoldMargin)) - { - return SetCurrAndReturn(prevCell.Id); - } - return SetCurrAndReturn(outdoorCellId); } } diff --git a/tests/AcDream.Core.Tests/Physics/CellGraphMembershipTests.cs b/tests/AcDream.Core.Tests/Physics/CellGraphMembershipTests.cs index 0801243..15faa9d 100644 --- a/tests/AcDream.Core.Tests/Physics/CellGraphMembershipTests.cs +++ b/tests/AcDream.Core.Tests/Physics/CellGraphMembershipTests.cs @@ -1,13 +1,10 @@ -using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Physics; using AcDream.Core.World.Cells; -using DatReaderWriter.Enums; using DatReaderWriter.Types; using Xunit; using DatEnvCell = DatReaderWriter.DBObjs.EnvCell; -using UcgCellPortal = AcDream.Core.World.Cells.CellPortal; namespace AcDream.Core.Tests.Physics; @@ -39,204 +36,4 @@ public class CellGraphMembershipTests Assert.Equal(result, cache.CellGraph.CurrCell!.Id); Assert.Equal(0xA9B40174u, cache.CellGraph.CurrCell!.Id); } - - // ── UCG W2 Task 3 — stab-list doorway hysteresis tests ─────────────────── - // - // Geometry: indoor cell 0xA9B40175 with a CellBSP splitting plane at Y=3.5, - // normal (0,-1,0). The interior is Y ≤ 3.5 (positive side of the plane). - // - // SphereIntersectsCellBsp with radius=0.3 fails (returns false) when: - // dist = dot((0,-1,0), center) + 3.5 = 3.5 - center.Y < -(0.3 + 0.01) = -0.31 - // i.e. center.Y > 3.81 - // - // SphereIntersectsCellBsp with expanded radius=0.3+0.2=0.5 fails when: - // center.Y > 3.5 + 0.51 = 4.01 - // - // So the hysteresis band is Y ∈ (3.81, 4.01]: - // - Strict check failed (falls through to outdoor) - // - Expanded check passes (hold the indoor cell) - // - // Test positions: - // Y=3.9 → strict fails, expanded holds → hysteresis HOLD - // Y=4.5 → both fail → genuine step-out, RELEASE - // - // The landblock is registered at worldOffset=(0,0) so localX=worldPos.X, - // localY=worldPos.Y. At X=0, Y=3.9: cx=0, cy=0 → lowCellId=1 → - // outdoorCellId = 0xA9B40001. - // - // Retail anchor: CObjCell::find_cell_list do_not_load_cells prune - // (acclient_2013_pseudo_c.txt:308829-308867). - - private const uint DoorwayIndoorId = 0xA9B40175u; - private const uint DoorwayLbId = 0xA9B40000u; - private const float SphereRadius = 0.3f; - // Y at which strict check fails but expanded (margin=0.2) still holds. - private static readonly Vector3 HoldPos = new(0f, 3.9f, 95f); - // Y at which both strict and expanded fail (genuine step-out). - private static readonly Vector3 ReleasedPos = new(0f, 4.5f, 95f); - - /// - /// Builds the engine+cache+prevCell fixture shared by the three hysteresis tests. - /// controls whether the outdoor candidate is in the stab list. - /// controls whether CellGraph.CurrCell is pre-set to prev. - /// Returns the engine, cache, and the EnvCell registered as the previous indoor membership. - /// - private static (PhysicsEngine engine, PhysicsDataCache cache, EnvCell prev) - BuildDoorwayFixture(IReadOnlyList stabList, bool setCurrCell = true) - { - var cache = new PhysicsDataCache(); - var engine = new PhysicsEngine { DataCache = cache }; - - // ── CellBSP: plane at Y=3.5 with normal (0,-1,0) ───────────────── - // Interior is at Y ≤ 3.5 (positive-side leaf). - // SphereIntersectsCellBsp(root, center, r) checks: - // dist = dot((0,-1,0), center) + 3.5 = 3.5 - center.Y - // if dist < -(r + 0.01) → sphere is fully outside → return false - var cellBspLeaf = new CellBSPNode { Type = BSPNodeType.Leaf }; - var cellBspRoot = new CellBSPNode - { - SplittingPlane = new Plane(new Vector3(0f, -1f, 0f), 3.5f), - PosNode = cellBspLeaf, - }; - var cellBsp = new CellBSPTree { Root = cellBspRoot }; - - // ── CellPhysics registered for the indoor sphere-check branch ───── - // This is what ResolveCellId's indoor branch looks up via GetCellStruct. - // We set CellBSP to cellBsp so the overlap test uses it. - var indoorCell = new CellPhysics - { - BSP = new PhysicsBSPTree { Root = new PhysicsBSPNode { Type = BSPNodeType.Leaf } }, - WorldTransform = Matrix4x4.Identity, - InverseWorldTransform = Matrix4x4.Identity, - Resolved = new Dictionary(), - CellBSP = cellBsp, - Portals = Array.Empty(), - PortalPolygons = new Dictionary(), - VisibleCellIds = new System.Collections.Generic.HashSet(), - }; - cache.RegisterCellStructForTest(DoorwayIndoorId, indoorCell); - - // ── EnvCell in the CellGraph with the same ContainmentBsp ───────── - // This is what the hysteresis prune reads from CellGraph.CurrCell. - // We use the SAME cellBsp instance so the test geometry is consistent. - var prev = new EnvCell( - id: DoorwayIndoorId, - worldTransform: Matrix4x4.Identity, - inverseWorldTransform: Matrix4x4.Identity, - localBoundsMin: new Vector3(-20f, -20f, -20f), - localBoundsMax: new Vector3(20f, 20f, 20f), - portals: Array.Empty(), - stabList: stabList, - seenOutside: true, - containmentBsp: cellBsp); - - cache.CellGraph.Add(prev); - if (setCurrCell) - cache.CellGraph.CurrCell = prev; - - // ── Stub landblock: terrain far below, worldOffset=(0,0) ────────── - // worldPos.X, worldPos.Y map directly to localX, localY. - // At X=0, Y=3.9: cx=0, cy=0 → lowCellId=1 → outdoorCellId=0xA9B40001. - var heights = new byte[81]; - var heightTable = new float[256]; - for (int i = 0; i < 256; i++) heightTable[i] = -1000f; - var stubTerrain = new TerrainSurface(heights, heightTable); - engine.AddLandblock( - landblockId: DoorwayLbId, - terrain: stubTerrain, - cells: Array.Empty(), - portals: Array.Empty(), - worldOffsetX: 0f, - worldOffsetY: 0f); - - return (engine, cache, prev); - } - - /// - /// UCG W2 Task 3 — HOLD (the RED→GREEN test). - /// - /// Position Y=3.9 is inside the hysteresis band: - /// - The strict overlap check (radius=0.3) fails → indoor branch falls through to outdoor. - /// - The expanded check (radius=0.3+0.2=0.5) still passes → hold the indoor cell. - /// - The outdoor candidate (0xA9B40001) is NOT in the stab list → prune fires. - /// - /// Expected: ResolveCellId returns 0xA9B40175 (held indoor). - /// Before the fix: returns 0xA9B40001 (outdoor — ping-pong side). - /// After the fix: returns 0xA9B40175 (held indoor — anti-ping-pong). - /// - /// Retail anchor: CObjCell::find_cell_list do_not_load_cells prune - /// (acclient_2013_pseudo_c.txt:308829-308867). - /// - [Fact] - public void ResolveCellId_DoorwayHysteresis_HoldsIndoorWhenInsideMargin() - { - var (engine, _, _) = BuildDoorwayFixture( - stabList: Array.Empty(), - setCurrCell: true); - - uint result = engine.ResolveCellId(HoldPos, SphereRadius, DoorwayIndoorId); - - Assert.Equal(DoorwayIndoorId, result); - } - - /// - /// UCG W2 Task 3 — RELEASE when fully outside (genuine step-out). - /// - /// Position Y=4.5 exceeds the expanded margin (4.01) — both the strict - /// and expanded overlap checks fail. The prune must NOT hold — the player - /// genuinely stepped out. - /// - /// Expected: ResolveCellId returns an outdoor cell id (low 16 bits < 0x100) - /// that is NOT 0xA9B40175. - /// - [Fact] - public void ResolveCellId_DoorwayHysteresis_ReleasesWhenFullyOutside() - { - var (engine, _, _) = BuildDoorwayFixture( - stabList: Array.Empty(), - setCurrCell: true); - - uint result = engine.ResolveCellId(ReleasedPos, SphereRadius, DoorwayIndoorId); - - // Must be an outdoor cell (low 16 bits < 0x100) and not the indoor cell. - Assert.NotEqual(DoorwayIndoorId, result); - Assert.True((result & 0xFFFFu) < 0x100u, - $"Expected an outdoor cell (low 16 < 0x100), got 0x{result:X8}"); - } - - /// - /// UCG W2 Task 3 — RELEASE when candidate is visible (stab-list gate). - /// - /// The prune only fires when the outdoor candidate is NOT in the previous - /// indoor cell's stab list. If the candidate IS in the stab list (genuinely - /// visible from the indoor cell), the transition should proceed normally — - /// hysteresis must not block it. - /// - /// Expected: ResolveCellId returns the outdoor cell id even though Y=3.9 - /// is inside the hysteresis band. - /// - [Fact] - public void ResolveCellId_DoorwayHysteresis_ReleasesWhenCandidateIsInStabList() - { - // Step 1: determine the outdoor id the engine picks at Y=3.9 - // by running with CurrCell=null (no hysteresis). - var (engineNull, _, _) = BuildDoorwayFixture( - stabList: Array.Empty(), - setCurrCell: false); - uint outdoorId = engineNull.ResolveCellId(HoldPos, SphereRadius, DoorwayIndoorId); - - // Sanity: must be outdoor (low 16 < 0x100). - Assert.True((outdoorId & 0xFFFFu) < 0x100u, - $"Expected outdoor fallback id (low 16 < 0x100), got 0x{outdoorId:X8}"); - - // Step 2: build a fresh fixture with the outdoor id IN the stab list. - var (engine, _, _) = BuildDoorwayFixture( - stabList: new uint[] { outdoorId }, - setCurrCell: true); - - uint result = engine.ResolveCellId(HoldPos, SphereRadius, DoorwayIndoorId); - - // Stab-list gate: outdoor candidate is visible → prune must NOT hold. - Assert.Equal(outdoorId, result); - } }