docs: UCG W2 (one membership) spec + plan
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
07e68e0aff
commit
83c452b87f
2 changed files with 333 additions and 0 deletions
196
docs/superpowers/plans/2026-06-02-unified-cell-graph-stage2.md
Normal file
196
docs/superpowers/plans/2026-06-02-unified-cell-graph-stage2.md
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
# Unified Cell Graph — Stage 2 (W2: One Membership) Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use `- [ ]`.
|
||||
|
||||
**Goal:** Make physics's `ResolveCellId` the single cell-membership answer (`CellGraph.CurrCell`), have render read it, and add retail `find_cell_list` stab-list doorway hysteresis — collapsing the dual render/physics resolvers.
|
||||
|
||||
**Architecture:** Spec `docs/superpowers/specs/2026-06-02-unified-cell-graph-stage2-design.md`. W1's `CellGraph`/`ObjCell`/`EnvCell`/`LandCell` are in place. Task 1 is additive (write-only, zero-behavior-change). Tasks 2–3 are behavior-changing → **visual verification at the Holtburg cottage is the acceptance gate** (do not mark W2 done without it).
|
||||
|
||||
**Tech Stack:** C# .NET 10, xUnit. Branch `claude/thirsty-goldberg-51bb9b` (unpushed — do NOT push). Every commit ends with `Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
- Modify `src/AcDream.Core/Physics/PhysicsEngine.cs` — Task 1 (write `CurrCell`) + Task 3 (hysteresis).
|
||||
- Modify `src/AcDream.App/Rendering/CellVisibility.cs` + `src/AcDream.App/Rendering/GameWindow.cs` — Task 2 (render reads `CurrCell`).
|
||||
- Tests: `tests/AcDream.Core.Tests/Physics/CellGraphMembershipTests.cs` (T1, T3); `tests/AcDream.App.Tests/...` (T2, if an App test project pattern fits — else a Core-side test of the root-selection helper).
|
||||
|
||||
---
|
||||
|
||||
## Task 1: `ResolveCellId` writes `CellGraph.CurrCell` (additive, zero-behavior-change)
|
||||
|
||||
**Files:** Modify `src/AcDream.Core/Physics/PhysicsEngine.cs`; Test `tests/AcDream.Core.Tests/Physics/CellGraphMembershipTests.cs`.
|
||||
|
||||
- [ ] **Step 1: Confirm the return sites.** Read `PhysicsEngine.ResolveCellId` (`PhysicsEngine.cs:259-372`). Identify every `return <resolvedId>;` that returns a *resolved* cell (indoor result ~:310/:328; outdoor result ~:367-368) versus the no-match fallback `return fallbackCellId;` (~:371) which must leave `CurrCell` UNCHANGED. Confirm `DataCache` is the `PhysicsDataCache?` property (`:45`) exposing `.CellGraph` (W1).
|
||||
|
||||
- [ ] **Step 2: Write the failing test** at `tests/AcDream.Core.Tests/Physics/CellGraphMembershipTests.cs`:
|
||||
|
||||
```csharp
|
||||
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_IndoorResult_WritesCurrCellToTheEnvCell()
|
||||
{
|
||||
var engine = new PhysicsEngine();
|
||||
var cache = new PhysicsDataCache();
|
||||
engine.DataCache = cache;
|
||||
|
||||
// Register an EnvCell in the graph for the id ResolveCellId will return.
|
||||
// Use a minimal synthetic dat cell (no PhysicsBSP -> CacheCellStruct drops it from
|
||||
// _cellStruct but the graph add runs first, per W1 Task 7).
|
||||
var cs = new CellStruct {
|
||||
VertexArray = new VertexArray { Vertices = new Dictionary<ushort, SWVertex>() },
|
||||
Polygons = new Dictionary<ushort, Polygon>(),
|
||||
PhysicsBSP = null,
|
||||
};
|
||||
var dat = new DatEnvCell {
|
||||
Flags = (DatReaderWriter.Enums.EnvCellFlags)0,
|
||||
CellPortals = new List<DatReaderWriter.Types.CellPortal>(),
|
||||
VisibleCells = new List<ushort>(),
|
||||
};
|
||||
cache.CacheCellStruct(0xA9B40174u, dat, cs, Matrix4x4.Identity);
|
||||
|
||||
// With no physics cell data for collision, ResolveCellId on a bare indoor fallback
|
||||
// returns the fallback id (the indoor branch's early-return when no BSP confirms),
|
||||
// which is the indoor cell — assert CurrCell tracks it.
|
||||
engine.ResolveCellId(new Vector3(0,0,0), 0.5f, 0xA9B40174u);
|
||||
|
||||
Assert.NotNull(cache.CellGraph.CurrCell);
|
||||
Assert.Equal(0xA9B40174u, cache.CellGraph.CurrCell!.Id);
|
||||
}
|
||||
}
|
||||
```
|
||||
> Step 1 may show `ResolveCellId` returns the fallback id under these minimal conditions (no loaded landblock/cell-struct). If the exact return differs, adjust the *setup* so the test exercises a resolved-id return and asserts `CurrCell.Id == thatId` — keep the assertion shape (CurrCell tracks the resolved id). If `PhysicsEngine`/`DataCache` can't be constructed this simply, report NEEDS_CONTEXT.
|
||||
|
||||
- [ ] **Step 3: Run, verify FAIL:** `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj -c Debug --filter "FullyQualifiedName~CellGraphMembershipTests"`
|
||||
|
||||
- [ ] **Step 4: Implement.** Add a private helper to `PhysicsEngine`:
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// UCG W2: record the resolved cell as the single membership answer. Additive —
|
||||
/// CurrCell is written here and read by the render root (W2 Task 2). Leaves CurrCell
|
||||
/// unchanged when the id can't be resolved in the graph (stale beats null).
|
||||
/// </summary>
|
||||
private uint SetCurrAndReturn(uint resolvedId)
|
||||
{
|
||||
if (DataCache?.CellGraph is { } cg && cg.GetVisible(resolvedId) is { } cell)
|
||||
cg.CurrCell = cell;
|
||||
return resolvedId;
|
||||
}
|
||||
```
|
||||
Wrap each *resolved-id* return in `ResolveCellId` as `return SetCurrAndReturn(<id>);` (the indoor-result returns + the outdoor-result return). Leave the no-match `return fallbackCellId;` (~:371) as-is (unchanged CurrCell). Do NOT alter any existing logic — only wrap the returns.
|
||||
|
||||
- [ ] **Step 5: Run, verify PASS.** Same filter. Then full Core build green.
|
||||
|
||||
- [ ] **Step 6: Confirm still zero-behavior-change.** `rg "CurrCell" src` shows only the W1 declaration + this write (no reader yet). Commit:
|
||||
```
|
||||
git add src/AcDream.Core/Physics/PhysicsEngine.cs tests/AcDream.Core.Tests/Physics/CellGraphMembershipTests.cs
|
||||
git commit -m "feat(core): UCG W2 Task 1 — ResolveCellId writes CellGraph.CurrCell (additive)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Render reads `CurrCell` (BEHAVIOR-CHANGING → visual gate)
|
||||
|
||||
**Files:** Modify `src/AcDream.App/Rendering/CellVisibility.cs`, `src/AcDream.App/Rendering/GameWindow.cs`.
|
||||
|
||||
- [ ] **Step 1: Confirm the render path.** Read `CellVisibility.ComputeVisibility`/`GetVisibleCells`/`FindCameraCell` (`CellVisibility.cs:327-413,488+`) and the call site `GameWindow.cs:7153-7157,7295`. Confirm: `TryGetCell(uint, out LoadedCell)` exists (`:286`); how `GameWindow` can reach the physics `CellGraph.CurrCell` (via `_physicsEngine.DataCache?.CellGraph` — confirm `_physicsEngine` field + `DataCache` accessibility). Report what you find before editing.
|
||||
|
||||
- [ ] **Step 2: Add `ComputeVisibilityFromRoot`.** In `CellVisibility`, add an overload that runs the existing portal BFS from a supplied root cell, falling back to the position path when the root is null:
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// UCG W2: compute visibility from a supplied root cell (the physics membership answer),
|
||||
/// instead of resolving the root from a position. Falls back to the position-based
|
||||
/// <see cref="ComputeVisibility(Vector3)"/> when <paramref name="root"/> is null (e.g. the
|
||||
/// cell isn't registered with this renderer yet) — so this never regresses below baseline.
|
||||
/// </summary>
|
||||
public VisibilityResult? ComputeVisibilityFromRoot(LoadedCell? root, Vector3 fallbackPos)
|
||||
{
|
||||
if (root is null) return ComputeVisibility(fallbackPos);
|
||||
_lastCameraCell = root; // seed the cache so the BFS roots here
|
||||
LastVisibilityResult = GetVisibleCells(root, fallbackPos); // BFS from root (see Step 3)
|
||||
return LastVisibilityResult;
|
||||
}
|
||||
```
|
||||
- [ ] **Step 3: Refactor `GetVisibleCells` to accept an explicit root.** Extract the BFS body into `GetVisibleCells(LoadedCell root, Vector3 viewerPos)` and have the existing `GetVisibleCells(Vector3)` call `FindCameraCell(pos)` then delegate to it (null root → null result, today's behavior). Keep the BFS/portal-clip logic byte-identical — only the root acquisition moves out.
|
||||
|
||||
- [ ] **Step 4: Wire `GameWindow` to prefer the physics root.** At `GameWindow.cs:7153-7156`, select the root from `CellGraph.CurrCell`:
|
||||
```csharp
|
||||
var visRootPos = (_playerMode && _playerController is not null) ? _playerController.Position : camPos;
|
||||
LoadedCell? physRoot = null;
|
||||
if (_physicsEngine.DataCache?.CellGraph.CurrCell is AcDream.Core.World.Cells.EnvCell e
|
||||
&& _cellVisibility.TryGetCell(e.Id, out var lc))
|
||||
physRoot = lc;
|
||||
var visibility = _cellVisibility.ComputeVisibilityFromRoot(physRoot, visRootPos);
|
||||
```
|
||||
(Confirm the exact `_physicsEngine` accessor in Step 1; adapt if the field name differs.)
|
||||
|
||||
- [ ] **Step 5: Test** the root-selection + fallback (in `AcDream.App.Tests` if that pattern exists; else a focused `CellVisibility` test): `ComputeVisibilityFromRoot(root, pos)` BFS-roots at `root`; `ComputeVisibilityFromRoot(null, pos)` equals `ComputeVisibility(pos)`. Build green; full Core+App build green.
|
||||
|
||||
- [ ] **Step 6: Commit** (behavior-changing — note the visual gate is pending):
|
||||
```
|
||||
git add src/AcDream.App/Rendering/CellVisibility.cs src/AcDream.App/Rendering/GameWindow.cs <test>
|
||||
git commit -m "feat(app): UCG W2 Task 2 — render root from physics CurrCell (FindCameraCell fallback)"
|
||||
```
|
||||
- [ ] **Step 7: STOP for visual verification (W2a).** Launch per CLAUDE.md; spawn in the cellar + walk the cottage; confirm the indoor path engages on spawn (no world-from-below from a null root). Do not proceed to Task 3 until the user confirms W2a.
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Stab-list doorway hysteresis (BEHAVIOR-CHANGING → visual gate)
|
||||
|
||||
**Files:** Modify `src/AcDream.Core/Physics/PhysicsEngine.cs`; Test in `CellGraphMembershipTests.cs`.
|
||||
|
||||
- [ ] **Step 1: Confirm the fall-through point + read retail.** In `ResolveCellId`, find where the indoor branch falls through to outdoor (after the `:326-328` sphere-overlap check fails). Read retail `find_cell_list` `do_not_load_cells` prune (`docs/research/named-retail/acclient_2013_pseudo_c.txt:308829-308867`) to finalize the predicate. Confirm `CellGraph.CurrCell` is readable here (it's on `DataCache.CellGraph`) and `EnvCell.StabList` is the landblock-prefixed visible-cell list.
|
||||
|
||||
- [ ] **Step 2: Write the failing test** — doorway hold: a foot-sphere position that `ResolveCellId` would classify outdoor (`outdoorCandidate`), with the previous `CurrCell` = an indoor `EnvCell` whose `StabList` does NOT contain `outdoorCandidate`, AND the sphere still within the prev cell's proximity ⇒ `ResolveCellId` returns the indoor cell id (held). A second case where `StabList` DOES contain the candidate (or the sphere has cleared proximity) ⇒ returns outdoor. Use #98 cottage fixtures (`0xA9B4014x`). (Write concrete arrange/act/assert; finalize the proximity predicate from Step 1.)
|
||||
|
||||
- [ ] **Step 3: Run, verify FAIL.**
|
||||
|
||||
- [ ] **Step 4: Implement the prune.** Before the indoor→outdoor fall-through in `ResolveCellId`:
|
||||
```csharp
|
||||
// UCG W2b — retail find_cell_list do_not_load_cells prune (pseudo_c:308829-308867):
|
||||
// only accept a change to the outdoor candidate if it's reachable from the current
|
||||
// cell's stab list; otherwise hold the indoor cell one more tick. Anti-ping-pong the
|
||||
// #98 saga lacked.
|
||||
if (DataCache?.CellGraph.CurrCell is AcDream.Core.World.Cells.EnvCell prev
|
||||
&& !prev.StabList.Contains(outdoorCandidate)
|
||||
&& /* stillNearPrev: sphere within prev's expanded local AABB */ )
|
||||
{
|
||||
return SetCurrAndReturn(prev.Id);
|
||||
}
|
||||
```
|
||||
Finalize `stillNearPrev` (a containment/proximity test against `prev`'s bounds, expanded by the sphere radius) per Step 1. Keep it additive — it only adds a hold path before the existing fall-through.
|
||||
|
||||
- [ ] **Step 5: Run, verify PASS.** Full Core build + suite (no NEW deterministic failures vs baseline).
|
||||
|
||||
- [ ] **Step 6: Commit:**
|
||||
```
|
||||
git add src/AcDream.Core/Physics/PhysicsEngine.cs tests/AcDream.Core.Tests/Physics/CellGraphMembershipTests.cs
|
||||
git commit -m "feat(core): UCG W2 Task 3 — stab-list doorway hysteresis in ResolveCellId"
|
||||
```
|
||||
- [ ] **Step 7: STOP for visual verification (W2b).** Walk room ↔ cellar ↔ outside at the inn doorway; confirm `[cell-transit]` (ACDREAM_PROBE_CELL=1) stays stable (no ping-pong). User confirms before W2 is marked done.
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Verify + roadmap
|
||||
- [ ] App build 0 errors; full Core suite no NEW deterministic failures vs the static-leak baseline.
|
||||
- [ ] Both visual gates (Task 2 Step 7 + Task 3 Step 7) confirmed by the user.
|
||||
- [ ] Flip W2 to shipped in `docs/plans/2026-04-11-roadmap.md` (the Phase W table). Commit.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
- **Spec coverage:** §3 W2a (Tasks 1–2) + §3 W2b (Task 3) + §5 acceptance (Task 4 visual gates). ✓
|
||||
- **Placeholders:** Tasks 2–3 carry "confirm/finalize" steps (the render path + the hysteresis predicate need confirmation against live code + retail) — these are verify-then-use, with the concrete code supplied. Task 3's `stillNearPrev` predicate is the one genuinely-to-finalize item; flagged explicitly.
|
||||
- **Zero-behavior-change boundary:** Task 1 is additive/write-only (CurrCell read by nobody until Task 2) — provable like a W1 task. Tasks 2–3 are behavior-changing and gated on visual verification.
|
||||
- **Type consistency:** `CellGraph.CurrCell` (W1), `EnvCell.StabList`/`.Id` (W1), `CellVisibility.TryGetCell`/`ComputeVisibility` (existing), `PhysicsEngine.DataCache` (`:45`), `SetCurrAndReturn` (T1) used in T3.
|
||||
Loading…
Add table
Add a link
Reference in a new issue