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
|
|
@ -1220,6 +1220,27 @@ matching the door's open/closed state.)
|
|||
(get_object_info pc:279992 governs only the NEXT frame — #116 notes);
|
||||
audit who clears the body's sliding normal when no contact recurs.
|
||||
|
||||
**MECHANISM 2 FIXED 2026-07-06 (audit complete — full lifecycle in
|
||||
`docs/research/2026-07-06-137-sliding-normal-lifecycle-audit.md`):**
|
||||
retail's ONLY in-transition sliding-normal writer is
|
||||
`validate_transition` (0x0050ac21); the BSP/sphere layer never writes it,
|
||||
and the body persistence (`SetPositionInternal` 0x005154c2/0x005154e1)
|
||||
is success-only. Our BSPQuery Contact-branch full-hit responses were
|
||||
STUBS (`SetSlidingNormal + return Slid`) where retail dispatches the
|
||||
real `slide_sphere` — the seam hit (a SUCCESSFUL full-advance resolve,
|
||||
`ok=True` in the log, not a failed one) leaked the phantom wall's normal
|
||||
into the body, and the seed absorbed every later forward push. Fix:
|
||||
both stub sites now route through the real
|
||||
`Transition.SlideSphereInternal` (`CSphere::slide_sphere` 0x00537440,
|
||||
in-frame, no sliding write) and the body writeback is gated on
|
||||
transition success. Pins: `Issue137SlidingNormalLifecycleTests` (2 site
|
||||
pins + the persist/absorb/clear wall lifecycle). Register: TS-4 amended
|
||||
(steep-tangent sites still write the normal — documented), TS-45 added
|
||||
(`SphereCollision`'s write — same class, out of blast radius). The
|
||||
absorbed exactly-anti-parallel frame at a REAL wall is retail-faithful
|
||||
(the persisted normal is a "still pressed" cache); only the phantom
|
||||
PROVENANCE was the bug. Corridor re-test rides the mechanism-1 session.
|
||||
|
||||
**Files:** `src/AcDream.Core/Physics/` (EnvCell collision, CellTransit, the door apparatus),
|
||||
`src/AcDream.Core/Physics/ShadowObjectRegistry.cs` (per-cell registration). See
|
||||
`claude-memory/project_physics_collision_digest.md` (the collision SSOT + DO-NOT-RETRY table).
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| TS-1 | PrecipiceSlide context missing — conservative stop-at-edge instead of retail's EdgeSlide → PrecipiceSlide / CliffSlide | `src/AcDream.Core/Physics/TransitionTypes.cs:1254` | Awaiting the next L.2c slice; a diagnostic records which ingredient (precipice context / steep plane / EdgeSlide flag) is missing | Player stops dead at precipice edges where retail slides along/over — visible mismatch at cliff and roof edges | retail EdgeSlide → PrecipiceSlide chain |
|
||||
| TS-2 | `BspOnlyDispatch` reduces retail's `(HAS_PHYSICS_BSP_PS && !pvpTargetPlayer && !missileIgnore)` to the flag test alone (M1.5 scope: no PK, no missiles) | `src/AcDream.Core/Physics/TransitionTypes.cs:660` | Both omitted terms are genuinely false pre-M2; comment directs wiring them with PK (M2+) and missiles (F.3) | If PK or missiles land without the terms, flagged entities get BSP-only where retail tests cyl+sphere — pass-through / wrong blocking in PvP/missile interactions | `FindObjCollisions` pc:276861; HAS_PHYSICS_BSP_PS acclient.h:2833 |
|
||||
| TS-3 | `FramesStationaryFall` accounting absent (`moved = true` unconditionally in the accepted-move branch) | `src/AcDream.Core/Physics/TransitionTypes.cs:3691` | Explicitly deferred to the full physics port | A body wedged falling-in-place never triggers retail's stuck-fall escalation — indefinite falling-animation wedges | CPhysicsObj frames_stationary_fall |
|
||||
| TS-4 | Path-6 steep-poly slide-tangent shortcut: airborne hits on >FloorZ polys skip retail's SetCollide → Path-4 → ContactPlane landing chain, returning Slid in place | `src/AcDream.Core/Physics/BSPQuery.cs:2001` | Deliberate deviation: our faithful port DID wedge (missing step_up_slide / cliff_slide details on grounded-steep); validated against the 2026-04-30 retail cdb trace (retail body didn't wedge). Filed L.5+ for retail-strict | Airborne steep contact never commits Contact / lands as retail — roof-bounce trajectories, landing events, grounded-steep transitions diverge | `BSPTREE::find_collisions` SetCollide pc:323783-323821 |
|
||||
| TS-4 | Path-6 steep-poly slide-tangent shortcut: airborne hits on >FloorZ polys skip retail's SetCollide → Path-4 → ContactPlane landing chain, returning Slid in place. **Includes a `SetSlidingNormal` write at both sites** — retail's BSP layer never writes `collision_info.sliding_normal` (only `validate_transition` 0x0050ac21 does; the #137 mechanism-2 class), so on transition success the steep-face normal persists to the body and seeds the next frame | `src/AcDream.Core/Physics/BSPQuery.cs` (Path-6 steep branches, `worldNormal.Z < FloorZ`) | Deliberate deviation: our faithful port DID wedge (missing step_up_slide / cliff_slide details on grounded-steep); validated against the 2026-04-30 retail cdb trace (retail body didn't wedge). Filed L.5+ for retail-strict | Airborne steep contact never commits Contact / lands as retail — roof-bounce trajectories, landing events, grounded-steep transitions diverge; a persisted steep-face normal can absorb an exactly-anti-parallel next-frame push (#137 wedge class) until an oblique input clears it | `BSPTREE::find_collisions` SetCollide pc:323783-323821 |
|
||||
| TS-5 | `CanJump` always true — burden/stamina gating deferred (stat plumbing incomplete pre-M2). R3-W3 extends this row: `IWeenieObject.JumpStaminaCost`/`PlayerWeenie.JumpStaminaCost` are new (feeding `jump_is_allowed`'s verbatim stamina-refusal branch) and are ALSO always-affordable/cost-0 stubs for the same reason | `src/AcDream.Core/Physics/PlayerWeenie.cs:44` (`CanJump`), `:52` (`JumpStaminaCost`, R3-W3) | Marked deferred; harmless until stats matter | Client launches jumps retail refuses (exhausted/overburdened) — server rejection / rubber-band; divergent jump availability vs retail muscle memory | CMotionInterp jump path stamina/burden inquiry; `jump_is_allowed` 0x005282b0 `JumpStaminaCost` vtable +0x44 |
|
||||
| TS-6 | Weather particle emission suppressed — all weathery DayGroups map to Overcast (correct fog/cloud tone, no precipitation); retail's camera-attached weather subsystem not yet located in the decomp | `src/AcDream.Core/World/WeatherState.cs:200` | Decomp research verified the sky loop never reads `DefaultPesObjectId`; an earlier name-based rain spawn regressed (rained where retail didn't, 2026-04-23) — inventing a name→rain path is forbidden until the real subsystem is found | Rainy/snowy/stormy days never show retail's precipitation effects (permanent missing visuals until the subsystem is found and ported) | FUN_00508010 / FUN_0051bed0→FUN_0051bfb0 (negative findings) |
|
||||
| TS-7 | SkyObject `weather_enabled` gate not honored — weather-flagged sky objects (bit 0x04) always instantiate | `src/AcDream.Core/World/SkyDescLoader.cs:50` | No weather_enabled toggle exists yet; IsWeather flag parsed + documented as the gate to wire | Weather-only sky meshes (rain cylinders) appear where retail-with-weather-off suppresses them | `GameSky::MakeObject` 0x00506ee0, guard at decomp:268630 |
|
||||
|
|
@ -225,6 +225,7 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| TS-41 | Grounded remote NPCs WITHOUT an armed moveto are body-driven by UP-synthesized server velocity (`HasServerVelocity` → the SERVERVEL per-tick leg: `Body.Velocity = ServerVelocity`, `MovementManager.UseTime` (the R5-V5 facade relay, ex-loose `MoveToManager.UseTime`) SKIPPED, stale-decay stop via `ApplyServerControlledVelocityCycle(Zero)`); retail has no wire-velocity leg-driver anywhere — `MovementManager::UseTime` runs UNCONDITIONALLY per tick and between-UP translation comes from the motion state (`get_state_velocity`), UPs only hard-snap. **#170 residual fix narrowed this branch: an ARMED moveto (`MovementTypeState != Invalid`) now always takes the MOVETO leg** — the old arbitration starved the verbatim MoveToManager for exactly the duration of a server-side chase (UPs flowing → UseTime never ran → legs stayed Ready while the body glided = the #170 slide; live funnel 16 arms → 1 run install). **R5-V3 (#171) narrowed it again: a STUCK entity (`PositionManager.GetStickyObjectId() != 0`) also takes the MOVETO leg** — after the sticky arrival the moveto is cleaned (Invalid) but `StickyManager::adjust_offset` owns the between-snap translation; SERVERVEL would glide the body against the sticky steer (same starvation class) | `src/AcDream.App/Rendering/GameWindow.cs` (`TickAnimations` grounded NPC branch, `moveToArmed`+`stickyArmed` gate) | ACE moves some entities by position updates alone (scripted paths, missiles) with no UM/moveto stream — without a velocity fallback they freeze between UPs; entities WITH a moveto now get the retail drive | An entity class that carries BOTH wire velocity and an armed moveto with conflicting truths follows the moveto; UP hard-snaps bound the drift. Non-moveto entities keep the non-retail stale-stop heuristics (AP-80 thresholds) | `CPhysicsObj::UpdateObjectInternal` 0x005156b0 (`MovementManager::UseTime` call @0x00515998, unconditional); retire in R6 (full per-tick order) |
|
||||
| TS-42 | Per-tick DRAIN ORDER inverted vs retail: acdream's `TickAnimations` runs `HandleTargetting` → `Movement.UseTime` (the R5-V5 MovementManager relay, ex-loose `MoveTo.UseTime`) FIRST and the animation-completion drain (Sequencer.Advance → AnimDone hooks → `MotionTableManager.AnimationDone`/`UseTime` → `CMotionInterp.MotionDone` pops) LAST, so every motion-completion-gated decision (`BeginTurnToHeading`'s `motions_pending` wait) sees a queue that is one frame STALE — the unblock after a stop/swing lands one frame later than retail. Retail order (pinned from the named decomp this session): `UpdatePositionInternal` (CPartArray::Update + `process_hooks` @0x00512d3d — the drain) runs BEFORE `TargetManager::HandleTargetting` @0x00515989 → `MovementManager::UseTime` @0x00515998 → `CPartArray::HandleMovement` @0x005159a4 (zero-tick sweep) in `UpdateObjectInternal` | `src/AcDream.App/Rendering/GameWindow.cs` (`TickAnimations` per-entity phase order; the R2-Q4 comment already marks the placement "provisional until R6") | Bounded to exactly ONE frame (~16 ms) of extra latency per completion-gated event; every queue eventually drains identically (RemoteChaseEndToEndHarnessTests conformance) | Motion-completion-gated transitions (chase turn start, post-swing re-arm) systematically lag retail by one frame; under compound churn the lag can cost an extra retry cycle | `CPhysicsObj::UpdateObjectInternal` 0x005156b0 + `UpdatePositionInternal` 0x00512c30 (`process_hooks` @0x00512d3d); retire in R6 (retail UpdateObjectInternal order) |
|
||||
| TS-44 | NPC UpdatePosition hard-snaps (position @`OnLivePositionUpdated` + orientation + velocity/cycle adoption) are SUPPRESSED while the entity is stuck (`PositionManager.GetStickyObjectId() != 0`) — an adaptation of retail's chain semantics to the legacy snap path: retail routes UP corrections through the InterpolationManager into the SAME per-tick `PositionManager::adjust_offset` chain where `StickyManager::adjust_offset` OVERWRITES them while armed (0x00555190 order, 0x00555430 assigns m_fOrigin), so a server correction can never fight an armed stick; the legacy NPC path snaps OUTSIDE the chain, producing snap-out/steer-back position flapping + stale-facing stomps (the 2026-07-04 #171 gate residuals). Bookkeeping (`LastServerPos/Time`, cell) still records; server truth reasserts on the first UP after unstick, bounded by the 1 s sticky lease | `src/AcDream.App/Rendering/GameWindow.cs` (`OnLivePositionUpdated` NPC section, `snapSuppressedByStick` gate) | Retail's mechanism (sticky-overwrites-interp) is unreachable until the NPC path gets the interp-queue architecture (the player-remote branch already has it — the R5-V3 combiner→sticky chain); the gate reproduces the retail-observable behavior on the snap architecture | A stick that stays armed while ACE moves the monster far (shouldn't happen — sticks follow the target by construction) would drift until unstick+next UP; worst case bounded by the 1 s lease + the next UM re-arm | `PositionManager::adjust_offset` 0x00555190; `InterpolationManager` UP routing (`CPhysicsObj::MoveOrTeleport`); retire when the NPC path unifies onto the interp queue (S6/R6) |
|
||||
| TS-45 | `SphereCollision` (the shadow-object Sphere response) is a hand-rolled 3-D wall-slide that ALSO calls `SetSlidingNormal` — retail's `CSphere::intersects_sphere` (0x00537A80) dispatches `CSphere::slide_sphere` (0x00537440), which slides in-frame and never writes `collision_info.sliding_normal` (the only in-transition writer is `validate_transition` 0x0050ac21). Same leak class as the #137 mechanism-2 stubs fixed 2026-07-06 (BSPQuery Contact branch); left in place because the response's blocking semantics for sphere-shaped server objects are untested against the real slide and #171 sticky-melee behavior is freshly gated | `src/AcDream.Core/Physics/TransitionTypes.cs` (`SphereCollision`, the `ci.SetSlidingNormal` tail) | The in-frame push-out already moves the check position; the extra sliding normal only persists on transition success, and pure-Sphere shadow shapes are rare (most creatures/statics are CylSphere, which routes through the real `SlideSphere` since #172) | A sphere-shaped object touch persists a normal retail would discard — an exactly-anti-parallel follow-up push absorbs to a zero offset (#137 wedge class) at that object until an oblique input clears it | `CSphere::intersects_sphere` 0x00537A80 → `slide_sphere` 0x00537440 (pc:321678+); fix = route the tail through `Transition.SlideSphere` like `CylSlideSphere` does |
|
||||
| TS-43 | Remote teleport has no `teleport_hook` equivalent: retail tears down the position managers on every teleport (`CPhysicsObj::teleport_hook` 0x00514ed0 — `CancelMoveTo(0x3c)` @0x00514edf, `PositionManager::UnStick` @0x00514eee, `StopInterpolating`/`UnConstrain`); acdream's remote teleport is a bare UP hard-snap, so a stuck/chasing remote that the server teleports keeps its stick/moveto for up to the 1 s sticky lease / next UM. The LOCAL player side IS wired (R5-V3: `PlayerMovementController.SetPosition` → `PositionManager.UnStick`; the moveto cancel was already there via `StopCompletely`; the teleport-arrival site also fires the hook's tail — `EntityPhysicsHost.NotifyTeleported` = `TargetManager::ClearTarget` + `NotifyVoyeurOfEvent(Teleported)` @0x00514f1b-0x00514f28, which is what makes mobs stuck to the player drop their sticks on a recall) | `src/AcDream.App/Rendering/GameWindow.cs` (`OnLivePositionUpdated` — no teleport-flag manager teardown for remotes) | Remote teleports are rare (recalls/summons); the sticky 1 s lease + UP hard-snaps self-correct within a second; wiring it properly wants the UP teleport-stamp plumbing (TS-26's stamp work) | A teleported-away attacker briefly steers toward its pre-teleport target from the new location (≤1 s) before the lease/next-UM corrects it | `CPhysicsObj::teleport_hook` 0x00514ed0; retire with the TS-26 UP-stamp port |
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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