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

9.2 KiB
Raw Blame History

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.