Inline-execution plan (frozen collision internals): dat-backed seam replay as the red pin, empirical pinning of the forward-move contact-plane loss, the localized retail-faithful grounding-retention fix (candidate tree keyed to the pin), #137 regression net, register/digest/ISSUES bookkeeping, live gate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
223 lines
14 KiB
Markdown
223 lines
14 KiB
Markdown
# #185 Outdoor-Stairs Phantom Fix — Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: this plan is intended for **inline execution by the
|
||
> lead engineer with full context** (superpowers:executing-plans), NOT subagent dispatch — it edits
|
||
> frozen collision internals and pins the fix empirically (CLAUDE.md: "Do not integrate via subagent
|
||
> unless the subagent has the full context"). Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** the local player walks up the full house-on-stilts outdoor staircase with no jam and no jump.
|
||
|
||
**Architecture:** restore retail's invariant that a grounded mover on a continuous walkable surface
|
||
keeps `contact_plane_valid` on the forward move (`transitional_insert` short-circuit, pc 273244), so
|
||
it never falls into the step-down recovery that cannot reach the *coplanar (at-level)* stair
|
||
continuation and fabricates a horizontal `(0,1,0)` sliding normal (`PrecipiceSlide`, the #137/TS-4
|
||
family). The exact retention-loss line is pinned by a dat-backed replay test, then fixed faithfully.
|
||
|
||
**Tech stack:** C# .NET 10, xUnit, `DatCollection` dat reader, `AcDream.Core.Physics`
|
||
(`PhysicsEngine.ResolveWithTransition`, `Transition`, `SpherePath`, `ShadowObjectRegistry`).
|
||
|
||
**Design spec:** `docs/superpowers/specs/2026-07-08-185-outdoor-stairs-fix-design.md` (read first).
|
||
|
||
**Fixtures already captured (this session, in the scratchpad):**
|
||
`185-recapture.jsonl` (jam pinned at `(131.71, 77.914, 61.485)`, `slidingNormal (0,1,0)`,
|
||
`collisionNormal (0,0.78,0.62)`); `gfxdump/0x01000AC5.gfxobj.json` (step-box tread = poly id 4, local
|
||
plane `(-0.625,0,0.781,-0.312)`); `[resolve-bldg]` per-object world origins, e.g. `0xF6822103 →
|
||
entOrigin_lb (132.0,77.2,60.4)`, tread world verts `(132.75,77.495,61.015)…`. **Copy the two gfxobj
|
||
dumps into `tests/AcDream.Core.Tests/Physics/fixtures/` in Task 1** so the test is self-contained.
|
||
|
||
**Reference tests to mirror (read before writing Task 1):**
|
||
`tests/AcDream.Core.Tests/Physics/Issue137CorridorSeamReplayTests.cs` (replay + probe-capture shape),
|
||
`DoorBugTrajectoryReplayTests.cs` / `CellarUpTrajectoryReplayTests.cs` (registering a **building/shadow
|
||
object** and replaying a trajectory — the outdoor-static harness #185 needs), `GfxObjDumpRoundTripTests.cs`
|
||
(loading a gfxobj + its physics polys), `ShadowObjectRegistryMultiPartTests.cs` (`RegisterMultiPart` API).
|
||
|
||
---
|
||
|
||
## Task 1: Red replay test — reproduce the wedge over the real stair objects
|
||
|
||
**Files:**
|
||
- Create: `tests/AcDream.Core.Tests/Physics/Issue185OutdoorStairsSeamReplayTests.cs`
|
||
- Create (fixtures): copy `…/scratchpad/gfxdump/0x01000AC5.gfxobj.json` (+ `0x01000ACA`) →
|
||
`tests/AcDream.Core.Tests/Physics/fixtures/`
|
||
- Read (harness API): `src/AcDream.Core/Physics/ShadowObjectRegistry.cs:92,152`
|
||
(`Register` / `RegisterMultiPart`), `PhysicsEngine.ResolveWithTransition`, and the two building-replay
|
||
reference tests above.
|
||
|
||
- [ ] **Step 1: Read the reference harness.** Read `DoorBugTrajectoryReplayTests.cs` and
|
||
`CellarUpTrajectoryReplayTests.cs` end-to-end to learn exactly how they (a) build a `PhysicsEngine`,
|
||
(b) register a building object's collision shape (`ShadowShapeBuilder`/`RegisterMultiPart` from a
|
||
Setup/GfxObj + world transform), and (c) drive `ResolveWithTransition`. Read
|
||
`GfxObjDumpRoundTripTests.cs` for how a gfxobj's `ResolvedPolygons` (the physics polys, incl. tread
|
||
poly id 4) are obtained from the dat. Confirm whether registration takes a Setup id or a GfxObj id;
|
||
the stairs are landblock statics whose collision is the GfxObj's own BSP/polys (no Setup cylsphere).
|
||
|
||
- [ ] **Step 2: Write the harness + the FAILING wedge test.** Build a `PhysicsEngine`, register the
|
||
stair step objects at their captured world transforms (each = `entOrigin_lb` translation × the +90°-about-Z
|
||
rotation that maps local tread normal `(-0.625,0,0.781)` → world `(0,-0.625,0.781)`), covering at least
|
||
the objects spanning the jam seam (`0xF68221` parts 1–3 at entOrigin Y 76.2/76.7/77.2 **and** the next
|
||
object up whose tread covers Y∈[77.5,78.0]+ — derive its origin as the +0.5 world-Y / +0.4 world-Z step
|
||
from `0xF68221` part 3). Seed a grounded body on tread N (contact plane + walkable poly = the tread quad,
|
||
world verts `(132.75,77.495,61.015),(131.25,77.495,61.015),(131.25,76.995,60.615),(132.75,76.995,60.615)`).
|
||
Replay the forward run from `(131.72,77.49,61.15)` toward `(131.71,78.18,61.15)` for ~6 held-forward
|
||
frames (flat target, +Y ≈ 0.2/frame — the movement controller sends flat, physics climbs). Use the
|
||
`ResolveWithTransition` args from the capture: `sphereRadius 0.48`, `sphereHeight 1.835`,
|
||
`stepUpHeight 0.6`, `stepDownHeight 1.5`, `isOnGround true`, `moverFlags IsPlayer|EdgeSlide`.
|
||
|
||
```csharp
|
||
// The wedge assertion (RED before the fix): the run must climb PAST the seam, not pin at Y≈77.9.
|
||
Assert.True(pos.Y > 78.10f,
|
||
$"Player must climb past the tread seam (reached Y={pos.Y:F3}); pinned at ~77.9 with " +
|
||
$"slidingNormal=(0,1,0) = the #185 fabricated-precipice wedge.");
|
||
Assert.False(body.TransientState.HasFlag(TransientStateFlags.Sliding),
|
||
"A continuous walkable ramp seam must not persist a horizontal sliding normal (#137 family).");
|
||
```
|
||
|
||
- [ ] **Step 3: Run it — verify it FAILS (reproduces the wedge).**
|
||
Run: `dotnet test tests/AcDream.Core.Tests --filter "FullyQualifiedName~Issue185OutdoorStairsSeamReplay" -v n`
|
||
Expected: **FAIL** — `pos.Y` pins at ~77.9 and/or `Sliding` flag set (the wedge reproduced offline).
|
||
**If it does NOT reproduce** (climbs clean offline), the live trigger involves state beyond the replay's
|
||
reach (as the #137 corridor replay initially did) — STOP and add the missing seed (the exact
|
||
`bodyBefore` from `185-recapture.jsonl` tick 4277: contact plane, walkable verts, and the prior-frame
|
||
`slidingNormal`) before proceeding; do not weaken the assertion.
|
||
|
||
- [ ] **Step 4: Commit the red test.**
|
||
```bash
|
||
git add tests/AcDream.Core.Tests/Physics/Issue185OutdoorStairsSeamReplayTests.cs \
|
||
tests/AcDream.Core.Tests/Physics/fixtures/0x01000AC5.gfxobj.json \
|
||
tests/AcDream.Core.Tests/Physics/fixtures/0x01000ACA.gfxobj.json
|
||
git commit -m "test(#185): red — dat-backed outdoor-stairs seam replay reproduces the wedge
|
||
|
||
Registers the real stair step-boxes and replays the captured forward run; pins the
|
||
jam (no advance past Y~77.9, persisted (0,1,0) sliding normal). RED before the fix.
|
||
|
||
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: Pin the exact retention-loss site
|
||
|
||
**Files:**
|
||
- Modify (temporary probe only, reverted at end): the replay test — wrap the seam-crossing frame with
|
||
`PhysicsDiagnostics.ProbeContactPlaneEnabled = true` (env `ACDREAM_PROBE_CONTACT_PLANE`) +
|
||
`ProbeStepWalkEnabled` + `ProbeIndoorBspEnabled`, capturing `Console.Out` (exactly as
|
||
`Issue137CorridorSeamReplayTests.SeamShake_WestBoundary_SnapshotExact_Advances` does, lines 207–236).
|
||
- Read: `src/AcDream.Core/Physics/TransitionTypes.cs` (TransitionalInsert loop `~1030–1110`
|
||
Slid-clears at 1047/1073/1102; the neg-poly/step-down dispatch `~1339`; `ValidateTransition` LKCP
|
||
retention `~4645–4735`), `src/AcDream.Core/Physics/PhysicsEngine.cs` (seed `~1004`, writeback `~1131`).
|
||
|
||
- [ ] **Step 1: Capture the seam-crossing frame's contact-plane transitions.** Run the red test with the
|
||
probes on; in the emitted trace, find the frame that reaches the step-down dispatch with
|
||
`ci.ContactPlaneValid == false`, and identify **which** upstream event set it false:
|
||
(a) an env/building/object channel returned **Slid** (clears cp at 1047/1073/1102), or
|
||
(b) the seed at `PhysicsEngine.cs:1004` never re-validated and the forward main-sweep set no fresh
|
||
contact plane, or
|
||
(c) `ValidateTransition`'s LKCP proximity guard (`~4698`, `radius+ε > |angle|`) DROPPED the plane
|
||
because the floated sphere is >radius from tread N's plane.
|
||
Record the concrete site (file:line + the branch) in a scratch note; this selects the Task-3 candidate.
|
||
|
||
- [ ] **Step 2: Confirm the retail-faithful expectation for that site.** Grep the named-retail decomp for
|
||
the corresponding retail function (the Slid channel → `BSPTREE::find_collisions` Contact branch
|
||
pc 323838+, or `set_contact_plane`/`get_object_info` pc 272547 for the retain path) and confirm what
|
||
retail does that keeps `contact_plane_valid` here. Do NOT invent behavior — cite the pc line. This is
|
||
the gate: the fix must make acdream match that retail behavior, not add a guard.
|
||
|
||
---
|
||
|
||
## Task 3: Apply the retail-faithful grounding-retention fix
|
||
|
||
**Files:**
|
||
- Modify: the pinned site from Task 2 in `src/AcDream.Core/Physics/TransitionTypes.cs` (or
|
||
`PhysicsEngine.cs`). Exactly ONE of the candidates below, chosen by Task 2's pin + Task 2's retail cite.
|
||
|
||
- [ ] **Step 1: Apply the fix matching the pinned site.** Candidate edits (pick the one Task 2 selected):
|
||
|
||
- **Candidate A (pin = spurious Slid on the coplanar-graze):** the grounded foot grazing the coplanar
|
||
continuation tread is being routed to a Slid (clearing cp) where retail records a walkable
|
||
contact/step (keeps cp). Fix the specific collision-response branch so a grounded mover grazing a
|
||
**walkable** (normal.Z ≥ FloorZ) coplanar poly it is resting on retains/sets the contact plane
|
||
instead of clearing it — mirroring the retail Contact-branch path Task 2 cited. Do not blanket-skip
|
||
Slid; scope to the walkable-coplanar-continuation case.
|
||
|
||
- **Candidate B (pin = forward move sets no fresh contact plane):** ensure the grounded forward move
|
||
re-establishes `ci.ContactPlane` from the coplanar tread the sphere rests on (retail's forward
|
||
landing keeps `contact_plane_valid`, pc 273244) — e.g. the walkable-landing/`find_walkable` on the
|
||
forward insert must find the at-level continuation, not only a below-foot surface.
|
||
|
||
- **Candidate C (pin = LKCP proximity guard drops the plane):** the floated sphere separates from
|
||
tread N's plane by >radius so the guard (`~4698`) drops it — but the sphere is genuinely resting on
|
||
the coplanar continuation. Fix so the proximity check is evaluated against the surface actually under
|
||
the sphere (the coplanar continuation), matching retail's `get_object_info` retain logic (pc 272547),
|
||
rather than dropping grounding at the seam.
|
||
|
||
Each candidate is a **localized** change. If the pinned faithful fix is larger than a localized edit,
|
||
STOP and return to the user (scope guard, per the spec §3).
|
||
|
||
- [ ] **Step 2: Register bookkeeping in the same commit.** Amend `docs/architecture/retail-divergence-register.md`
|
||
row **TS-4** (or add a new row) to record the grounding-retention change and its retail oracle
|
||
(pc 273244 / the Task-2 cite); retire/narrow as the port dictates.
|
||
|
||
---
|
||
|
||
## Task 4: Green the replay + remove the temporary probes
|
||
|
||
- [ ] **Step 1: Remove the temporary probe wrapping** added in Task 2 (leave the test asserting behavior,
|
||
no probe side effects).
|
||
- [ ] **Step 2: Run the red test — verify it now PASSES.**
|
||
Run: `dotnet test tests/AcDream.Core.Tests --filter "FullyQualifiedName~Issue185OutdoorStairsSeamReplay" -v n`
|
||
Expected: **PASS** — the run climbs past Y=78.10 with no persisted sliding normal.
|
||
- [ ] **Step 3: Commit the fix + green test.**
|
||
```bash
|
||
git add -A
|
||
git commit -m "fix(#185): keep the mover grounded on a continuous walkable ramp seam
|
||
|
||
<one line naming the pinned site + the retail oracle from Task 2>. The grounded
|
||
forward move now retains contact_plane_valid across the coplanar stair seam
|
||
(retail transitional_insert pc 273244) instead of dropping into the step-down
|
||
recovery that can't reach the at-level continuation and fabricates a horizontal
|
||
(0,1,0) sliding normal (PrecipiceSlide, #137/TS-4 family). Register TS-4 amended.
|
||
|
||
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: Regression — the shared grounded-move path
|
||
|
||
- [ ] **Step 1: Run the #137 suites (must stay green / un-skipped).**
|
||
Run: `dotnet test tests/AcDream.Core.Tests --filter "FullyQualifiedName~Issue137" -v n`
|
||
Expected: PASS (`Issue137CorridorSeamReplayTests`, `Issue137SlidingNormalLifecycleTests`,
|
||
`Issue137CorridorSeamInspectionTests`). Also run `CellarUpTrajectoryReplayTests`, `BSPStepUpTests`,
|
||
`SphereCollisionFamilyTests` — the step-up/step-down/slide neighbors.
|
||
- [ ] **Step 2: Full build + test.**
|
||
Run: `dotnet build src/AcDream.App/AcDream.App.csproj -c Debug` then
|
||
`dotnet test` (Core / App / UI / Net). Expected: all green, 0 warnings.
|
||
- [ ] **Step 3: If any regressed,** the fix over-reached (not scoped to the walkable-coplanar-continuation
|
||
case) — narrow it and re-run; do NOT loosen the regression assertions.
|
||
|
||
---
|
||
|
||
## Task 6: Bookkeeping (docs + memory)
|
||
|
||
- [ ] **Step 1:** `docs/ISSUES.md` — move **#185** to Recently closed with the fix SHA.
|
||
- [ ] **Step 2:** `claude-memory/project_physics_collision_digest.md` — add the #185 banner (root cause +
|
||
the fix + DO-NOT-RETRY: don't un-zero Z, don't touch step-up budget, fix the grounding provenance).
|
||
- [ ] **Step 3:** roadmap/milestones — note the M1.5 #137-family stair fix if a phase line applies.
|
||
- [ ] **Step 4: Commit the bookkeeping.**
|
||
```bash
|
||
git add docs/ISSUES.md claude-memory/project_physics_collision_digest.md
|
||
git commit -m "docs(#185): close — grounding-retention fix for the outdoor-stairs phantom
|
||
|
||
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7: Live visual gate (USER — acceptance)
|
||
|
||
- [ ] **Step 1: Build green, then launch** (canonical live command; no pre-close of running clients).
|
||
- [ ] **Step 2: USER walks `+Acdream` up the FULL outdoor staircase** — must climb with no jam and no
|
||
jump. Then regression spot-checks: indoor cellar/corridor stairs, another outdoor building, a ramp,
|
||
flat ground — no new phantom blocks AND no sliding through walkable ramps/edges.
|
||
- [ ] **Step 3:** on pass, #185 is closed (bookkeeping already landed in Task 6). On fail, capture with
|
||
the apparatus (§6 of the spec) and re-pin — do not guess a follow-up.
|