fix #137 (mechanism 2): BSP full-hit stubs leaked sliding normals — the corridor absorbing wedge
The Facility Hub corridor dead-stop's second half: after one seam hit, every forward resolve returned ok=False hit=no with zero advance. The body-persisted SlidingNormal (-1,0,0) projected the exactly-anti-parallel corridor push to zero in AdjustOffset and the step loop aborted at step 0 before any collision test could refresh the state. Audit (docs/research/2026-07-06-137-sliding-normal-lifecycle-audit.md): retail's only in-transition sliding-normal writer is validate_transition (0x0050ac21); the whole sphere/BSP layer writes NONE (grep-verified), and the body persistence (SetPositionInternal 0x005154c2, SLIDING_TS bit sync 0x005154e1) runs only on transition success. Our BSPQuery Contact-branch full-hit responses were stubs (SetCollisionNormal + SetSlidingNormal + return Slid) where retail dispatches the real slide_sphere — so the seam hit (a SUCCESSFUL full-advance resolve per the live log) persisted the phantom wall's normal, which retail's lifecycle structurally cannot do. - BSPQuery Contact foot full-hit fallback + head full-hit now route through Transition.SlideSphereInternal (CSphere::slide_sphere 0x00537440 — in-frame slide, no sliding-normal write; ACE BSPTree.cs:202,310-316). The dead stub is rewritten as the faithful BSPTREE::slide_sphere wrapper. - PhysicsEngine sliding writeback gated on ok (retail success-only placement; behaviorally latent, removes the failed-frame leak class). - Register: TS-4 amended (Path-6 steep-tangent sites still write the normal — now documented), TS-45 added (SphereCollision's write — same leak class, left for a follow-up out of #171's blast radius). - Pins: Issue137SlidingNormalLifecycleTests — both site pins RED->GREEN, plus the retail persist/absorb/clear wall lifecycle (validate-write persistence, faithful absorbed anti-parallel frame, oblique escape clears the bit). BSPQueryTests full-hit pin updated to the real slide. Mechanism 1 (PortalSide portal polys solid in the physics set) stays OPEN - #137 not closed; the corridor re-test rides that session. Suites: Core 2545 / App 713 / UI 425 / Net 385, 0 failures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e73e45da54
commit
a11df5b8d3
7 changed files with 474 additions and 31 deletions
|
|
@ -0,0 +1,82 @@
|
|||
# #137 mechanism 2 — the sliding-normal lifecycle audit (2026-07-06)
|
||||
|
||||
The pickup prompt (`2026-07-06-137-corridor-phantom-pickup-prompt.md`) asked:
|
||||
*who writes the body's persisted SlidingNormal, and where does retail CLEAR
|
||||
it when contact does not recur?* This note is the complete decomp-verified
|
||||
answer, the divergence map it produced, and the fix that shipped.
|
||||
|
||||
## Retail lifecycle (every site, named-retail-verified)
|
||||
|
||||
`collision_info.sliding_normal` (per-transition) and
|
||||
`CPhysicsObj::sliding_normal` + `SLIDING_TS` (transient_state bit 4,
|
||||
body-persisted) form a two-level cache:
|
||||
|
||||
| Step | Function | Address / pc line | What it does |
|
||||
|---|---|---|---|
|
||||
| seed | `CPhysicsObj::get_object_info` | 0x00511cc0, seed @0x00511d44 | `if (transient_state & 4) CTransition::init_sliding_normal(&this->sliding_normal)` — last frame's persisted normal seeds the new transition |
|
||||
| consume | `CTransition::adjust_offset` | 0x0050a370 | `dot(offset, sliding_normal) < 0` → project the per-step offset (crease `cross(contact_plane.N, sliding_normal)` when grounded, `offset −= n·dot` otherwise); `dot >= 0` (moving away) → `sliding_normal_valid = 0` |
|
||||
| step gate | `CTransition::find_transitional_position` | 0x0050bdf0, small-offset @0x0050bf83-0x0050bfb7 | adjusted offset `|off|² < EPSILON²` at step 0 → transition FAILS; at step i>0 → succeed iff last validate state OK. **The absorbed frame is retail-faithful** — the persisted normal is a "still pressed against this wall" cache that suppresses re-testing |
|
||||
| per-step clear | same | @0x0050c010 | `sliding_normal_valid = 0` (+ contact plane) BEFORE each step's `transitional_insert` — a step that runs and does not re-collide leaves the transition clean |
|
||||
| in-transition write | `CTransition::validate_transition` | 0x0050aa70, write @0x0050ac21-ac30 | `if (collision_normal_valid) set_sliding_normal(collision_normal)` — **the ONLY in-transition writer**. Fires when a step needed collision handling |
|
||||
| body writeback | `CPhysicsObj::SetPositionInternal` | copy @0x005154c2, bit sync @0x005154e1 | `sliding_normal = transition's; SLIDING_TS ⇔ sliding_normal_valid`. **Success-only** — a failed `find_valid_position` discards the transition whole; the body keeps its prior state |
|
||||
| NOT writers | `CSphere::slide_sphere` 0x00537440, `CCylSphere::slide_sphere` 0x0053b2a0, `BSPTREE::slide_sphere`/`step_sphere_up`/`find_collisions` | pc:321400+, 323700+ | grep-verified: **zero** `sliding_normal` references in the whole sphere/BSP layer (nothing between pc 283518 and 1155326). The sphere-level slide is IN-FRAME (`add_offset_to_check_pos`) |
|
||||
|
||||
So the answer to "where does retail clear it": **the success writeback**
|
||||
(bit 4 syncs to the transition's final `sliding_normal_valid`, which the
|
||||
per-step clear leaves false unless the last step's validate re-recorded a
|
||||
collision) plus `adjust_offset`'s moving-away invalidation. On a FAILED
|
||||
transition nothing clears it — and nothing needs to, because a persisted
|
||||
normal can only have come from a validate write against real geometry
|
||||
(pressed-at-a-wall is a correct absorbed state; any oblique input escapes
|
||||
via the tangential projection remainder and the escape frame's writeback
|
||||
clears the bit).
|
||||
|
||||
ACE mirrors all of it: seed `PhysicsObj.cs:2611`, writeback
|
||||
`PhysicsObj.cs:1249-1251`, validate write `Transition.cs:1027`, the only
|
||||
`SetSlidingNormal` call sites in ACE's whole physics tree.
|
||||
|
||||
## The wedge (live evidence, launch-175-verify2.log:42858)
|
||||
|
||||
The seam-hit frame **succeeded with full advance** (`ok=True`,
|
||||
`out == tgt` in XY, +8 mm step-up settle, crossing 0x8A02016E→0x8A02017A)
|
||||
and still recorded `hit=yes n=(−1.00,0.03,−0.03)`. Retail ending that frame
|
||||
would write back `sliding_normal_valid=0` (no blocked step at the end → the
|
||||
per-step clear wins) and the bit would CLEAR. We persisted a normal anyway —
|
||||
because our BSP Contact branch carried **stub** slide responses
|
||||
(`SetCollisionNormal + SetSlidingNormal + return Slid`) at the sites where
|
||||
retail dispatches the real `slide_sphere`. Every following forward resolve
|
||||
then seeded the stale normal, `adjust_offset` projected the
|
||||
exactly-anti-parallel corridor push to zero, and the step-0 abort returned
|
||||
`ok=False hit=no` with zero advance — before any collision test could
|
||||
refresh the state. An absorbing wedge; strafing escapes because an oblique
|
||||
offset keeps a tangential remainder.
|
||||
|
||||
## Divergence map → what shipped
|
||||
|
||||
| Site | Was | Retail | Action |
|
||||
|---|---|---|---|
|
||||
| `BSPQuery` Contact foot full-hit, step-up unavailable (recursion guard / engine-null) | stub | blocked step-up funnels to `step_up_slide` → `CSphere::slide_sphere` | **FIXED** — routes through `Transition.SlideSphereInternal` (the real port, #116-verified thresholds) |
|
||||
| `BSPQuery` Contact head full-hit | stub | `BSPTREE::slide_sphere` @0x0053a697 (ACE BSPTree.cs:202, 310-316 — slides GlobalSphere[0]) | **FIXED** — same routing; the dead private stub rewritten as the faithful `BSPTREE::slide_sphere` wrapper |
|
||||
| `PhysicsEngine.ResolveWithTransition` sliding writeback | unconditional (ran on `ok=False`) | `SetPositionInternal` success-only | **FIXED** — gated on `ok` (behaviorally latent today: a failed transition's ci always still holds the seed, so gate-vs-rewrite is value-identical; the gate removes the class) |
|
||||
| `BSPQuery` Path-6 steep slide-tangent (2 sites) | in-frame projection + `SetSlidingNormal` | no BSP-layer write | left (documented deviation TS-4 — row amended to name the sliding write); L.5+ retail-strict follow-up |
|
||||
| `Transition.SphereCollision` (shadow Sphere objects) | hand-rolled slide + `SetSlidingNormal` | `CSphere::intersects_sphere` → `slide_sphere`, no write | left — **new register row TS-45**; fix = route the tail through `SlideSphere` like `CylSlideSphere` (#172) does |
|
||||
| seed / step loop / `AdjustOffset` / validate write @TransitionTypes:4317 / real `SlideSphere` port | — | — | verified faithful, unchanged |
|
||||
|
||||
Tests: `Issue137SlidingNormalLifecycleTests` — two site pins (Contact
|
||||
foot-fallback + head full-hit must not write the sliding normal; face-on
|
||||
grounded → `Collided` per the degenerate crease projection) + the
|
||||
engine-level wall lifecycle pin (persist-on-block via validate →
|
||||
absorbed exactly-anti-parallel frame → oblique escape CLEARS the body
|
||||
state). Full solution suite green (Core 2545 / App 713 / UI 425 / Net 385).
|
||||
|
||||
## What this does and does not fix in the corridor
|
||||
|
||||
The wedge (mechanism 2) is fixed at its entry: a full-advance hit can no
|
||||
longer persist a phantom normal, so the corridor dead-stop degrades to
|
||||
whatever mechanism 1 actually produces — a momentary stutter if the portal
|
||||
poly only face-hits in the crossing window, or an honest (re-tested,
|
||||
strafe-escapable) wall if it keeps colliding. **Mechanism 1 — PortalSide
|
||||
portal polygons being solid in our physics set — is still open**; the
|
||||
pickup's step −1 cdb session (trace whether retail's BSP test chain ever
|
||||
TESTS those polys, or membership order never reaches them from the passable
|
||||
side) remains the next action, and needs the user to run retail.
|
||||
Loading…
Add table
Add a link
Reference in a new issue