docs(#185): implementation plan — red replay -> pin -> retention fix -> regress
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>
This commit is contained in:
parent
06c3ecd89d
commit
75e3b76445
1 changed files with 223 additions and 0 deletions
223
docs/superpowers/plans/2026-07-08-185-outdoor-stairs-fix.md
Normal file
223
docs/superpowers/plans/2026-07-08-185-outdoor-stairs-fix.md
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
# #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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue