feat(core): UCG W2 Task 1 — ResolveCellId writes CellGraph.CurrCell (additive)

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) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-02 10:17:09 +02:00
parent 83c452b87f
commit 0e27a6cc3f
2 changed files with 56 additions and 4 deletions

View file

@ -256,6 +256,19 @@ public sealed class PhysicsEngine
/// Design: <c>docs/superpowers/specs/2026-05-19-indoor-portal-cell-tracking-design.md</c>
/// </para>
/// </summary>
/// <summary>
/// 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).
/// </summary>
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);
}
}