diff --git a/docs/research/2026-07-08-185-outdoor-stairs-phantom-handoff.md b/docs/research/2026-07-08-185-outdoor-stairs-phantom-handoff.md new file mode 100644 index 00000000..4db7f942 --- /dev/null +++ b/docs/research/2026-07-08-185-outdoor-stairs-phantom-handoff.md @@ -0,0 +1,158 @@ +# 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`.