acdream/docs/research/2026-07-08-185-outdoor-stairs-phantom-handoff.md

158 lines
9.2 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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