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);
+ }
+}