acdream/docs/superpowers/plans/2026-07-08-185-outdoor-stairs-fix.md
Erik 75e3b76445 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>
2026-07-08 08:42:36 +02:00

14 KiB
Raw Blame History

#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 13 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.

// 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: FAILpos.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.

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 207236).

  • Read: src/AcDream.Core/Physics/TransitionTypes.cs (TransitionalInsert loop ~10301110 Slid-clears at 1047/1073/1102; the neg-poly/step-down dispatch ~1339; ValidateTransition LKCP retention ~46454735), 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.
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.
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.