From 0e27a6cc3f07b9863e4fb73eed10e1e00204c0c0 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 2 Jun 2026 10:17:09 +0200 Subject: [PATCH] =?UTF-8?q?feat(core):=20UCG=20W2=20Task=201=20=E2=80=94?= =?UTF-8?q?=20ResolveCellId=20writes=20CellGraph.CurrCell=20(additive)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add private `SetCurrAndReturn(uint)` helper in PhysicsEngine that looks up the resolved id in `DataCache.CellGraph` and writes `CurrCell` when the cell is present. Wrap the four RESOLVED-id return sites in ResolveCellId: - indoor no-CellBSP return (trust FindCellList) - indoor sphere-overlaps-CellBSP return - outdoor→indoor building-transit return (foreach candidate) - outdoor terrain-grid return The final no-match `return fallbackCellId;` is intentionally NOT wrapped — stale beats null (the caller's seed is preserved unchanged). CurrCell has zero readers in src/ (verified by ripgrep); this is additive write-only, identical observable behavior to W1. One new unit test (CellGraphMembershipTests) proves RED→GREEN. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.Core/Physics/PhysicsEngine.cs | 21 ++++++++-- .../Physics/CellGraphMembershipTests.cs | 39 +++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 tests/AcDream.Core.Tests/Physics/CellGraphMembershipTests.cs diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index b9cf6ec..562d204 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -256,6 +256,19 @@ public sealed class PhysicsEngine /// Design: docs/superpowers/specs/2026-05-19-indoor-portal-cell-tracking-design.md /// /// + /// + /// UCG W2 Task 1: record the resolved cell as the single membership answer. Additive — + /// CurrCell is written here; it has no reader yet (render-read is a later task). + /// Leaves CurrCell unchanged when the id can't be resolved in the graph (stale beats null). + /// Retail anchor: CPhysicsObj::set_cell_id (acclient_2013_pseudo_c.txt). + /// + private uint SetCurrAndReturn(uint resolvedId) + { + if (DataCache?.CellGraph is { } cg && cg.GetVisible(resolvedId) is { } cell) + cg.CurrCell = cell; + return resolvedId; + } + internal uint ResolveCellId(Vector3 worldPos, float sphereRadius, uint fallbackCellId) { if (fallbackCellId == 0) return 0; @@ -307,7 +320,7 @@ public sealed class PhysicsEngine // optionally re-enter an indoor cell via CheckBuildingTransit. var indoorCell = DataCache.GetCellStruct(indoorResult); if (indoorCell?.CellBSP?.Root is null) - return indoorResult; // Can't verify (no CellBSP); trust FindCellList. + return SetCurrAndReturn(indoorResult); // Can't verify (no CellBSP); trust FindCellList. // Issue #90 fix (2026-05-20): use SPHERE-overlap instead of POINT-in // for the indoor verification. The previous point-only check caused @@ -325,7 +338,7 @@ public sealed class PhysicsEngine // BSPTREE::sphere_intersects_cell_bsp at :323267. var localCenter = Vector3.Transform(worldPos, indoorCell.InverseWorldTransform); if (BSPQuery.SphereIntersectsCellBsp(indoorCell.CellBSP.Root, localCenter, sphereRadius)) - return indoorResult; + return SetCurrAndReturn(indoorResult); // Fall through to outdoor resolution: player has FULLY left the // indoor portal-connected graph (sphere no longer overlaps). @@ -359,12 +372,12 @@ public sealed class PhysicsEngine { // First candidate wins — building portal containment is // mutually exclusive in retail (one interior cell per portal). - foreach (var c in candidates) return c; + foreach (var c in candidates) return SetCurrAndReturn(c); } } } - return outdoorCellId; + return SetCurrAndReturn(outdoorCellId); } } diff --git a/tests/AcDream.Core.Tests/Physics/CellGraphMembershipTests.cs b/tests/AcDream.Core.Tests/Physics/CellGraphMembershipTests.cs new file mode 100644 index 0000000..15faa9d --- /dev/null +++ b/tests/AcDream.Core.Tests/Physics/CellGraphMembershipTests.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Numerics; +using AcDream.Core.Physics; +using AcDream.Core.World.Cells; +using DatReaderWriter.Types; +using Xunit; +using DatEnvCell = DatReaderWriter.DBObjs.EnvCell; + +namespace AcDream.Core.Tests.Physics; + +public class CellGraphMembershipTests +{ + [Fact] + public void ResolveCellId_Resolved_WritesCurrCellTrackingTheResolvedId() + { + var engine = new PhysicsEngine(); + var cache = new PhysicsDataCache(); + engine.DataCache = cache; + + var cs = new CellStruct { + VertexArray = new VertexArray { Vertices = new Dictionary() }, + Polygons = new Dictionary(), + PhysicsBSP = null, + }; + var dat = new DatEnvCell { + Flags = (DatReaderWriter.Enums.EnvCellFlags)0, + CellPortals = new List(), + VisibleCells = new List(), + }; + cache.CacheCellStruct(0xA9B40174u, dat, cs, Matrix4x4.Identity); // registers in the graph (W1) + + uint result = engine.ResolveCellId(new Vector3(0, 0, 0), 0.5f, 0xA9B40174u); + + // CurrCell tracks whatever id ResolveCellId returned (when that id is in the graph). + Assert.NotNull(cache.CellGraph.CurrCell); + Assert.Equal(result, cache.CellGraph.CurrCell!.Id); + Assert.Equal(0xA9B40174u, cache.CellGraph.CurrCell!.Id); + } +}