acdream/docs/research/2026-07-30-ts4-116-oracle-plan.md

36 KiB

TS-4 / #116 oracle pass — Campaign P final physics slice

Status: RESEARCH ONLY. No source changes. This is a follow-up oracle pass on top of docs/research/2026-07-30-response-layer-edge-family-pseudocode.md (hereafter "the P2 doc"), specifically its §4 (TS-4), §5 (#116), and §7 item 6 (the P2 implementation attempt's wedge diagnosis). That attempt correctly localized the freeze to TransitionalInsert's Phase 2 retry loop but concluded the mechanism was "Phase 3 structurally unreachable" without tracing far enough to find the actual convergence/divergence point. This pass reads one layer deeper — into BSPQuery.cs's Path 4 dispatch (the path.Collide gate) and AdjustOffset's crease-projection math — and finds a concrete, retail-decomp-cited mechanism for both TS-4 and (as a byproduct of reading the same dispatch structure) strong new evidence for #116 shapes 1 and 2.

Every claim is tagged FACT (read directly from the named-retail pseudo-C, ACE source, or current acdream source in this worktree, with file:line / address citations) or INFERENCE (derived from those FACTs by direct reasoning, not yet confirmed by a live capture/cdb run).


0. Binding DO-NOT-RETRY entries (copied verbatim)

From memory/project_physics_collision_digest.md (3-day-old snapshot, re-verified against current source where cited below) and docs/ISSUES.md #116:

  1. Do NOT add SetSlidingNormal calls in the BSP/sphere collision layer. Retail's only in-transition writer of collision_info.sliding_normal is validate_transition (0x0050ac21/0x0050aa70). A leaked normal + success writeback = an absorbing wedge at empty space. This pass's TS-4 finding is a variant of exactly this failure class — see §1 below — but the writer in question (validate_transition's unconditional SetSlidingNormal(CollisionNormal)) IS the retail-faithful one; the problem is not an extra writer, it's what AdjustOffset does with a placeholder UnitZ value when it reads SlidingNormal back.
  2. Do NOT re-add a forced constant-shell de-penetration. Retail slides tangentially and never force-separates.
  3. SphereCollision no longer calls SetSlidingNormal (TS-45 retired) — keep it that way.
  4. Do NOT patch the degenerate-offset guard in slide_sphere ad hoc for #116 — oracle-driven only.
  5. Do NOT re-introduce a topology-based outside-add / radial sweep to cell membership while touching this family.
  6. calc_friction threshold is retail 0.25 vs acdream 0.0` (AP-7) — orthogonal to this slice, do not fold in.
  7. Shape-1 of #116 is NOT the degenerate-offset guard threshold — that guard kills slides under ~1.4 cm; the lost tick-22760 slide was 3.57 cm. The divergence is the collision-normal SOURCE.
  8. Do NOT guess the BN test ah,5 x87 branch polarity/squaring in slide_sphere — Ghidra MCP is down for this pass too; this pass does not touch that question (see §3, shape-2 — the finding here is about dispatch routing, not the x87 comparisons inside slide_sphere/AdjustOffset themselves, which remain unconfirmed and out of scope).
  9. AP-4 (CliffSlide check moved before retail's Branch-1 gate) — a live, load-bearing reordering. Not touched by this pass.
  10. TS-46 (two-scalar sphere reconstruction) is OUT OF SCOPE.

1. TS-4 — the actual convergence/divergence mechanism

1.1 Summary answer (read this first)

Retail does not "avoid" the Adjusted↔retry oscillation inside transitional_insert's attempt loop any differently than acdream does — both structurally deadlock the same way within a single resolve. What lets retail's live trace escape (and what the P2 fixture's synthetic trajectory does not) is that retail's AdjustOffset (CTransition::adjust_offset, 0x0050a370) re-projects the next tick's gravity offset through whatever ContactPlane + SlidingNormal survived the previous tick's collision — and for a pure, zero-horizontal-velocity vertical fall onto a steep surface, that projection is mathematically degenerate and crushes the offset to (near-)zero every tick, which abort-small-offsets before TransitionalInsert even runs again. This is retail-faithful behavior, present identically in the raw decomp, in ACE's port, and in acdream's current port — it is not a bug introduced by the TS-4 shortcut's removal. The Ts4SteepRoofWedgeCaptureTests fixture reproduces it because it drops the body straight down with zero horizontal velocity, which is very likely a different (and more degenerate) input than the live 2026-04-30 debugger trace that validated the shortcut (a player jumping or running onto a roof, which has residual horizontal velocity).

1.2 The chain, FACT by FACT

Step A — Path 6 fires, sets Collide, does not reposition (FACT). BSPQuery.cs:2217-2224 (faithful branch, shortcut removed):

path.SetCollide(worldNormal0);
path.WalkableAllowance = PhysicsGlobals.LandingZ;
return TransitionState.Adjusted;

SpherePath.SetCollide (TransitionTypes.cs:752-759) only sets Collide=true, backs up CheckPos, and stores StepUpNormal — it does not touch CollisionInfo.ContactPlane or CollisionNormal. Matches retail exactly: pseudo-C:323818-323821 (0x0053a7bf, SPHEREPATH::set_collide(&sphere_path, &normal); walkable_allowance = 0.0871556997f; return 3;) — no set_collision_normal, no set_contact_plane call at this site either.

Step B — the SAME attempt's retry does NOT re-hit Path 6; it routes to Path 4 (FACT, both acdream and retail). BSPQuery.cs:1961 gates on if (path.Collide) — checked before the Path 5/6 tests, at the top of the same dispatch function. Since Collide was just set in Step A and is never cleared except inside TransitionalInsert's Phase 3 (sp.Collide = false at TransitionTypes.cs:1816, reachable only on an OK result — never reached while Path 6/Path 4 keep returning Adjusted), every subsequent attempt (within the same resolve and across ticks) dispatches to Path 4, not back to Path 6. Retail: raw pseudo-C:323784 if (eax->sphere_path.collide == 0) {...} else {...} — the identical gate, at the identical position in the dispatch (confirmed independently against ACE BSPTree.cs:163-187, if (path.Collide) { RootNode.find_walkable(...); if (changed) {... return Adjusted;} else return OK; }).

Step C — Path 4 (FindWalkableInternal) is what actually establishes ContactPlaneValid (FACT). BSPQuery.cs:1968-2018: calls FindWalkableInternal; if it finds a candidate (changed && hitPoly is not null), it repositions the sphere (path.AddOffsetToCheckPos(worldOffset)), sets a real ContactPlane via collisions.SetContactPlane(worldPlane, ...) (line 2006), caches the walkable polygon (SetWalkableTransformed), and returns Adjusted. This is the only site that gives the mover a real (steep) contact plane in this whole trajectory — not the Phase-3 DoCheckWalkable gate the P2 doc's item-6 diagnosis assumed was the relevant site (that gate is downstream and, per Step B, unreachable here). Matches ACE BSPTree.cs:163-184 exactly (SetContactPlane, SetWalkable, return Adjusted).

Step D — the attempt-exhausted Adjusted gets collapsed to OK with position reverted, but ContactPlaneValid survives the revert (FACT, both engines). TransitionalInsert's outer for-loop exhausts (acdream hardcodes return TransitionState.Slid; at TransitionTypes.cs:2093; ACE/retail return the true last value, Adjusted here — see §1.4 for why this particular divergence doesn't change the outcome). Either way, ValidateTransition's "not OK" branch runs (TransitionTypes.cs:5493-5501): if (!CollisionNormalValid) SetCollisionNormal(UnitZ); (fires — Path 4/6 never touched CollisionNormal, only ContactPlane/StepUpNormal), then SetCheckPos(CurPos, CurCellId) (revert — no net movement), transitionState = OK. Retail: pseudo-C:272563-272596 (0x0050aad9), identical collapse (COLLIDED_TS/ADJUSTED_TS/SLID_TS all treated the same, default CollisionNormal=UnitZ if unset, revert check_pos to curr_pos). Crucially, none of this touches ContactPlaneValid — it carries forward from Step C untouched by the revert. Then the shared tail (TransitionTypes.cs:5504-5533, retail pc:272621-272656) runs: if (CollisionNormalValid) SetSlidingNormal(CollisionNormal) — now SlidingNormal = UnitZ (the placeholder from the default, not a real second surface) — and if (ContactPlaneValid) { ...; oi.State|=Contact; if (Normal.Z>=FloorZ) OnWalkable=true else false; } — since the steep polygon's Normal.Z (≈0.447 for the fixture's 63.4° slope) < FloorZ (≈0.664), OnWalkable stays false but Contact becomes true. This exactly reproduces the fixture's own captured state at the landing tick: InContact=true, OnWalkable=false.

Step E — the NEXT tick's AdjustOffset crushes a purely-vertical offset to zero (FACT for the math, INFERENCE that this is the actual observed freeze cause — not independently re-run this pass). TransitionTypes.cs:4936-5014 (acdream), Transition.cs:34-87 (ACE), pseudo-C:272271-272393 (0x0050a370, retail) are all structurally identical:

slidingAngle = Dot(offset, SlidingNormal)
if (SlidingNormalValid) { if (slidingAngle < 0) checkSlide = true; else SlidingNormalValid = false; }
...
if (checkSlide) {
    slideOffset = Cross(ContactPlane.Normal, SlidingNormal)
    normalize slideOffset (or zero out if degenerate)
    result = Dot(slideOffset, offset) * slideOffset
}

With offset = (0, 0, -dz) (pure gravity, zero horizontal component), SlidingNormal = UnitZ = (0,0,1): slidingAngle = -dz < 0checkSlide = true. slideOffset = Cross(ContactPlane.Normal, UnitZ) — for any non-vertical plane normal N=(Nx,Ny,Nz), this cross product is (Ny, -Nx, 0) — a horizontal vector (Z=0), lying in the slope's contour line (perpendicular to the downhill direction), not the degenerate/near-zero case (the 63.4° slope's normal is not parallel to UnitZ, so NormalizeCheckSmall does not fire). Dot(slideOffset, offset) = Dot((Ny,-Nx,0), (0,0,-dz)) = 0 exactly, because slideOffset.Z = 0 and offset is purely Z. result = 0 * slideOffset = Vector3.Zero. The projected GlobalOffset is zero (up to float noise), which trips the "abort-small-offset" guard (TransitionTypes.cs:1466-1478, retail's non-viewer |offset|² < F_EPSILON² gate at pseudo-C:272845/0x0050bdf0, cited already in the existing AdjustOffset port comment) before TransitionalInsert is even called again — so ValidateTransition never runs on subsequent ticks either, meaning the stale ContactPlaneValid/SlidingNormal=UnitZ state simply perpetuates unchanged, forever. This is the freeze.

Step F — why the existing frames_stationary_fall (fsf) escape valve can't rescue this case (INFERENCE, follows directly from Step E). The digest's #182 rebuild already ported retail's fsf ladder (TransitionTypes.cs:5625-5667, ACE Transition.cs:1029-1061, pseudo-C:272625-656) — after 3 consecutive non-advancing ticks it manufactures a flat UnitZ contact plane and forces OnWalkable=true, which is exactly the kind of "unstick" mechanism one would look for here. But that ladder lives inside ValidateTransition, which Step E's abort-small-offset guard prevents from ever running again once the crease projection first crushes the offset to zero. The rescue mechanism is downstream of a gate the degenerate input never lets execution reach — in both acdream and (per identical source) retail.

1.3 Why this reconciles the shortcut's own "retail did not wedge" comment (INFERENCE)

The shortcut's comment (BSPQuery.cs:2190-2199) says the interim fix was "Validated against retail debugger trace 2026-04-30: retail body did not wedge." A live player jumping or walking onto a roof virtually always carries some horizontal velocity component (WASD input, residual momentum). For a non-purely-vertical offset, Dot(slideOffset, offset) is generally non-zero (only a component exactly along the pure downhill/gravity line is annihilated by this specific cross product — any lateral drift survives), so AdjustOffset would produce a small but non-zero sideways offset each tick — enough to move the sphere off the exact same collision point, avoid the abort-small-offset short-circuit, let TransitionalInsert/ValidateTransition run again, and (via repeated Path-4 find_walkable re-probes and the fsf ladder) eventually resolve. The Ts4SteepRoofWedgeCaptureTests fixture's pos = (0.5, 0, 3.0) straight-down drop with fallVelocityZ as the only non-zero component is very likely a stricter, more degenerate input than the live 2026-04-30 repro ever exercised. This is not yet independently re-confirmed by re-running the fixture with a horizontal component (see §4 Step 1 below for the concrete next action), so it is flagged INFERENCE — but it is the only hypothesis consistent with every FACT gathered in §1.2, and it does not require inventing any new mechanism.

1.4 The acdream-only bug that does NOT explain the freeze, but is real and should still be fixed

TransitionTypes.cs:2091-2093:

// Exhausted retry attempts — return whatever the last iteration said.
// (Defaults to Slid in practice since that's the only case that retries.)
return TransitionState.Slid;

This is hardcoded, not "whatever the last iteration said" as the comment claims. ACE's equivalent (Transition.cs:933, return transitState;) and retail's (pseudo-C:273363, 0x0050b949, return edi;) both return the true last value — Adjusted in this scenario, not Slid. FACT: this is a real, citable divergence. FACT: it does not explain the freezeValidateTransition's "not OK" branch (§1.2 Step D) treats Collided/Adjusted/Slid identically (acdream TransitionTypes.cs:5493-5501, ACE Transition.cs:993-1017, retail pseudo-C:272563-272596 all gate on result > OK_TS && result <= SLID_TS as one combined range, with no per-value branching). Fixing the hardcoded return is a one-line, zero-risk correctness fix (worth doing — it's a real citable port-accuracy bug and prevents future confusion when tracing this loop) but it is not the TS-4 fix and should not be presented as one.

1.5 What TS-4's actual fix shape is, given this

The mechanism in §1.2 is not something BSPQuery.cs's Path 6 can fix by itself — the freeze happens one tick after Path 6/Path 4 run, inside AdjustOffset, and is a property of the (already retail-faithful) validate_transition + adjust_offset pipeline reacting to a specific degenerate trajectory. Concretely, TS-4's shortcut removal is very likely safe for the realistic case (nonzero horizontal velocity) and only exposes this specific zero-horizontal-velocity degenerate, which:

  • may be a genuine, narrow, retail-faithful edge case (a player falling perfectly plumb onto a slope with zero horizontal drift essentially never happens in live play — WASD input, camera-relative movement, and even tiny numerical noise almost always inject some horizontal component), in which case it is not a blocker for TS-4 at all and should be documented as an accepted (retail-matching) corner case rather than "fixed", or
  • may indicate the fixture itself should be revised to match the original live repro's actual trajectory shape (nonzero horizontal velocity) before it's trusted as TS-4's gating fixture.

See §4 for the concrete, low-cost verification step (re-run the fixture with a small horizontal velocity component) that would settle which of these is true without guessing.


2. #116 shape-1 — collision-normal recording divergence (new candidate, INFERENCE, needs one instrumentation run to confirm)

2.1 What the existing research already ruled out (FACT, restated)

Ghidra-confirmed (2026-06-12, digest lines 1268-1275): acdream's cn=UnitZ default on a blocked move is retail-faithful (validate_transition does the identical default). The real divergence is upstream — at tick-22760, acdream's collision_normal_valid was false where retail's was true (retail had recorded the door-face normal (0,+1,0)). The candidate site named in the P2 doc §5 was "the PathClipped/collide_with_pt arm... or a sibling Path-1-class function not yet read."

2.2 PathClipped is NOT the answer (checked this pass, negative result — FACT)

ObjectInfoState.PathClipped (TransitionTypes.cs:32, bit 0x8) is only set on a mover when MoverPhysicsState & PhysicsStateFlags.Missile != 0 (PhysicsEngine.cs:1160-1163), with an explicit citation to retail's own CPhysicsObj::get_object_info (0x00511CC0): "Missile contributes PathClipped only." A normal player push against a door is not a missile mover, so neither acdream nor retail would set PathClipped for this scenario — this rules out "PathClipped state differs between engines" as shape-1's cause. (The camera/viewer sweep does carry PathClipped via a different, explicit caller-supplied flag, but that's a different mover than the one in the tick-22760 door-push capture.)

2.3 The real candidate: acdream's Path-6 sphere1(head)-hit handling diverges from retail/ACE (FACT for the divergence, INFERENCE that it explains tick-22760)

Retail's BSPTREE::find_collisions, in the not-yet-in-Contact branch (state&1==0, i.e. airborne / first contact — pseudo-C:323784- 323836, 0x0053a4e3-0x0053a730+): when sphere0 (foot) does not hit but num_sphere > 1 and sphere1 (head) does hit, retail does not defer through SetCollide/Adjusted — it calls COLLISIONINFO::set_collision_normal directly with the head poly's transformed normal and returns COLLIDED_TS (2) immediately (pseudo-C:323824-323834, 0x0053a793/0x0053a7a4). Cross-checked independently against ACE BSPTree.cs:221-230:

else if (path.NumSphere > 1)
{
    if (RootNode.sphere_intersects_poly(localSphere_, movement, ref hitPoly, ref contactPoint) || hitPoly != null)
    {
        var collisionNormal = path.LocalSpacePos.LocalToGlobalVec(hitPoly.Plane.Normal);
        collisions.SetCollisionNormal(collisionNormal);
        return TransitionState.Collided;
    }
}

— an exact structural match to the raw decomp, confirming this is not a BN misdecompile.

acdream's corresponding code (BSPQuery.cs:2227-2264) does NOT do this. It applies the same SetCollide-and-defer (or steep→Slid) treatment to a sphere1 hit as it does to sphere0 — there is no branch that returns Collided with a direct SetCollisionNormal write for "foot clear, head hit" while airborne. This means: in acdream, an airborne mover whose HEAD sphere alone contacts a polygon (foot sphere clear) gets SetCollide + deferred Adjusted (no immediate CollisionNormal write) — exactly the same "the real normal gets lost until validate_transition's UnitZ default kicks in" symptom the digest already diagnosed for shape-1. A door push where the player's capsule brushes the door frame near chest/head height while the foot sphere tracks slightly differently (a very plausible geometry for "pushing a closed door face at a near-perpendicular angle," matching the tick-22760 description) is a strong candidate for exactly this code path.

Caveat, stated honestly: this is contingent on sphere0 (foot) not fully hitting while sphere1 (head) does — if the door's collision geometry is a full vertical plane, sphere0 would very likely hit too, and the code would never reach the sphere1 branch (BSPQuery.cs:2156's if (hit0 || hitPoly0 is not null) returns early). This has not been confirmed against the actual tick-22760 replay this pass — it is the single next concrete step (see §4).

2.4 Instrumentation to run (concrete, low-cost, no guessing required)

Extend DoorBugTrajectoryReplayTests.Diagnostic_Tick22760_DumpEngineInternals (tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs:162) to log, at the tick-22760 resolve, which of hit0/hitPoly0/hit1/ hitPoly1 were non-null/true inside BSPQuery.cs's Path-6 dispatch (a one-line Console.WriteLine gated behind the existing ProbeIndoorBspEnabled/ProbeBuildingEnabled diagnostics, or a new narrowly-scoped probe flag per the project's diagnostic-owner pattern).

Accept criterion: if hit0/hitPoly0 are both null/false and hit1/hitPoly1 fire, §2.3's hypothesis is confirmed — the fix is to port retail's direct sphere1-hit-without-sphere0-hit → Collided + SetCollisionNormal branch into BSPQuery.cs's Path 6 (mirroring the already-correct Path 5/Contact-branch treatment at BSPQuery.cs:2103-2140, which already handles the analogous grounded case correctly — this would be a narrow, well-precedented port, not a new design).

Reject criterion: if sphere0 hits too (hit0 or hitPoly0 truthy), this hypothesis is wrong for tick-22760 specifically, and the search should move to the other named-retail sibling not yet read this pass — BSPTREE::collide_with_pt's own internal structure for a non-PathClipped context is not reachable (its outer gate requires state&8), so the next candidate would be whatever governs CObjCell::find_obj_collisions's insertion order relative to find_env_collisions for a door's building channel (the BR-7/A6.P4 per-cell shadow architecture) — not yet examined this pass; would need a fresh read of that dispatch specifically for polygon ordering/precedence when multiple candidate polys are tested per cell.


3. #116 shape-2 — first-airborne-frame hard-stop vs in-frame slide (strong structural finding, INFERENCE, narrows but does not eliminate the need for a confirming run)

3.1 The dispatch structure resolves the ROUTING question without cdb (FACT, cross-referenced against 3 sources: raw BN pseudo-C, ACE, current acdream)

Both the raw retail decomp and ACE's BSPTree.cs (an independent, clean-language port — the "fastest oracle" the mission suggested) show the same two-tier gate, keyed on ObjectInfoState.Contact:

  • Already grounded (Contact set) + head-sphere hitslide_sphere called directly, in-line, same tick (ACE BSPTree.cs:192-202; retail pseudo-C region immediately following 0x0053a730's state&1 branch — the else arm at ~323838+, not fully re-quoted here but structurally mirrored by ACE's clean port). acdream's BSPQuery.cs Path 5 (:2103-2120) already matches this exactly — SlideSphere called directly for a grounded head-hit.
  • NOT yet grounded (Contact unset, i.e. airborne / first contact) + foot-sphere hit → the Path-6 default: SetCollide + WalkableAllowance=LandingZ + return Adjustedno repositioning, no slide_sphere call at all (ACE BSPTree.cs:210-219; retail pseudo-C:323815-323821). Only a sphere1(head)-hit-without- sphere0-hit gets an immediate response in this branch, and that response is Collided (§2.3), still not slide_sphere.

This means: for a genuine first-airborne-frame FOOT-sphere wall hit (the D4 fixture's actual shape — a mover falling into a tall wall), neither retail nor ACE's port calls slide_sphere on contact frame 1. The sphere is left exactly where it was (SetCollide does not reposition — confirmed in §1.2 Step A), Collide gets set, and the very next retry attempt (same tick, same TransitionalInsert loop, per §1.2 Step B) routes to Path 4 (find_walkable) instead. For a tall, vertical wall (D4's actual geometry — "TallWall" per the test name), find_walkable's nearby-walkable-surface search would very plausibly find no candidate (a sheer vertical face has no near-horizontal polygon to "land" on nearby) — changed=false — so Path 4 returns OK (ACE BSPTree.cs:185-186, else return TransitionState.OK;). TransitionalInsert's Phase 3 (if (sp.Collide), now finally reachable since objState==OK) then runs: ContactPlaneValid is false (Path 4's changed=false arm never sets it), so the else reset=true; branch fires (TransitionTypes.cs:1842-1843), RestoreCheckPos() reverts to the pre-hit position, and the retail-faithful gate at TransitionTypes.cs:1863-1898 (matching pseudo-C:273231-273239 exactly, already cited in-code) fires: since this is the first airborne contact, LastKnownContactPlaneValid is false, so SetCollisionNormal(sp.StepUpNormal) (the wall's real normal, captured back at the original Path-6 hit) runs and the function returns Collided — a hard stop, in place, with the correct wall normal recorded — not a slide.

3.2 What this means for D4

INFERENCE, well-supported but not independently re-run this pass: the D4 pin's original expectation (frame 1 hard-stops at Z=2.0, the slide begins frame 2 off the cached sliding normal) is structurally much closer to what retail's own dispatch produces for a true vertical-wall first-contact than the engine's current in-frame slide-to-Z=1.92 behavior. This narrows — but does not eliminate — the open question. What remains genuinely unconfirmed by static reading (and is exactly the class of question DO-NOT-RETRY item 8 warns against guessing):

  • Whether find_walkable's internal walkable-search radius/height actually returns "nothing found" for the specific D4 fixture geometry (a wall tall enough that no nearby floor exists within its search envelope) — this is a testable, non-cdb question: instrument or step through FindWalkableInternal for the D4 geometry and confirm changed=false.
  • The exact x87 comparison polarities inside slide_sphere and find_walkable themselves (unrelated to this pass's routing finding) remain unconfirmed per DO-NOT-RETRY item 8 — but those don't matter for D4 if slide_sphere is never reached on frame 1 in the first place.
  1. First (cheap, no cdb): run/instrument the existing BSPStepUpTests.D4_AirborneMover_TallWall_PersistsSlidingNormalAcrossFrames fixture (currently Skip-tagged citing #116) with a probe on which BSPQuery.cs path fires on frame 1 (Path 6 vs Path 4 vs a full-hit- the-second-attempt path) and whether FindWalkableInternal returns changed=true or false for that specific wall. Accept: if Path 6 fires (SetCollide+Adjusted, no reposition), Path 4 then fires with changed=false, and the final result is Collided with StepUpNormal as the recorded normal — this confirms §3.1/§3.2, and the fix is to flip the D4 pin back to hard-stop (retire the Skip, assert Z=2.0 frame 1) rather than changing the engine. Reject: if the trace shows something else (e.g. Path 4 actually finds a walkable candidate for this wall, or a different dispatch arm fires entirely) — then the routing hypothesis in §3.1 doesn't hold for this specific fixture geometry, and a live cdb trace becomes necessary after all.
  2. Only if step 1 disagrees with the FACT-cited dispatch structure: a live cdb trace of an actual airborne wall hit in retail, per the CLAUDE.md "Retail debugger toolchain" section. Concrete script outline (adapting the documented pattern):
    .logopen ts4-116-airborne-wallhit.log
    .sympath C:\Users\erikn\source\repos\acdream\refs
    .symopt+ 0x40
    .reload /f acclient.exe
    
    r $t0 = 0
    bp acclient!BSPTREE::find_collisions "r $t0 = @$t0 + 1; .if (@$t0 % 1 == 0) { .printf \"hit %d: state=%%d collide=%%d\\n\", @$t0 } gc"
    bp acclient!CSphere::slide_sphere "r $t1 = @$t1 + 1; .printf \"SLIDE_SPHERE HIT #%d\\n\", @$t1; .if (@$t1 >= 3) { qd } .else { gc }"
    bp acclient!BSPTREE::collide_with_pt "r $t2 = @$t2 + 1; .printf \"COLLIDE_WITH_PT HIT #%d\\n\", @$t2; gc"
    g
    
    User reproduces: jump toward a tall vertical wall so the FIRST wall contact happens while airborne (not already grounded). The key signal is whether slide_sphere fires on the same engine tick as the first find_collisions hit against that wall (in-frame slide, confirming the CURRENT engine behavior) or only on a later tick (confirming the hard-stop-then-slide-frame-2 pin). Auto-detaches via qd after 3 slide_sphere hits to bound game lag.

  1. [Lowest risk, do first] Fix the TransitionalInsert exhausted-loop hardcoded return (§1.4): change return TransitionState.Slid; to return the real last transitState value, matching ACE/retail. Blast radius: essentially zero — ValidateTransition treats Collided/Adjusted/Slid identically downstream (confirmed §1.2 Step D), so this is a pure code-correctness fix with no observable behavior change in any currently-passing test. Good precursor because it removes a misleading comment/return before anyone traces this loop again.

  2. [Cheap, decides whether TS-4 needs anything further] Re-run Ts4SteepRoofWedgeCaptureTests with a small horizontal velocity component (e.g. vx = 0.3 m/s alongside the existing straight-down fall), shortcut removed. Per §1.3's hypothesis, this should not wedge (the crease projection produces a non-zero tangential offset). Accept (doesn't wedge): TS-4's shortcut removal is safe for the realistic case; land it, retire the TS-4 register row, and either (a) accept the pure-vertical case as a documented, retail-faithful corner case (cite §1.2/§1.3 in the register row) or (b) if the team wants zero residual risk, also file a narrow follow-up for the zero-horizontal-velocity degenerate specifically (not a TS-4 blocker). Reject (still wedges even with horizontal velocity): §1.3's hypothesis is wrong or incomplete; do NOT land TS-4 yet — re-open with a fresh capture of the actual velocity vector at the wedge point and compare against what AdjustOffset computes step by step (a ACDREAM_DUMP_EDGE_SLIDE-style trace of AdjustOffset's intermediate slidingAngle/collisionAngle/slideOffset values, not yet instrumented, would be the concrete next apparatus).

  3. [Independent of 1-2] #116 shape-1 instrumentation (§2.4): add the one-line hit0/hitPoly0/hit1/hitPoly1 probe to Diagnostic_Tick22760_DumpEngineInternals and re-run. Blast radius: zero (diagnostic-only). If confirmed, the fix (porting retail's direct sphere1-hit → Collided+SetCollisionNormal branch into Path 6) is a narrow, well-precedented addition mirroring the already-correct Path 5 treatment — moderate blast radius (touches the shared Path-6 dispatch used by every airborne two-sphere mover), needs the existing SphereCollisionFamilyTests/Issue137* suites re-run plus a fresh tick-22760 comparison before landing.

  4. [Independent of 1-3] #116 shape-2 instrumentation (§3.3 step 1): add the BSPQuery-path + FindWalkableInternal changed probe to the D4 fixture. Blast radius: zero (diagnostic-only) for the instrumentation itself. If confirmed, flipping the D4 pin (un-skip, assert hard-stop frame 1) is a test-only change with zero production code change — the engine's current dispatch already produces this result per §3.1's reading; only the test's own expectation is currently wrong. This is the lowest-risk of all four items once confirmed, because it requires touching zero engine code.

Suggested order given the above: 1 → 4 → 3 → 2, since 4 (#116 shape-2) is the cheapest to fully resolve (test-only fix, zero engine change, per this pass's structural finding) and 2 (TS-4's own confirming run) benefits from having item 1's return-value fix landed first (removes a confusing false signal before re-tracing).


5. What genuinely still needs cdb or Ghidra (not resolved by this pass)

  1. #116 shape-2, only if §3.3 step 1's confirming run disagrees with the FACT-cited dispatch structure. The routing question itself (does frame 1 reach slide_sphere) is resolved by static reading against 3 independent sources in this pass; only a surprising, contradicting instrumentation result would re-open the need for a live trace. The cdb script outline is in §3.3 step 2.
  2. The x87 comparison polarities inside slide_sphere, find_walkable, and AdjustOffset's own internal branches (DO-NOT-RETRY item 8) — untouched by this pass, remain Ghidra/cdb- gated as before. This pass's findings are about which function gets called (dispatch routing), not the exact comparison operators inside those functions.
  3. AP-7's cos(10°) vs 0.99999536f discrepancy (P2 doc §1) — unrelated to this pass, still needs a Ghidra decompile of 0050ee70 when Ghidra MCP is back up.
  4. TS-1 gaps #2/#3's last_known_contact_plane maintenance and Path-4 LandingZ acceptance audit (P2 doc §2, §6 Step 2) — per the current source read in this pass, this already carries an in-code citation ("TS-1 gap #3 (register AD-54, Campaign P Slice P2 2026-07-30)") suggesting it was addressed in the same implementation session that produced the P2 doc's item-6 update; not independently re-verified this pass.

6. One-paragraph summary for the calling agent

TS-4: the Adjusted↔retry loop the P2 doc's implementation attempt found is real, but its root cause is one layer downstream of where that attempt looked. Path 6 sets Collide=true without moving the sphere; every subsequent attempt (same tick and later ticks, since Collide is never cleared outside Phase 3) routes to Path 4 (FindWalkableInternal), which is what actually establishes the steep ContactPlane (matching the fixture's observed InContact=true, OnWalkable=false). The freeze itself happens one tick later, inside AdjustOffset: validate_transition's retail-faithful CollisionNormal → UnitZ default feeds SetSlidingNormal, and AdjustOffset's crease-projection (Cross(ContactPlane.Normal, SlidingNormal)) is mathematically orthogonal to a purely vertical input offset — every subsequent tick's gravity-only offset gets crushed to zero and abort-small-offsets before the engine can run again. This exact mechanism is present identically in the raw retail decomp, ACE's port, and acdream's current port — it is very likely not a code bug but a narrow degenerate case that a live player's residual horizontal velocity (present in the original validating debugger trace) would not trigger. The concrete next step is cheap and decisive: re-run Ts4SteepRoofWedgeCaptureTests with a small horizontal velocity component before deciding whether TS-4's shortcut removal needs anything beyond the register-row writeup.

Addendum (P-review byte decode, 2026-07-30): AD-55 RESOLVED — retail's sled flatness test is cos(10°), ACE's constant is a radians/degrees bug

Raw bytes of CPhysicsObj::calc_friction @ 0x0050ee70 (PDB-paired binary, technique reference_pe_byte_decode), Sledding fast-sled branch at 0x0050ef52-0x0050ef6a:

d9 86 38 01 00 00   fld  dword [esi+0x138]    ; contact_plane.Normal.Z
dd 05 28 6b 7c 00   fld  qword [0x007c6b28]   ; = 0.17453292519943295 (10 deg in RADIANS)
d9 ff               fcos                       ; st0 = cos(10 deg) = 0.984807753
de d9               fcompp
df e0 / f6 c4 41 / 7a  fnstsw; test ah,0x41; jp

FACT: retail genuinely computes cos(10°) ≈ 0.9848078 at runtime and compares Normal.Z against it. ACE's 0.99999536f equals cos(0.1745 DEGREES) — the radian literal evaluated in degree mode; a proven ACE porting error, not a BN artifact. Sibling constants byte-confirmed: threshold float 0.25 @0x007c6b00, doubles 6.25/1.5625 @0x007c6b30/38, friction overrides 1.0f/0.2f as immediates.

Feel impact: retail's 0.2-friction fast-sled override engages on ground within 10° of flat; the shipped ACE-derived constant engages only within 0.17° (never, in practice) — part of the #166 sled family. FIX (queue for the TS-4/#116 implementation slice, which owns PhysicsBody): replace 0.99999536f with 0.98480775f (cos 10°), cite this addendum, retire register row AD-55 in the same commit.