# Design: #185 — local player jams half-way up outdoor stairs (fix) **Date:** 2026-07-08 · **Status:** DESIGN (approved approach: A) — implementation not started **Milestone:** M1.5 (indoor world feels right) · **Family:** #137 / TS-4 synthetic-sliding-normal at a walkable seam, stair edition **Investigation handoff:** `docs/research/2026-07-08-185-outdoor-stairs-phantom-handoff.md` **Decomp cross-check workflow:** `wf_3c1120c4-a04` (5 tracers + 3 adversarial verifiers, all high-confidence) --- ## 1. Symptom Running the **LOCAL player** up the outside stairs of a house-on-stilts, you hit an invisible block part-way up: you shuffle sideways but cannot advance. **A jump clears it** and you continue up normally. Pre-existing; unrelated to #184. Jam at world ≈ (132, 77.9, 61.5), landblock `0xf682`, cell `0xF682002C`. ## 2. Root cause — CONFIRMED (decomp cross-check + live apparatus) ### 2.1 The geometry (dumped this session) The staircase is a **continuous, coplanar 38.7° ramp** built from stacked multi-part **step-box objects** (`entityId` `0x00F68220`, `0x00F68221`, `0x00F68222`, …; each holds 3 step parts; gfxObj `0x01000AC5` = one step-box, `0x01000ACA` = a sibling variant). Every step's tread (poly id 4, local plane `(-0.625,0,0.781)`, world `(0,-0.625,0.781)`) lies on the **same infinite plane** — verified: adjacent treads share both normal AND plane-d, so they are genuinely coplanar, forming one continuous ramp. The tread quads abut at **0.5 m seams in world Y**, and those seams fall on **object boundaries** (the objects interleave; e.g. `0xF68222` part-3 at Y≈76.49 abuts `0xF68221` part-1 at Y≈76.50). gfxobj dumps: `…/scratchpad/gfxdump/0x01000AC5.gfxobj.json`, `0x01000ACA.gfxobj.json`. ### 2.2 The mechanism (live capture `185-recapture.jsonl` + `[step-walk]` / `[stepdown-decide]`) Walking up, acdream re-grounds every frame via the **step-down-to-maintain-contact** recovery (`DoStepDown`, `TransitionTypes.cs:1339`). This *succeeds* at most positions (`accept=True` climbing to Y=77.914). At one seam it fails, and the fabrication chain runs: 1. The grounded forward move floats the foot **past the current tread's polygon** (kept "grounded" on the tread plane via the `LastKnownContactPlane` proximity restore) to Y=78.12, Z=61.65 — but the forward `TransitionalInsert` reaches the **step-down dispatch with `ci.ContactPlaneValid = false`** (the seed from `body.ContactPlane` is lost mid-sweep; pinning *why* is implementation step 1, §3). Note: `ValidateTransition`'s LKCP proximity restore later re-validates it, so `bodyAfter.contactPlaneValid = true` in the capture even though the mover is wedged — the loss is transient-but-decisive, at the dispatch, not at frame end. 2. Because contact is invalid at the dispatch, the step-down branch fires. `DoStepDown` first **drops the sphere the full `stepDownHeight`** (Z 61.65 → 60.90) then sweeps `find_walkable` **downward** — but the continuation tread is **coplanar (at the foot's level), now *above* the dropped sphere** → nothing found → `[stepdown-decide] accept=False` (340× in the run). 3. `DoStepDown` fails → `EdgeSlideAfterStepDownFailed` → **branch3** (`TransitionTypes.cs:1516`) → `PrecipiceSlide` → `FindCrossedEdge` returns `cross(tread_normal, top_edge)` = **`(0,±0.78,±0.62)`** → `SlideSphere` sets it as `CollisionNormal`. 4. `ValidateTransition` (`:4635`) → `SetSlidingNormal` **zeroes Z** → **`(0,±1,0)`**. 5. Body persists it (`PhysicsEngine.cs:1136`); next frame **seeds** it (`:1020`); `AdjustOffset` absorbs the +Y up-stairs offset → **sideways-only wedge**, pinned at Y=77.914. Self-sustaining (seed→absorb→step-0-abort), with a fresh `(0,0.78,0.62)` re-fabricated every ~6 ticks. A jump's +Z velocity survives the horizontal `(0,1,0)` plane, which is why a jump clears it. Capture signatures (`185-recapture.jsonl`): pinned `result.position = (131.71, 77.914, 61.485)`; `bodyAfter.slidingNormal = (0,1,0)`; `result.collisionNormal = (0,0.78,0.62)` on the re-fabrication ticks; `contactPlane = walkablePlane = (0,-0.625,0.781)` (the real tread). ### 2.3 The retail divergence (named-retail decomp, high confidence) Retail's grounded forward move **keeps `contact_plane_valid`** and hits the short-circuit `if (this->collision_info.contact_plane_valid != 0) return 1;` (`transitional_insert`, pc 273244 / `0x0050b818`) — it **never runs step-down at an interior ramp point**, so it never reaches `edge_slide`/`precipice_slide`. acdream instead **loses grounding on the forward move** and leans on the fragile per-frame step-down recovery, which cannot reach a **coplanar (at-level)** continuation because step-down is strictly downward. The wedge is a *symptom* of over-relying on step-down where retail stays grounded. ### 2.4 What is FAITHFUL — do NOT touch (adversarially verified) - `PrecipiceSlide` math (`find_crossed_edge` → `slide_sphere`, conditional sign-negate to oppose travel): **faithful** (retail `precipice_slide` pc 274316/274354; conditional negate pc 274347). - `SetSlidingNormal` **zeroing Z**: **faithful** — retail `set_sliding_normal` (`0x0050a060`, pc 272137) also sets `sliding_normal.z = 0f` then normalizes. Un-zeroing is a DEAD END and would itself be a divergence; and it is geometrically moot (both `cp.N` and the fabricated normal have x=0, so the crease is pure-X for both the zeroed `(0,1,0)` and the 3D `(0,0.78,0.62)` — the +Y offset is annihilated identically). - The whole-cell **multi-object search**: **faithful** — `FindObjCollisionsInCell` iterates the full per-cell shadow list; both stair objects are tested (`[resolve-bldg]` confirms hits on `0xF68220/21/22`). Not a scoping/registration gap. ## 3. The fix — Approach A (keep the mover grounded on the forward move) **Invariant to restore:** a grounded mover walking a continuous walkable surface **retains `contact_plane_valid` on the forward move** (retail pc 273244), so it does not depend on the step-down recovery that fails at a coplanar seam. **Scope (narrow):** the grounded forward-move path in `Transition.TransitionalInsert` / the contact-plane retention it feeds (`TransitionTypes.cs` ~1030–1110 Slid-clears + the neg-poly/ step-down dispatch ~1339, and the `ValidateTransition` LKCP retention ~4645–4735). The change: when the grounded foot sphere grazes the **coplanar walkable continuation** at an object seam, retain / re-establish the contact plane from the tread it is resting on, rather than clearing it (a spurious Slid) and dropping into the step-down recovery. **Explicitly NOT changed:** the fabrication code, `SetSlidingNormal`, `PrecipiceSlide`, `FindCrossedEdge`, the multi-object search — all verified faithful (§2.4). **Pinning the exact line — the #137 method, not a guess:** implementation step 1 is a **dat-backed replay test** reproducing this exact seam crossing (fixtures already captured — §6). The replay reproduces the wedge headlessly; the forward-move contact-plane transitions are instrumented *inside the test* (plus a short `ACDREAM_PROBE_CONTACT_PLANE` live pass ONLY if the replay can't reach the exact frame) to identify the precise line where `contact_plane_valid` is lost. The fix targets *that* line to match retail's retention. Same pattern as `Issue137CorridorSeamReplayTests`. **Scope guard:** if pinning reveals the faithful fix is larger than a localized retention change, STOP and return to the user before expanding scope (frozen-internals rule). ## 4. Alternatives considered - **B — make step-down find the coplanar/at-level continuation.** Bends `DoStepDown`/`StepSphereDown` away from retail's strictly-downward design; risks being a subtle divergence rather than a faithful fix. Rejected as primary. - **C — gate `EdgeSlide`/`PrecipiceSlide` to not fabricate when a live walkable continuation exists.** Smallest blast radius, and retail's `edge_slide` does gate on `walkable != 0`, but it treats the symptom site and masks the upstream grounding loss (weaker against no-workarounds). Kept as the fallback if A proves too invasive. ## 5. Test strategy - **Red→green pin:** a new dat-backed replay (`Issue185OutdoorStairsSeamReplayTests`, pattern of `Issue137CorridorSeamReplayTests`) driving the captured trajectory through the failing seam: pre-fix reproduces the wedge (no advance past Y≈77.9, `slidingNormal=(0,1,0)`); post-fix the mover climbs past the seam with no fabricated sliding normal. - **Regression net (the whole point — shared grounded-move path):** - `Issue137*` suites (`Issue137CorridorSeamReplayTests`, `Issue137SlidingNormalLifecycleTests`, `Issue137CorridorSeamInspectionTests`) green / kept un-skipped. - Live spot checks (user visual gate): indoor cellar/corridor stairs, other outdoor buildings, ramps, and flat ground — **no new phantom blocks AND no players sliding through walkable edges/ramps.** - `dotnet build` + `dotnet test` green (Core / App / UI / Net). ## 6. Apparatus / fixtures (already captured this session) - Structured resolve capture: `…/scratchpad/185-recapture.jsonl` (jam pinned at `(131.71, 77.914, 61.485)`; `slidingNormal (0,1,0)`; `collisionNormal (0,0.78,0.62)`). - gfxobj geometry: `…/scratchpad/gfxdump/0x01000AC5.gfxobj.json` (+ `0x01000ACA`) — the step-box tread (poly 4) + faces; local tread plane `(-0.625,0,0.781, d=-0.312)`. - `[resolve-bldg]` per-object world origins + hit-poly world verts (raw task `.output`); tread N world verts `(132.75,77.495,61.015)…`; per-step `entOrigin_lb` e.g. `0xF6822103 → (132.0,77.2,60.4)`. - Reusable probes: `ACDREAM_CAPTURE_RESOLVE`, `ACDREAM_PROBE_STEP_WALK`, `ACDREAM_PROBE_INDOOR_BSP` (`[stepdown-decide]`), `ACDREAM_PROBE_BUILDING` (`[resolve-bldg]`, heavy — raw `.output`, not the Tee'd log), `ACDREAM_PROBE_CONTACT_PLANE` (forward-move contact transitions — for site pinning), `ACDREAM_DUMP_GFXOBJS` (fixture extraction). ## 7. Bookkeeping (in the fix commit) - Register: amend **TS-4** (or add a new row) to record the grounding-retention change; retire/narrow as the faithful port dictates. - `claude-memory/project_physics_collision_digest.md`: add the #185 banner + DO-NOT-RETRY entries. - `docs/ISSUES.md`: move #185 to Recently closed with the fix SHA. - Roadmap/milestones: note the M1.5 #137-family stair fix. ## 8. Acceptance - The LOCAL player **walks up the full outdoor staircase with no jam and no jump**; no sideways-slide pin at the tread seams. - Regression pass (§5): no new phantom blocks AND no players sliding through walkable ramps/edges; `Issue137*` green; build + test green. - Register bookkeeping done in the fix commit. ## 9. DO-NOT-RETRY (carried from the handoff + this investigation) - Do NOT touch step-up budget / step-up logic — there is no riser; it is a continuous walkable ramp. - Do NOT "fix the geometry" — the dat is real, coplanar, walkable treads. - Do NOT un-zero Z in `SetSlidingNormal` — faithful to retail and geometrically moot (§2.4). - Do NOT ship a per-frame sliding-normal clear or a small-offset-abort patch — fix the PROVENANCE (the grounding loss), not the symptom (#137 lesson). - Do NOT read the Tee'd launch log for `[resolve-bldg]` — it's buffered; read the raw task `.output`.