diff --git a/docs/research/2026-07-06-137-corridor-phantom-pickup-prompt.md b/docs/research/2026-07-06-137-corridor-phantom-pickup-prompt.md new file mode 100644 index 00000000..2773d01e --- /dev/null +++ b/docs/research/2026-07-06-137-corridor-phantom-pickup-prompt.md @@ -0,0 +1,76 @@ +# Pickup prompt — #137 corridor phantom collision (paste into a fresh session) + +Read `claude-memory/project_physics_collision_digest.md` FIRST (binding +DO-NOT-RETRY table), then **ISSUES #137** (the 2026-07-05 CHARACTERIZED +section — the full evidence chain lives there), then this file. The 2026-07-05 +session's ledger for context: #172 (CCylSphere family port), #173 (remote +ceiling bounce), #174 (motion-queue drain — doors work after jumping), #175 +(door collision at the motion-table closed pose, two takes). + +**The bug (user-verified repro, Facility Hub 0x8A02):** running down a +corridor, an INVISIBLE blocker stops the player mid-corridor; the player can +walk around it. Two stacked mechanisms, both evidence-pinned: + +## Mechanism 1 — PortalSide portal polygons are solid for us + +- Live: crossing corridor cells `0x8A02016E → 0x8A02017A` at world x≈85.25 + recorded ONE wall hit, normal (−1,0,0) — straight against the movement + (`launch-175-verify2.log:42858`, worktree root). +- Dat (`Issue137CorridorSeamInspectionTests`, committed): cell 0x8A02016E's + three portals to 0x011E (polys 1/3/5, flags=**PortalSide**, NOT ExactMatch) + are PRESENT in `CellStruct.PhysicsPolygons`; every ExactMatch portal in the + same cell is absent from the physics set. The cell's rotation maps those + portal planes into the world −X wall the player hit. +- Oracle greps DONE (do not repeat): `CCellStruct::UnPack` 0x00533d00 loads + physics polys + BSP verbatim (no portal stripping); + `CPolygon::pos_hits_sphere`/`hits_sphere`/`polygon_hits_sphere_slow_but_sure` + (0x005394f0/0x00539540/0x00538a10) are pure geometry; + `CCellPortal` (0x0053bab0) carries portal→CPolygon + portal_side + + exact_match but the BSP test chain never consults it. +- **NEXT (step −1 protocol, needs the user):** cdb-attach a live retail + client at this exact corridor (Facility Hub, the 016E↔011E portals) and + trace which path lets retail through: breakpoint + `BSPTREE::find_collisions` / `BSPLEAF::sphere_intersects_poly` / + `CPolygon::pos_hits_sphere` and see whether the portal polys are ever + TESTED (candidate A: sidedness/stippling — the polys carry stip=NoPos — + or the pos_hits_sphere tail's directional cull) or never REACHED + (candidate B: transit/membership order hands the sphere to the neighbor + cell whose geometry has a real hole). Toolchain crib: + `claude-memory/project_retail_debugger.md` + the CLAUDE.md + "Retail debugger toolchain" section. Verify the binary with + `py tools/pdb-extract/check_exe_pdb.py` first. +- ⚠️ Do NOT ship a "skip portal polys in the physics BSP" filter on + assumption — if retail's answer is sidedness or test order, a blanket + skip opens holes (some PortalSide polys may be legitimately solid from + one side — one-way drops etc.). + +## Mechanism 2 — the sliding-normal absorbing wedge (fix independently) + +- After the single seam hit, EVERY forward resolve returns `ok=False + hit=no` with zero advance: the body-persisted SlidingNormal (−1,0,0) + projects the +X offset to exactly ZERO in `Transition.AdjustOffset`, and + the stepping loop's abort-small-offset fires at step 0 (TransitionTypes + `FindValidPosition` loop, `return i != 0 && …`) — BEFORE any collision + test could refresh the state. An ABSORBING wedge; strafing escapes it + (the user's "push through on the side"). +- Retail re-derives slide state per frame — `OBJECTINFO::get_object_info` + pc:279992 "governs only the NEXT frame" (#116 notes in the digest). + AUDIT: who writes the body's persisted SlidingNormal + (PhysicsEngine.ResolveWithTransition seed ~:995-1040 + the writeback), + and where retail CLEARS it when contact does not recur. This is the #116 + slide-response family — check ISSUES #116 before changing anything + (oracle-first; the digest's DO-NOT-RETRY table applies). +- Likely the bigger playability win: without the wedge, mechanism 1 alone + would be a momentary stutter, not a dead stop. + +**Order:** mechanism 2 first (pure acdream-side audit + fix, testable with a +replay-style unit test: seed a body with a stale sliding normal, resolve +forward with no obstruction in range, assert the step is NOT zeroed), then +the mechanism-1 cdb session when the user can run retail side-by-side. + +**Gotchas:** PowerShell Tee logs are UTF-16 (`tr -d '\000'` before grep); +the user manages client lifecycle; probes RESOLVE/CELL/BUILDING are +DebugPanel-toggleable (ACDREAM_DEVTOOLS=1); the [shape-pose] line +(ACDREAM_DUMP_MOTION=1) prints each BSP registration's pose source. +Register rows to touch if fixes ship: none exist yet for either mechanism — +add per the same-commit rule; #116's row interactions per the digest.