docs(#185): handoff for the outdoor-stairs phantom fix (synthetic sliding-normal, #137/TS-4 family)

This commit is contained in:
Erik 2026-07-08 07:35:11 +02:00
parent 99c22fad48
commit 4a067e34a9

View file

@ -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.25132.75, Y 77.0077.50, Z 60.6261.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=<path>` → per-player-resolve JSONL (inputs + PhysicsBody before/after
+ ResolveResult, filtered to IsPlayer). Analyse: find blocked-while-moving frames
(`|targetcur|_xy > 0.03` but `|resultcur|_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`.