The mechanism-1 theory (PortalSide portal polys solid in our physics set)
is REFUTED for the corridor repro, and the remaining half of the phantom
is fixed — no cdb session needed:
- The live hit normal (-1.00,0.03,-0.03) matches NO dat polygon: a
world-space sweep of both seam cells + every portal-adjacent neighbor
(CorridorSeam_FindPolygonMatchingLiveHit) returns zero candidates. The
normal is the negated movement direction — the SYNTHETIC value
slide_sphere's opposing-normals branch records (reversed = -gDelta).
- Cell 0x8A02016E has IDENTITY rotation (the prior session's 'rotation
maps the portal planes into the -X wall' was a misattribution). The
PortalSide polys to 0x011E are +-Y planes 1.4 m beside the player's
track, perpendicular to the +X run — pos_hits_sphere's directional
cull rejects them for that movement. They ARE referenced by the dat's
physics-BSP leaves (CorridorCell_PhysicsBspLeafMembership), so retail
tests them too when approached into their plane; the dat's
keep-PortalSide / strip-ExactMatch asymmetry reads as intentional
(solid window/grate-class portals). No portal-poly filter — exactly
the blanket-skip the pickup warned against.
- Port fix: CSphere::slide_sphere's opposing-normals branch
(0x005375d7-0x0053762c) records the reversed displacement and returns
COLLIDED_TS; our port returned OK ('retail returns OK here' was a
decomp misread), letting the step complete as-is with the synthetic
collision normal that validate's epilogue then persisted as the
sliding normal the wedge absorbed on. TransitionTypes opposing branch
now returns Collided; pinned by
SlideSphere_OpposingNormals_ReturnsCollided_WithReversedDisplacementNormal
(RED->GREEN).
- Dat-backed replay (Issue137CorridorSeamReplayTests) reproduces the
live hit frame verbatim (same in/out to the millimeter, same 016E->017A
transit, same +8mm settle) and runs the corridor CLEAN: hit=no, no
sliding normal persisted, six further forward frames advance freely.
- Inspection tests extended: physics-BSP leaf membership walk +
hit-normal candidate sweep + downward-poly sweep (all report-style,
dat-gated). Pickup prompt banner'd SUPERSEDED; ISSUES #137 updated
(door half stays open); audit doc extended with the resolution.
Suites: Core 2551 / App 713 / UI 425 / Net 385, 0 failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
87 lines
5.3 KiB
Markdown
87 lines
5.3 KiB
Markdown
# Pickup prompt — #137 corridor phantom collision (paste into a fresh session)
|
||
|
||
> **SUPERSEDED 2026-07-06.** The corridor phantom is FIXED (visual gate
|
||
> pending) — see `docs/research/2026-07-06-137-sliding-normal-lifecycle-audit.md`.
|
||
> Mechanism 2 was real (BSPQuery stub slide responses leaked sliding
|
||
> normals; fixed). Mechanism 1's framing was WRONG: the recorded wall
|
||
> normal was SYNTHETIC (slide_sphere's opposing branch + a `return OK` vs
|
||
> retail's COLLIDED_TS misport — fixed); the PortalSide polys are ±Y
|
||
> planes perpendicular to the run, directionally culled, tested by
|
||
> retail's own BSP leaves too, and plausibly legitimately solid
|
||
> (window/grate class). The step −1 cdb session below is NOT needed for
|
||
> this repro. Kept for the audit trail only.
|
||
|
||
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.
|