# Handoff: #185 — LOCAL player jams half-way up outdoor stairs (house on stilts) **Date:** 2026-07-08 · **Status:** ROOT CAUSE PINNED (definitive, probe-confirmed) — fix NOT started. The fix touches the **frozen collision internals** (the #137 / TS-4 sliding-normal family), so per the project workflow it needs a **brainstorming gate BEFORE any code**. This handoff is the investigation; do the brainstorm + fix in the new session. **Read these first, in order:** 1. This handoff. 2. `claude-memory/project_physics_collision_digest.md` — the #137 banners + the DO-NOT-RETRY table + the sliding-normal-provenance lessons (this is the SSOT for the collision arc). 3. `docs/research/2026-07-06-137-sliding-normal-lifecycle-audit.md` — the #137 sliding-normal lifecycle audit (persist / absorb / clear; the synthetic-normal mechanism). 4. `docs/architecture/retail-divergence-register.md` — rows **TS-4** (steep-tangent `SetSlidingNormal`), **TS-45** (retired — `SphereCollision` leak), **AP-83/84** (CSphere/ CylSphere family). 5. `docs/ISSUES.md` #185 (the one-paragraph version). --- ## 0. Symptom Running the **LOCAL player** up the outside stairs of a house-on-stilts, you hit an invisible block **half-way up** — you shuffle sideways in place but cannot advance up. **A jump clears it** and you continue up the rest of the stairs normally. Pre-existing (seen before the #184 work); unrelated to the remote de-overlap thread. --- ## 1. Root cause — DEFINITIVE (probe-confirmed): a SYNTHETIC sliding normal, not geometry The stairs are **real, walkable 38.7° tread quads** in the building collision mesh. The player walks straight up them. At **one convex tread edge** (near a building-part seam), the foot sphere straddles the edge, hits the lower tread poly, and the collision **RESPONSE fabricates a perpendicular normal** — the tread normal **rotated 90°** — which is then flattened to a pure horizontal `(0,1,0)` sliding plane. The player's +Y (up-stairs) movement is fully absorbed by that plane → **sideways-only slide** (pinned in Y and Z). The jump clears it because the jump's +Z velocity is not in the horizontal slide plane. **It is NOT geometry, NOT a step-height / step-up-budget issue, NOT a missing riser.** ### The evidence **A — `ACDREAM_CAPTURE_RESOLVE` (structured player-resolve JSONL).** At the jam the player is pinned at **Y=77.812, Z=61.404** and only slides in X (~0.10 m of the ~0.68 m requested). Constant across every stuck frame: - `collisionNormal = (0.00, 0.78, 0.62)` — a ~52° face (**NOT** the #137 negated-run-direction). - `slidingNormal = (0, 1, 0)` — that collision normal with **Z zeroed + renormalised** (horizontalised). - `contactPlane / walkablePlane = (0, -0.625, 0.781)` — the 38.7° tread the player stands on. - `result.collisionNormalValid=false` on the zero-move frames (the offset is pre-absorbed → target==current). **B — `ACDREAM_PROBE_BUILDING=1` (`[resolve-bldg]` hit-poly log).** The decisive read: at the jam, **ALL 156 building collisions are ONE polygon** — the 38.7° tread: ``` [resolve-bldg] obj=0xF6822103 entityId=0x00F68221 partIdx=3 gfxObj=0x01000AC5 hasPhys=True bspR=1.05 vAabbR=1.15 entOrigin_lb=(132.0,77.2,60.4) hitPoly: numVerts=4 plane=(-0.625,0.000,0.781,-0.312) v0_world=(132.75, 77.50, 61.01) v1_world=(131.25, 77.50, 61.01) v2_world=(131.25, 77.00, 60.62) v3_world=(132.75, 77.00, 60.62) ``` - **No riser / wall polygon exists in the collision mesh** — the only distinct hit-poly plane across all 156 hits is that tread. - Local plane `(-0.625,0,0.781)` maps (building rotation ≈ +90° about Z) to the **world tread normal `(0,-0.625,0.781)`** — matching capture A's contact plane. - The capture's blocking normal `(0,0.78,0.62)` is that **tread normal rotated 90°** (perpendicular: dot ≈ 0). It does **not** exist in the dat → **synthetic**, fabricated by the collision response. ### Geometry facts (for the repro / test fixture) - Object `0xF6822103` (entity), `entityId=0x00F68221`, `partIdx=3`; **gfxObj `0x01000AC5`** (+ a neighbouring building part `gfxObj 0x01000ACA`, `obj 0xF6821201`, right at the seam). - Landblock `0xf682`, cell `0xF682002C`. **Login spawn `@0xF682002C` is at the house** (the streaming recenters to (246,130) on spawn), so the stairs load immediately — no travel needed. - Jam world position ≈ **(132, 77.8, 61.4)**; the involved tread quad world verts are listed above (X 131.25–132.75, Y 77.00–77.50, Z 60.62–61.01; ~0.4 m rise / 0.5 m run = 38.7°). - The player collides as a **body SPHERE** (human Setup 0x02000001; r≈0.48), so the relevant response is the **CSphere family** (dispatcher `0x00537A80`, `slide_sphere 0x00537440`) — the one the #182 port (`96ae2740`) rebuilt. --- ## 2. Classification + where the fix lives **#137 / TS-4 "synthetic sliding-normal at a convex edge/seam" family — stair edition.** Same class as the corridor-phantom work (`a11df5b8`, `e8651b38`) and the seam-shake fix (`d4869154`). The building geometry is fine; our slide/validate response invents the wall. **The core open question for the brainstorm:** *where does the 90°-rotated normal get fabricated?* Candidates (trace these): - `CSphere::slide_sphere` (`0x00537440`) — the crease `collisionNormal × contactPlane.Normal` at a convex edge (a cross product IS a 90° rotation; the tread × something could yield the perpendicular normal). **Prime suspect.** - `validate_transition` (`0x0050aa70`) sliding-normal epilogue (the only in-transition sliding-normal writer per the #137 audit) — is it persisting a fabricated normal? - The step-up / neg-poly path at a convex edge (the seam-shake class — a lifted CheckPos lost, re-test grazes the neighbour tread). - The CSphere `collide_with_point` / edge-vertex case at the tread top edge. The `(0,1,0)` horizontalisation of `(0,0.78,0.62)` (Z zeroed) is the **absorb** step — the player's forward offset is projected out. Find who WRITES `(0,0.78,0.62)` first. --- ## 3. Apparatus (reusable — RE-CAPTURE in the new session; the temp files won't persist) Repro: launch (canonical live command, CLAUDE.md), spawn `@0xF682002C`, run up the outside stairs, hold forward against the jam (~world 132,77.8,61.4) for a couple of seconds. - `ACDREAM_CAPTURE_RESOLVE=` → per-player-resolve JSONL (inputs + PhysicsBody before/after + ResolveResult, filtered to IsPlayer). Analyse: find blocked-while-moving frames (`|target−cur|_xy > 0.03` but `|result−cur|_xy < 0.4×that`); read `collisionNormal`, `slidingNormal`, `contactPlane`, `walkableVertices`. - `ACDREAM_PROBE_BUILDING=1` → `[resolve-bldg]` blocks (gfxObj, hit-poly plane + world verts). **⚠️ GOTCHA:** the diagnostic Console output lands in the **raw background-task `.output` file** (the process stdout captured by `run_in_background`), **NOT** the `Tee-Object` log — `Tee` buffers and shows almost nothing. Grep the task `.output`. Also enables `[bsp-test]` / `[bldg-channel]` (very high volume) — grep for `resolve-bldg` / `hitPoly`. - Optional: `ACDREAM_PROBE_RESOLVE=1` / `ACDREAM_PROBE_PUSH_BACK=1` for the step-up / push-back dispatch internals if the fabrication site needs a finer trace. - Build a **dat-backed replay test** reproducing the tread-edge hit to the millimetre (pattern: `Issue137CorridorSeamReplayTests`) — the fix's red→green pin. Dump the cell/GfxObj with `ACDREAM_DUMP_GFXOBJS=0x01000AC5` (+ `_DIR`) for the fixture. --- ## 4. DO-NOT-RETRY - **Do NOT touch the step-up budget or step-up logic.** There is no riser here — it's a continuous walkable 38.7° ramp of tread quads; the jam is a fabricated normal, not a tall step. (The register/digest already warned: stair sticks are the sliding-normal family; six past step-up attempts were all wrong.) - **Do NOT "fix the geometry" / mesh generation.** All 156 building hits are ONE real, walkable tread poly; the dat is fine. There is no bad 52° face to remove. - **Do NOT read the Tee'd launch log for `[resolve-bldg]`** — it's buffered/empty; read the raw task `.output` (§3 gotcha). - **Do NOT ship a per-frame sliding-normal clear or a small-offset-abort patch** (the #137 DO-NOT-RETRY): fix the normal's PROVENANCE, not the symptom. A leaked/fabricated normal + success writeback = an absorbing wedge at empty space — the exact pattern the corridor fix addressed by routing through the faithful `SlideSphere` and returning COLLIDED on the opposing branch. --- ## 5. Acceptance - The LOCAL player **walks up the full outdoor staircase without jamming or jumping**; no sideways-slide pin at the ~Y 77.5 / Z 61.0 tread edge. - **Regression pass** (the whole point — this is the shared slide response): indoor cellar/ corridor stairs (#137 repros), other outdoor buildings, ramps, and flat ground — **no new phantom blocks AND no players sliding through walkable ramps/edges.** Un-skip / keep green the `Issue137*` replay suites. - Register bookkeeping in the fix commit (amend TS-4 / add a row as the change dictates). ## 6. Pointers - ISSUES `#185`; committed root cause `99c22fad` (+ filing `29e01c38`). - Collision digest DO-NOT-RETRY table + the #137 corridor/​seam banners. - Register TS-4 / TS-45 / AP-83 / AP-84; #137 fixes `a11df5b8`, `e8651b38`, `d4869154`; CSphere port `96ae2740`.