acdream/docs/research/2026-07-30-response-layer-edge-family-pseudocode.md

66 KiB
Raw Blame History

P2 — Collision response-layer edge family: port-ready pseudocode

Status: RESEARCH PASS COMPLETE (2026-07-30); RETAIL RESPONSE ORDER PORTED (2026-07-31). Originally a research-only doc for Campaign P Slice P2 (docs/plans/2026-07-29-physics-parity-campaign.md §P2); a same-day implementation session landed TS-1's retirement and AP-7's fix, attempted TS-4 per this doc's own §6 Step 3 fixture-first order, reproduced the historical wedge, and stopped — see §7 item 6 for the full capture and root-cause diagnosis. TS-4 is NOT retired; its shortcut stays in place. #166 got a reattribution note in ISSUES.md rather than new code (per §3). #116 remains untouched (oracle-first, out of implementation scope). Headline findings that change the plan's assumptions: TS-1 was already substantially ported (the register row and plan phrasing were stale — see §2); the one real gap needed no code change (acdream's unified world-space SpherePath design makes retail's per-cell recache a no-op correction here — see §2 and the TS-1 register row's retirement text); #166 is very likely NOT about a literal PhysicsState.Sledding auto-toggle at all (see §3); AP-7's L.3c regression does not reproduce on the production graphical root-motion path post-R6, and now ports retail's confirmed 0.25f threshold (see §1); TS-4's shortcut removal is coupled to TS-1's completion and reproduces a wedge even after TS-1 lands — see the §7 item 6 update for the precise mechanism (Phase 3 of TransitionalInsert is structurally unreachable from Path 6's unconditional SetCollide, which returns Adjusted without repositioning the sphere) and what a future attempt needs to check first; #116 remains a genuine oracle-first research item needing live cdb/Ghidra, not an implementation item (see §5). Read §6 (port order) before starting further implementation on this family. Campaign P Slice 1B subsequently performed that fresh transitional_insert read and removed AP-3, AP-4, AD-53, and AD-54; the exact closeout and controls are in §8. TS-4 remains deliberately unchanged.

Every claim below is tagged FACT (grep/read-verified against the named-retail decomp, the register, ISSUES.md, or current acdream source in THIS worktree at the time of writing) or INFERENCE (reasoned but not yet decomp/cdb/Ghidra-confirmed — do not port on an INFERENCE alone without the flagged follow-up).


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

memory/project_physics_collision_digest.md, 3-day-old snapshot — re-verify line numbers before the implementation session)

These bind the P2 implementer. Do not re-attempt any of these shapes.

  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. (#137 mechanism-2 lesson; directly governs TS-4 item 4 below.)
  2. Do NOT re-add a forced constant-shell de-penetration to the sphere/cyl response. Retail slides tangentially (crease = collisionNormal × contactPlane.Normal) and never force-separates.
  3. SphereCollision no longer calls SetSlidingNormal (TS-45 retired) — the only in-transition sliding-normal writer is validate_transition. Keep it that way.
  4. Do NOT patch the degenerate-offset guard in slide_sphere ad hoc for #116 — the issue explicitly wants an oracle-driven pass, not a symptom patch. (#116 DO-NOT-RETRY, both the digest and ISSUES #116.)
  5. Do NOT re-introduce a topology-based outside-add / radial sweep to cell membership while touching this family — unrelated layer, but the digest's adjacent #98/#116 sessions warn subagents drift there.
  6. calc_friction threshold is retail 0.25 vs acdream 0.0 — this is AP-7. The L.3c attempt (naive bump to 0.25, no state gate) regressed normal walking 3 → 0.16 m/s and was reverted. Do NOT repeat a bare threshold bump without decoding the state gate first.
  7. Shape-1 of #116 (tick-22760 lateral-slide loss) is NOT the degenerate-offset guard threshold — that guard kills slides under ~1.4 cm; the lost slide was 3.57 cm, well above it. The real divergence is the collision-normal SOURCE (recording layer), not slide/validate. Do not re-chase the guard threshold for shape-1.
  8. Do NOT guess the BN test ah,5 x87 branch polarity/squaring in slide_sphere — this exact construct is called out as undecodable from BN alone (the PosHitsSphere-saga warning). Ghidra MCP settled it once already (2026-06-12) for the EPSILON-vs-EpsilonSq bug; Ghidra MCP is DOWN for this research pass — mark any residual x87-ambiguous claim Ghidra-verify, cite ACE as the fallback tiebreaker, do not silently guess.
  9. SUPERSEDED 2026-07-31 by Campaign P Slice 1B. AP-4's CliffSlide-first compensation was removed only after the complete retail transitional_insert/edge_slide order was read and branch-order plus graph/flat multi-frame roof/ledge controls passed. Do not reintroduce the compensation; see §8 and the retired AP-4 row.
  10. TS-46 (two-scalar sphere reconstruction) is OUT OF SCOPE for P2 (it's P3) but shares files (TransitionTypes.cs InitPath) — do not fold TS-46 sphere-list work into a P2 commit.

1. AP-7 — friction state gate

The function (FACT — named-retail grep-first)

CPhysicsObj::calc_friction is named at pseudo-C:276694 (address 0050ee70), called from UpdatePhysicsInternal-equivalent at pseudo-C:278490 (0050f0a… region, CPhysicsObj::calc_friction(this, arg2, var_28) where arg2=dt/quantum, var_28=velocity_mag2 — matches acdream's calc_friction(float dt, float velocityMag2) signature already).

Full structure read from pseudo-C:276694-276822 (FACT, direct read, not Ghidra):

void CPhysicsObj::calc_friction(dt, velocityMag2) {
    if ((transient_state & 2) != 0) {              // OnWalkable (see below)
        if ((state & MASK) == 0) {                  // MASK: BN-garbled string constant, see below
            // ---- Branch A ----
            dot = dot(contact_plane.N, velocity);    // order N·v
            if (!p_5)                                // p_5 from (dot < 0.25f) comparison, x87-ambiguous
                velocity -= dot * contact_plane.N;
                friction = this->friction;            // read field (decompiler shows bare read;
                                                        // almost certainly `1.0f - this->friction`
                                                        // collapsed/elided — see ACE cross-check)
              label_50f00e:
                velocity *= pow(friction, dt);         // shared tail with Branch B
        } else {
            // ---- Branch B ----
            dot = dot(velocity, contact_plane.N);       // same value, operand order swapped
            if (!p_1) {                                  // p_1 from (dot < 0.25f), x87-ambiguous
                friction = 0.2f;                          // LOCAL default inside this branch
                velocity -= dot * contact_plane.N;
                p_2 = (velocityMag2 ⋛ 1.5625f);            // x87-ambiguous direction
                if (p_2) {
                    p_3 = (velocityMag2 ⋛ 6.25f);          // x87-ambiguous direction
                    if (p_3)
                        p_4 = (cos(10°=0.17453292519943295 rad) ⋛ contact_plane.N.z);  // x87-ambiguous
                    if (!p_3 || !p_4)
                        friction = this->friction;          // fall back to object's own friction
                }
                goto label_50f00e;
            }
        }
    }
}

Constants confirmed FACT by direct read: 0.25f (both branches, independently re-derived — not copy-paste, two separate x87 loads), 0.2f, 1.5625f, 6.25f, 0.17453292519943295 (=10° in radians, fed to __fcos). The outer gate bit is transient_state & 2.

Cross-check: ACE PhysicsObj.calc_friction (FACT, references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs:2120-2141)

public void calc_friction(double quantum, float velocity_mag2)
{
    if (!TransientState.HasFlag(TransientStateFlags.OnWalkable)) return;

    var angle = Vector3.Dot(Velocity, ContactPlane.Normal);
    if (angle >= 0.25f) return;

    Velocity -= ContactPlane.Normal * angle;

    var friction = Friction;                    // this->friction — SAME baseline in ALL cases
    if (State.HasFlag(PhysicsState.Sledding))
    {
        if (velocity_mag2 < 1.5625f)
            friction = 1.0f;
        else if (velocity_mag2 >= 6.25f && ContactPlane.Normal.Z > 0.99999536f)
            friction = 0.2f;
    }

    var scalar = (float)Math.Pow(1.0f - friction, quantum);
    Velocity *= scalar;
}

TransientStateFlags.OnWalkable = 0x2 (FACT, references/ACE/Source/ACE.Server/Physics/PhysicsEngine.cs:11) — matches the decomp's transient_state & 2 outer gate exactly, and matches acdream's own TransientStateFlags.OnWalkable bit already.

This resolves the "state gate" mystery differently than the register's current framing. ACE shows ONE linear function, not two branches — the state/MASK test that BN rendered as two duplicated blocks is PhysicsState.Sledding (SLEDDING_PS = 0x800000, confirmed FACTacclient.h:2838 places it directly in the PhysicsState enum next to EDGE_SLIDE_PS = 0x400000, and references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs:2130 gates on exactly that flag with the exact same 1.5625/6.25/near-1.0 constants). The BN decompiler almost certainly duplicated a single if (state & SLEDDING_PS) { ... } block into what read as two near-mirror branches — this is a known BN artifact class (feedback_bn_decomp_field_names.md). ACE-derived reading, mark Ghidra-verify: I could not confirm from the raw pseudo-C alone why the branches read as fully separate blocks rather than one if; a live Ghidra decompile of 0050ee70 would settle whether the source truly had duplicated logic (possible if the original C++ had two near-identical inlined call sites) or whether this is purely a BN rendering artifact. Do not restructure the port around "two branches" — port ACE's single linear shape; it is structurally consistent with every constant the raw decomp independently confirms.

The threshold-direction ambiguity (p_5/p_1/p_2/p_3/p_4, all test ah,0x5-class x87 flag tests) is NOT independently Ghidra-verified this pass (Ghidra MCP is down). ACE's clean if (angle >= 0.25f) return; is adopted as the ACE-derived, Ghidra-verify reading for p_5/p_1 polarity. The p_2/p_3/p_4 velocity-magnitude-band and slope-flatness polarities are likewise ACE-derived, Ghidra-verify.

⚠️ Constant discrepancy found (FACT, needs Ghidra-verify to resolve): the raw decomp's slope test literally computes __fcos(0.17453292519943295) (= cos(10°) ≈ 0.984808) and compares it against contact_plane.N.z. ACE's port instead compares ContactPlane.Normal.Z > 0.99999536f directly — no cos() call, and 0.99999536 corresponds to an angle of only ≈0.175° from flat (acos(0.99999536) ≈ 0.175°), not 10°. These are physically very different tests (cos(10°) accepts any slope within 10° of flat; 0.99999536 accepts only essentially-perfectly-flat ground). Two hypotheses, neither confirmed: (a) BN misdecompiled a raw float-constant load as an __fcos() call (a known BN artifact class — spurious x87 opcode reinterpretation); (b) ACE's own decompile/port made an independent error and cos(10°)=0.984808 is correct. Do not silently pick one. File as an open Ghidra question (§7); when Ghidra MCP is back, decompile 0050ee70 directly and check whether the FCOS opcode is actually present at that instruction, or whether it's a raw FLD of 0.99999536 (or of 0.984808).

Why the L.3c naive threshold bump (0.0 → 0.25) hammered walking — and why that may no longer be true today (FACT + INFERENCE, high confidence, code-derived)

src/AcDream.Core/Physics/PhysicsBody.cs:576-602 is acdream's current calc_friction. Its threshold is 0.0 (if (dot >= 0f) return;), attributed to the OLDER, unnamed Ghidra decomp (FUN_0050f940 — a DIFFERENT address than the named function 0050ee70; the two decomp passes disagree, and per CLAUDE.md the named decomp wins). The named decomp's independently-confirmed 0.25f (both branches) is FACT-level confirmation that ACE's 0.25f reading — and the register's AP-7 row — are correct, and acdream's in-code comment ("we match the decompile" at 0.0) was matching the wrong (superseded, unnamed) decomp pass.

The recorded L.3c failure (2026-04-30): bumping the threshold alone to 0.25f, with NO other change, dropped measured forward locomotion from ~3 m/s to ~0.16 m/s (≈5.3% remaining) in PlayerMovementControllerTests. The math checks out exactly: Friction = DefaultFriction = 0.95f (confirmed FACT — both src/AcDream.Core/Physics/PhysicsBody.cs:120 and references/ACE/Source/ACE.Server/Physics/PhysicsGlobals.cs:15 agree on 0.95f, so this is NOT a divergent constant), and flat-ground walking has dot(velocity, groundNormal) ≈ 0 (velocity is ~horizontal, normal is ~vertical). With threshold 0.0, dot(≈0) >= 0 triggers the early return — friction NEVER engaged during flat walking. With threshold 0.25, dot(≈0) < 0.25 — friction ALWAYS engaged, decaying velocity by pow(1 0.95, dt) = 0.05^dt EVERY tick. At 60 Hz over 1 second: 0.951^60 ≈ 0.049 — a ~95% velocity loss in one second. That is the observed 3 → 0.16 m/s hammering almost exactly.

INFERENCE, code-derived (high confidence, not yet live-verified): the L.3c test predates the 2026-07-17 "local player animation-owned grounded movement" landing (R6). Reading current src/AcDream.Runtime/Gameplay/PlayerMovementController.cs:1742-1756:

if (_body.OnWalkable)
{
    float savedWorldVz = _body.Velocity.Z;
    if (hasAnimationRootMotion)
    {
        _body.Velocity = new Vector3(0f, 0f, savedWorldVz);   // <-- XY ZEROED
    }
    else
    {
        Vector3 stateVelocity = _motion.get_state_velocity();
        _body.set_local_velocity(
            new Vector3(stateVelocity.X, stateVelocity.Y, savedWorldVz),
            autonomous: _body.LastMoveWasAutonomous);
    }
}
...
_body.UpdatePhysicsInternal(tickDt);   // calls calc_friction internally

When the graphical client's animation root motion drives the walk (hasAnimationRootMotion == true, the production path since R6), _body.Velocity.X/Y are forced to zero immediately before calc_friction runs every tick — because walking displacement now comes from pmDelta.Origin (the animation Frame delta, applied directly to _body.Position at lines 1764-1766), not from integrating Velocity. Friction decaying an already-zero horizontal Velocity is a no-op. This means the L.3c hammering mechanism is very likely ARCHITECTURALLY MOOT for the production graphical local-player path today — the regression that blocked AP-7 in April may not reproduce post-R6.

The else branch (no animation root motion — the headless/test-controller path, _motion.get_state_velocity()) still feeds real XY speed into Velocity, so that path (used by Slice K headless bots and any test without an attached animation source) is still exposed to the same hammering risk a naive threshold-only port would reintroduce.

Action for the implementer, not yet executed by this research pass: before porting, re-run PlayerMovementControllerTests (or an equivalent fresh capture) with a 0.25f threshold and the corrected ACE-derived single-linear-function shape, checked separately against (a) the graphical/animated local-player path, (b) the headless/get_state_velocity path, and (c) remote/NPC movers (RuntimeRemotePhysicsUpdater.cs, RemoteMotion.cs — confirm whether their Velocity is root-motion-zeroed the same way, or whether they remain velocity-integrated and therefore friction-sensitive). Do not assume (a) is safe without a fresh capture — this section's confidence is code-derived, not measured.

AP-7 verdict

Port shape: replace acdream's two-threshold, dead-Sledding-branch calc_friction with ACE's single linear function (0.25f threshold, single friction = Friction baseline, PhysicsState.Sledding-gated override using the already-present 1.5625/6.25/near-flat constants — acdream already has these at PhysicsBody.cs:591-597, just unreachable because nothing ever sets the state bit; see §3). Flag the cos(10°) vs 0.99999536 discrepancy (Ghidra-verify) and pick ACE's 0.99999536f provisionally since acdream's own current dead code already uses it (least churn) — do not silently resolve the discrepancy by picking one without a citation in the eventual commit; carry the open question into the register row.

2. TS-1 — PrecipiceSlide / EdgeSlide / CliffSlide chain

⚠️ Major finding: the register/plan framing is STALE — TS-1 is already substantially ported (FACT, code-verified)

The register row (docs/architecture/retail-divergence-register.md:238) says: "PrecipiceSlide context missing — conservative stop-at-edge instead of retail's EdgeSlide → PrecipiceSlide / CliffSlide", citing TransitionTypes.cs:1254. Line 1254 today is unrelated stepping-loop code (the viewer last-step-remainder computation) — the file has moved substantially since that row was written. Reading the actual current implementation:

  • SpherePath.PrecipiceSlide(Transition)TransitionTypes.cs:943-970 — a real port of SPHEREPATH::precipice_slide (pseudo-C:274316, 0050cc80), calling BSPQuery.FindCrossedEdge (a real port of CPolygon::find_crossed_edge, pseudo-C:322909, 00539300BSPQuery.cs:438-482), sign-flipping via a dot product exactly like retail's x87_r7_17 sign test, then delegating to Transition.SlideSphereInternal exactly like retail's tail call to CSphere::slide_sphere.
  • Transition.CliffSlide(Plane)TransitionTypes.cs:2037-2102 — a real port of CTransition::cliff_slide (pseudo-C:272397, 0050a6d0): cross product of the two plane normals, Z-flattened, rotated 90° in the XY plane ((-Y, X, 0)), degenerate-check, sign-resolved offset application via SetCollisionNormal. Return-value mapping verified FACT-correct against acclient.h:6100-6108 (OK_TS=1, COLLIDED_TS=2, ADJUSTED_TS=3, SLID_TS=4): the degenerate case returns TransitionState.OK (matches retail's return 1; = OK_TS); the success case returns TransitionState.Adjusted (matches retail's return 3; = ADJUSTED_TS). This is correctly ported, not guessed.
  • Transition.EdgeSlideAfterStepDownFailedTransitionTypes.cs:1907-2035 — a dispatcher structurally mirroring CTransition::edge_slide (pseudo-C:273001-273090, 0050b3d0), including the AP-4-registered reordering (steep-contact-plane CliffSlide check moved before the !OnWalkable || !EdgeSlide bail, to compensate for acdream's OnWalkable bookkeeping — see DO-NOT-RETRY §0 item 9).

This means TS-1's row and the P2 plan's "port the EdgeSlide → PrecipiceSlide / CliffSlide chain" framing describe work that is largely DONE. The register row was not retired in the same commit that landed this — a process gap, not a code gap. Recommend re-verifying with a live capture before writing new code, not blindly re-porting from scratch.

Retail source, quoted in full (FACT, pseudo-C:273001-273090, 0050b3d0)

TransitionState CTransition::edge_slide(arg2/*out*/, arg3=step_down_height, arg4=zVal) {
    state = object_info.state
    if ((state & 2) == 0 || (state.byte[1] & 2) == 0) {       // NOT walkable-capable / NOT EdgeSlide-capable
        walkable = null; check_pos = backup_check_pos; check_cell = backup_cell;
        contact_plane_valid = 0; contact_plane_is_water = 0;
        *arg2 = OK_TS; return cache_global_sphere(null);
    }
    if (contact_plane_valid) {
        p_1 = (contact_plane.N.z - arg4) ⋛ 0            // x87-ambiguous, ACE-derived: "N.z >= zVal"
        if (!p_1) {                                       // contact steeper than allowed (zVal = FloorZ or LandingZ)
            walkable = null; restore_check_pos();
            *arg2 = cliff_slide(this, &contact_plane);
            contact_plane_valid = 0; contact_plane_is_water = 0;
            return 0;
        }
        // else: falls through (contact IS walkable-steep-enough)
    }
    if (walkable != null) {
        restore_check_pos();
        contact_plane_valid = 0; contact_plane_is_water = 0;
        result = precipice_slide(sphere_path, collision_info);
        *arg2 = result;
        return (result == COLLIDED_TS);
    }
    if (contact_plane_valid) {                            // walkable was null, contact fell through as OK
        walkable = null; restore_check_pos(); cell_array_valid = 1;
        contact_plane_valid = 0; contact_plane_is_water = 0;
        *arg2 = OK_TS; return ...;
    }
    // ---- back-probe fallback: neither contact nor walkable available ----
    offset = global_curr_center - global_sphere.center;    // back toward where we came from
    add_offset_to_check_pos(offset);
    step_down(this, arg3, arg4);
    contact_plane_valid = 0; contact_plane_is_water = 0;
    restore_check_pos();
    if (walkable == 0) {
        walkable = null; *arg2 = COLLIDED_TS; cell_array_valid = 1; return ...;
    }
    contact_plane_valid = 0; contact_plane_is_water = 0;
    walkable_scale = sphere_path.walkable_scale;
    cache_localspace_sphere(get_walkable_pos(sphere_path), walkable_scale);   // <-- NOT PRESENT IN ACDREAM
    set_walkable_check_pos(sphere_path, localspace_sphere);                  // <-- NOT PRESENT IN ACDREAM
    result = precipice_slide(sphere_path, collision_info);
    *arg2 = result;
    return (result == COLLIDED_TS);
}

Precise, evidenced gap #1: the back-probe fallback path skips retail's localspace re-cache before its second precipice_slide call (FACT)

Retail's edge_slide has two distinct precipice_slide call sites: one direct (when sphere_path.walkable != 0 already), one in the back-probe fallback (when NEITHER a valid contact plane NOR a walkable polygon survived) — and ONLY the second site performs walkable_scale/cache_localspace_sphere/get_walkable_pos/ set_walkable_check_pos first (pseudo-C:274318-274326, 0050b4e0-0050b507).

acdream's SpherePath has no WalkableScale, LocalspaceSphere, GetWalkablePos, or SetWalkableCheckPos member/method anywhere (confirmed by grep across TransitionTypes.cs — zero hits), and its single unified PrecipiceSlide(Transition) method (TransitionTypes.cs:943-970) is called identically from BOTH the direct branch3 case (TransitionTypes.cs:2002, matches retail's first call site correctly — no re-cache needed there either) AND the back-probe fallback (TransitionTypes.cs:2031, should match retail's second call site but is missing the re-cache step retail performs first).

Port-ready shape (INFERENCE for the exact field semantics — the walkable_scale/localspace-sphere machinery itself needs a fresh read of SPHEREPATH::get_walkable_pos/cache_localspace_sphere/ set_walkable_check_pos, not yet read this pass; FACT that the gap exists, INFERENCE on the fix shape): add the three missing SpherePath members/methods, call them in EdgeSlideAfterStepDownFailed's final fallback block (TransitionTypes.cs:2015-2031) immediately before the sp.PrecipiceSlide(this) call at line 2031, matching retail's ordering. get_walkable_pos/cache_localspace_sphere/set_walkable_check_pos were not read this pass (budget) — read them fresh before porting (pseudo-C, search near SPHEREPATH::get_walkable_pos — a hit already appears in the edge_slide grep at the top of this section's raw quote, so the symbol exists and is findable).

Precise, evidenced gap #2 (likely unregistered adaptation, not a bug per se): CliffSlide's reference-normal fallback chain is an acdream invention (FACT: not in the retail read; adaptation reasoning IS documented in-code)

Retail's cliff_slide (pseudo-C:272397) uses this->collision_info.last_known_contact_plane.N directly, with no fallback chain as the second cross-product operand. acdream's CliffSlide (TransitionTypes.cs:2037-2070) instead tries THREE sources in priority order: LastWalkablePlane (if Normal.Z >= FloorZ), then LastKnownContactPlane (same threshold), then Vector3.UnitZ world-up. The in-code comment ("L.4-cliffslide-fallback", dated 2026-04-30) explains the reasoning (degenerate cross-product when the player has been on a continuous steep slope for >1 frame and LastKnownContactPlane itself became steep) but this reasoning does not appear to have a corresponding register row (checked AP-4, TS-1 — neither mentions the fallback chain specifically; AP-4 is about re-ordering the CliffSlide-vs-Branch1 check, a different concern). Flag for the implementer: either (a) find this exact prioritization in a further retail read (unlikely given the raw decomp's directness, but not yet exhaustively ruled out — last_known_contact_plane itself might be retail-maintained differently than acdream's equivalent field, which could make the fallback chain compensate for an upstream divergence rather than being a pure invention), or (b) register it explicitly as an AD/AP row with this citation before or alongside the P2 commit. Do not silently leave an unregistered behavioral invention in place while "retiring" the TS-1 row — that violates the register's same-commit rule.

Precise, evidenced gap #3 (likely unregistered adaptation): the walkable-polygon steepness reroute in EdgeSlideAfterStepDownFailed

TransitionTypes.cs:1972-1997 — when sp.HasWalkablePolygon is true, acdream additionally checks sp.WalkablePlane.Normal.Z < FloorZ and, if so, reroutes to CliffSlide(sp.WalkablePlane) INSTEAD of calling PrecipiceSlide. Retail's raw edge_slide (if (walkable != null) { ... precipice_slide(...) }) has no steepness branch on the walkable polygon itself — it always calls precipice_slide once walkable != null. The in-code comment ("L.4-walkable-steep") argues this compensates for acdream's Path-4 airborne-landing branch accepting steep roofs as "walkable" under the permissive LandingZ threshold (a claim this research pass did not independently verify — Path-4 was not read this session; TS-4 §4 below covers the adjacent but distinct Path-6 concern). Flag for the implementer: same as gap #2 — verify whether this reroute is compensating for a real upstream divergence (in which case it should be an AD/AP row) or is masking a bug that should be fixed at its source (Path-4's LandingZ acceptance) instead of patched here. Do not retire TS-1 while leaving this unregistered.

TS-1 verdict

Port shape: narrow, not a rewrite. (1) Add the retail walkable_scale/cache_localspace_sphere/get_walkable_pos/ set_walkable_check_pos step to the back-probe fallback path only (gap #1 — a real, missing piece). (2) Audit gaps #2 and #3 against a fresh, focused retail re-read of last_known_contact_plane maintenance and the Path-4 landing-acceptance threshold; register whichever holds up as AD/AP rows, or align to retail exactly if the compensation turns out unnecessary. (3) Only THEN retire the TS-1 register row, in the same commit, updating its citation (the current :1254 citation is already stale and should point at EdgeSlideAfterStepDownFailed/CliffSlide/ PrecipiceSlide instead). Do not re-derive edge_slide/cliff_slide/ precipice_slide from scratch — the existing port is real and largely correct; re-porting risks discarding correct, already-tested work (the CLAUDE.md worldbuilder-inventory lesson applies here by analogy: don't re-port what's already ported and tested).

3. #166 — landing sled (Sledding state set/clear)

Finding: PhysicsState.Sledding appears to be DATA-AUTHORED, not an automatic landing response (FACT, cross-referenced across 3 independent repos)

Searched for a SET (|= SLEDDING_PS) or automatic-toggle call site for PhysicsState.Sledding in: the named-retail pseudo-C (no string "sled" anywhere in the 1.4M-line file — grep -in sled returns zero hits; the raw 0x800000 hex literal also returns zero physics-related hits, only unrelated Watson/crash-dump flags), references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs (full file, only 3 hits: the two calc_friction/UpdateObjectInternal READ sites already covered, no WRITE site), and references/ACViewer/ACE/... (same ACE-derived code, same result).

The only WRITE sites for PhysicsState.Sledding anywhere in any reference repo are:

// references/ACE/Source/ACE.Server/WorldObjects/WorldObject_Properties.cs:1105-1109
public bool? Sledding
{
    get => GetPhysicsState(PhysicsState.Sledding);
    set => SetPhysicsState(PhysicsState.Sledding, value);
}

— a per-weenie boolean game-data property (the same pattern as Ethereal, Static, etc.), packed onto the broadcast PhysicsState in WorldObject_Networking.cs:586-588,700-704. This is server/database-set, not a client-side automatic landing-response toggle. Since CPhysicsObj is shared code between the retail client and server binaries (the same class that produced calc_friction, UpdateObjectInternal, etc. — all independently confirmed to structurally match ACE's port), the total ABSENCE of a write site anywhere in ACE's ~10,000-line PhysicsObj.cs is strong evidence that retail's own CPhysicsObj does not automatically enter Sledding state on a downhill landing either.

INFERENCE (well-supported, not yet cdb/Ghidra-confirmed): ordinary downhill-jump glide-and-bounce in retail is NOT the literal Sledding physics state at all for an ordinary player. Sledding is most likely reserved for specific data-authored world content (dungeon/event objects with the property baked into their weenie default PhysicsState, e.g. an actual in-world "sled ride" mechanic) — a narrow, data-driven case outside a generic movement port's scope. Grepping WCID/weenie class name lists in references/ACViewer/ACE/.../WeenieClassName.cs for "sled" found no obviously-named sled-ride weenies, but that catalog is not exhaustive for retail's original 2013 content and this was not chased further (out of scope — data content, not an algorithm).

Cross-check against ISSUES.md #166's OWN root-cause text (FACT)

docs/ISSUES.md:4254-4262 (filed 2026-07-03, i.e. BEFORE the P2 campaign plan's phrasing) already attributes #166 to a named composite of three rows, and explicitly does not mention a Sledding set/clear mechanism:

"This is the REGISTER-PREDICTED composite of three known deferred deviations: AD-25 (landing wall-bounce velocity reflection suppressed...), AP-7 (calc_friction threshold 0.0 without retail's 0.25-with-state-gate...), and TS-4 (Path-6 steep-poly slide-tangent shortcut...). Retiring those three rows IS this issue."

Cross-referencing the digest's #182 rebuild notes (memory/project_physics_collision_digest.md:837-841, 2026-07-07): AD-25's LOCAL-PLAYER landing-bounce reflection was already ported in the #182 verbatim UpdateObjectInternal/handle_all_collisions rebuild ("Contact committed BEFORE the reflect... retire AD-25's micro-bounce (AD-25 narrowed to the remote-DR sweep)"). What remains open for AD-25 is remote/NPC-only and is explicitly P3 scope (docs/plans/2026-07-29-physics-parity-campaign.md §P3 item 2), not P2.

#166 verdict: for the LOCAL PLAYER (the case the user actually reported — "jumping down a hill" on their own character), the bounce half (AD-25) is already shipped; what's missing is the glide-deceleration curve (AP-7, §1 above) and the airborne-steep landing chain (TS-4, §4 below). Porting AP-7 + TS-4 correctly should retire #166 for the local player WITHOUT inventing any client-side Sledding auto-toggle — inventing one would be exactly the kind of unargued behavior addition CLAUDE.md's no-guessing rule forbids (no decomp evidence supports it). The campaign plan's P2 item 3 phrasing ("port the landing sled (Sledding state set/clear sites)") appears to rest on an assumption not borne out by this research pass.

Recommendation for the implementer: do NOT build a Sledding auto-set/clear mechanism speculatively. Land AP-7 + TS-4 first, capture a fresh downhill-jump-landing trajectory (extend ACDREAM_CAPTURE_RESOLVE), and check the #166 visual-matrix item (row 5, "Downhill jump landing: sled glide + bounce") against that alone. If the glide/bounce still visibly mismatches retail after AP-7+TS-4 land, THAT capture — not a guess — is what should drive any further Sledding-state work, and it should go through cdb against live retail (a downhill jump landing, watching this->state for the 0x800000 bit) before any client auto-toggle is written. Keep the already-present dead PhysicsStateFlags.Sledding branch in calc_friction (§1) since it's cheap, decomp-consistent, and harmless if never entered — but do not manufacture a caller that sets it.

4. TS-4 — Path-6 steep-poly shortcut removal

The current shortcut (FACT, src/AcDream.Core/Physics/BSPQuery.cs:2149-2266)

Path-6 (the default sphere_intersects_poly → collide_with_pt / SetCollide dispatch) tests each hit polygon's world-space normal. For BOTH sphere0 (feet) and sphere1 (head), if worldNormal.Z < PhysicsGlobals.FloorZ (steeper than ~49° from horizontal), acdream takes a SPECIAL BRANCH: projects the move along the steep face, writes collisions.SetCollisionNormal(worldNormal) and collisions.SetSlidingNormal(worldNormal), and returns TransitionState.Slid immediately — bypassing SetCollide entirely for steep hits. Only the shallow case (worldNormal.Z >= FloorZ) reaches path.SetCollide(worldNormal); path.WalkableAllowance = LandingZ; return Adjusted;.

The in-code comment is unusually candid about why: "This is a deliberate deviation from retail... Validated against retail debugger trace 2026-04-30: retail body did not wedge; our retail-faithful port DID wedge because we're missing implementation details of the step_up_slide / cliff_slide chain on grounded-steep movement." — i.e., this shortcut was shipped SAME-DAY as (and BECAUSE) the retail-faithful EdgeSlideAfterStepDownFailed/CliffSlide/PrecipiceSlide chain (TS-1, §2 above — also dated 2026-04-30, tagged "L.4") still wedged in testing when tried without this shortcut.

Retail: NO steepness branch at the BSP layer (FACT, pseudo-C:323740-323783, 0053a730 region)

Read directly from the named decomp (the sphere_intersects_poly / set_collide dispatch inside BSPTREE::find_collisions's default path):

if (sphere_intersects_poly(...) || eax_26 != 0) {
    localtoglobalvec(sphere_path.localspace_pos, &saved_ebx, &poly->plane.N);
    SPHEREPATH::set_collide(&sphere_path, &saved_ebx);
    sphere_path.walkable_allowance = 0.0871556997f;   // = PhysicsGlobals.LandingZ, exact bit match
    return 3;   // ADJUSTED_TS
}

There is no steepness test here at all. Retail's BSP layer calls set_collide and returns ADJUSTED_TS unconditionally, for a steep roof exactly the same as a shallow ramp. walkable_allowance is always set to LandingZ (the permissive landing threshold) at this layer, regardless of the actual polygon slope. This directly confirms the P2 plan's description and the digest's #137-mechanism-2 finding (memory/project_physics_collision_digest.md:1008-1014): retail's BSP/sphere collision layer never writes collision_info.sliding_normal at all — only validate_transition (0x0050ac21/0x0050aa70) does, and only success-gated. The steepness differentiation (walkable vs. merely-in-contact, and whether a slide response is needed) happens STRICTLY DOWNSTREAM, in ValidateTransition's FloorZ OnWalkable test and — when that surface turns out too steep to be walkable — EdgeSlideAfterStepDownFailedCliffSlide/PrecipiceSlide (TS-1's domain, §2 above).

TS-4 port shape (FACT-grounded, mechanically simple)

Delete both if (worldNormal{0,1}.Z < PhysicsGlobals.FloorZ) { ... return TransitionState.Slid; } blocks (BSPQuery.cs:2200-2215 and :2240-2255) entirely. Both sphere0 and sphere1 hits should fall straight through to the existing path.SetCollide(worldNormal); path.WalkableAllowance = PhysicsGlobals.LandingZ; return TransitionState.Adjusted; — i.e., make Path-6 do EXACTLY what its own shallow branch already does, for every hit, matching retail's unconditional set_collide. This mechanically retires both SetSlidingNormal write sites (satisfying DO-NOT-RETRY §0 item 1 permanently — deleted, not just avoided) with no replacement logic needed at this layer.

⚠️ Port-order coupling with TS-1 (INFERENCE, but directly evidenced by the shortcut's own commit history)

This is the single most important sequencing fact in this whole document. The shortcut's comment proves TS-1's retail-faithful chain was ALREADY BUILT once (same day, same "L.4" slice) and STILL wedged — that is why the shortcut exists instead of the faithful chain. Simply deleting the shortcut today, without first confirming TS-1's gaps (§2: missing localspace re-cache in the back-probe fallback; the two unregistered adaptations) are closed, risks reintroducing the EXACT "stuck in falling animation on the roof" / "walks up steep roofs" wedge that motivated the shortcut in the first place.

Recommended order: (1) close TS-1 gap #1 (the missing walkable_scale/cache_localspace_sphere/set_walkable_check_pos step) first, on its own, with the TS-4 shortcut still in place as a safety net. (2) Capture a grounded-steep-slope trajectory (a roof or steep terrain walk, matching whatever repro the 2026-04-30 L.4 session used — check docs/research/ and git log around that date for the specific repro if it wasn't captured as a fixture) with the shortcut TEMPORARILY disabled behind a flag or in a scratch branch, and confirm no wedge. (3) Only once that capture is clean, delete the TS-4 shortcut for real, in the same commit that closes the TS-4 register row. (4) Re-run the P2 final-matrix items 4-6 (cliff/roof edge, downhill landing, shallow wall graze) plus a fresh regression sweep before calling TS-4 done — this is exactly the kind of change the digest's #137 sagas warn compounds subtly (a leaked SetSlidingNormal "absorbing wedge at empty space" was the recurring failure mode across three separate historical incidents in the digest, all triggered by a similar not-quite-faithful shortcut).

5. #116 — slide-response family oracle pass

This item is explicitly an oracle-first investigation, not a known fix (docs/ISSUES.md:8426 status: "OPEN (narrowed)"; digest memory/project_physics_collision_digest.md:1509: "OPEN — oracle-first investigation; NOT cell-set"). This section defines the SCOPE of the oracle pass precisely, per the mission's request, rather than proposing a fix — a fix without the live trace below would repeat the exact mistake the digest's DO-NOT-RETRY table already warns against (§0 items 4, 7, 8).

The two shapes (FACT, restated with citations — both already

independently verified against source by the 2026-06-12 Ghidra session)

Shape-1 — tick-22760 lateral-slide loss. Live retail: blocked southward push at a cottage door face, KEPT a tiny lateral slide (X 0.0357 m, collision_normal=(0,+1,0), the door face). acdream's harness hard-stops both components (collision_normal=(0,0,1), i.e. the UnitZ ground-fallback default). Ghidra-confirmed (memory/project_physics_collision_digest.md:1259-1275): TransitionTypes.cs:3701-3702's UnitZ default on invalid collision-normal is retail-faithful — retail's validate_transition (0x0050aa70) has the identical if (collision_normal_valid==0) set_collision_normal(UnitZ). The divergence is UPSTREAM of both slide and validate: at tick-22760, acdream's collision_normal_valid was FALSE where retail's was TRUE (retail HAD recorded the door-face normal). The slide guard threshold is exonerated — the 3.57 cm lost slide is ~18× above the ~1.4 cm degenerate-offset cutoff (F_EPSILON = 0.0002, compared against SQUARED magnitude — Ghidra-confirmed, see §0 item 8), so retail's own guard would have kept the slide too.

Shape-2 — D4 first-airborne-frame slide vs. hard-stop. Ghidra confirms CSphere::slide_sphere (0x00537440) applies its slide IN-FRAME (add_offset_to_check_pos → returns SLID_TS) — acdream's current in-frame slide to Z=1.92 on frame 1 (BSPStepUpTests.D4_AirborneMover_TallWall_PersistsSlidingNormalAcrossFrames, tests/AcDream.Core.Tests/Physics/BSPStepUpTests.cs:560-602, currently Skip-tagged citing #116) is likely faithful TO slide_sphere itself. What's unconfirmed is whether retail's first airborne wall contact frame REACHES slide_sphere at all, or whether an earlier stage (collide_with_environment's dispatch, or the absence of a last_known_contact_plane on the very first airborne frame) intercepts it with a hard stop before slide_sphere ever runs. The #116 threshold fix (EpsilonSqF_EPSILON, shipped bf18a543) did not move D4 — confirming the D4 offset is a real slide, not a near-degenerate one the threshold fix would have caught.

What the P2 oracle pass must determine (mission-specified scope)

  1. Shape-1's real root cause: where does acdream's collision-normal RECORDING diverge from retail's at tick-22760? Not slide_sphere, not validate_transition (both exonerated) — the recording path that feeds collision_info.collision_normal_valid/.collision_normal during the BSP/environment hit-test itself. The digest's own next-step (memory/project_physics_collision_digest.md:1274-1275) is unchanged by this research pass: instrument DoorBugTrajectoryReplayTests.Diagnostic_Tick22760_DumpEngineInternals (tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs:162) to trace exactly where, in the BSP hit-test chain feeding FindObjCollisionsInCell/BSPQuery, the door-face normal (0,+1,0) gets computed-and-discarded in acdream but retained in retail. Candidate governing retail functions (need a fresh, focused read — not done this pass, budget): BSPTREE::find_collisions (the same dispatch region read for TS-4 in §4, pseudo-C ~323700-323830) and whatever populates collision_info.collision_normal/ .contact_plane on a BLOCKED (not slid, not adjusted) door-face hit specifically — this is a DIFFERENT code path than either Path-6's set_collide (which returns ADJUSTED_TS, not COLLIDED_TS) or slide_sphere (called only after a walkable/precipice context exists). A blocked door push is most likely dispatched through CollideWithPt/collide_with_pt (the PathClipped branch already visible in Path-6, §4, BSPQuery.cs:2158-2163) or a sibling Path-1-class function not yet read this pass.
  2. Shape-2's real answer: does retail's FIRST airborne wall-contact frame reach slide_sphere, or hard-stop upstream? This needs either a Ghidra decompile of the caller chain immediately above slide_sphere (checking whether a last_known_contact_plane existence gate exists before the call on frame 1 of an airborne trajectory) or a live cdb trace of an actual airborne wall hit in retail (per the digest's already-written cdb plan, memory/project_physics_collision_digest.md:8537-8543 / ISSUES.md #116). Ghidra MCP is down this pass — do not guess this one; it directly controls which of D4's two competing expectations (Z=1.92 in-frame slide vs. Z=2.0 hard-stop-then-slide-frame-2) is correct, and a wrong guess "regresses ALL wall-slide behavior" per the digest's own warning (§0 item 8).
  3. Governing retail functions to anchor the oracle pass (FACT, already cited in ISSUES #116 and this document's own reads): CSphere::slide_sphere (0x00537440, pseudo-C:321403-321532 per ISSUES #116's own citation — not re-read this pass, already Ghidra-verified for the epsilon fix), validate_transition (0x0050aa70, read indirectly via its UnitZ default confirmed in §2/§4's cross-references), and the find_collisions/environment hit-test recording sites this pass identified as the likely Shape-1 location (BSPTREE::find_collisions region, pseudo-C ~323700-323830, the same region TS-4 §4 already partially read for the unconditional set_collide call — a focused re-read of the SIBLING branches in that same dispatch, specifically the PathClipped/collide_with_pt arm and whatever arm produces a hard COLLIDED_TS with a recorded normal, is the concrete next research step).

#116 verdict — this is NOT a P2 implementation item, it is a P2

research item with its own follow-up research session

Given the depth already logged in the digest and ISSUES.md (an entire Ghidra session, a threshold fix already shipped, two shapes precisely characterized, and an explicit "needs a LIVE cdb session" conclusion reached independently by that prior work), this research pass concurs with the existing plan of record: #116 needs (a) an instrumented replay of DoorBugTrajectoryReplayTests for shape-1, and (b) either a Ghidra decompile of slide_sphere's caller chain or a live cdb trace of an airborne wall hit for shape-2, BEFORE any code changes. Do not patch the degenerate-offset guard, the UnitZ default, or slide_sphere itself speculatively (DO-NOT-RETRY §0 items 4, 7, 8 all apply directly). This document does not add new pseudocode for #116 beyond what's already recorded, because doing so without the trace would be exactly the kind of guess CLAUDE.md's workflow forbids.

6. Port order + blast radius

Recommended sequence, reasoning, and required fixtures/captures BEFORE each step's behavior change — not a generic "do them in plan order" list, because this research pass found real coupling the plan's slice ordering doesn't surface.

Step 1 — TS-1 gap #1 (localspace re-cache in the back-probe fallback), §2

Do this FIRST, alone. It's the narrowest, most mechanically certain change (a missing setup step before an existing call, not a behavior redesign), and per §4's coupling analysis, TS-4's shortcut removal is UNSAFE until this lands and is proven not to wedge. Blast radius: only the back-probe fallback arm of EdgeSlideAfterStepDownFailed — the rarest of the four dispatch arms (reached only when neither a contact plane nor a walkable polygon survived the step-down probe). Low risk of regressing the already-working branch3/branch1/branch2 arms.

Required before/after: a targeted unit test exercising the back-probe arm specifically (check whether one already exists — this pass didn't find one in BSPStepUpTests.cs/DoorBugTrajectoryReplayTests.cs; if absent, write one first per TDD discipline). No connected capture needed for this step alone — it's a narrow internal-consistency fix.

Step 2 — TS-1 gaps #2 and #3 (register audit), §2

Read the raw decomp fresh for last_known_contact_plane maintenance (what writes it, and whether acdream's equivalent field diverges upstream — which would justify gap #2's fallback chain as a real compensating adaptation) and for Path-4's airborne-landing LandingZ acceptance logic (cited but not read this pass — needed to judge gap #3). Either register both as AD/AP rows with citations, or remove the acdream-only branches and re-verify against the existing CliffSlide/PrecipiceSlide test coverage. This step can run in parallel with Step 1 (different files, no shared state — a candidate for superpowers:dispatching-parallel-agents if the implementer wants to split it out).

Step 3 — capture a steep-slope / roof trajectory with TS-4's shortcut disabled

Before touching BSPQuery.cs, build a scratch/flagged path that runs Path-6 WITHOUT the steepness branch (i.e., always SetCollide + WalkableAllowance=LandingZ + Adjusted, matching retail) and replay whatever fixture reproduces the original "stuck in falling animation on the roof" / "walks up steep roofs" symptom the 2026-04-30 L.4 session used to justify the shortcut. Find that fixture/repro before writing new code — check docs/research/2026-04-30-* and git log around that date; if no fixture survives, this step needs a fresh capture via ACDREAM_PROBE_RESOLVE=1 against a known steep-roof landblock. Do not skip this step even though Step 1 theoretically closes the gap that caused the original wedge — "theoretically closes" is not "proven not to wedge," and this exact failure mode (a not-quite-faithful shortcut masking a deeper gap) has recurred at least three times in the digest's own history (#137 mechanisms 1-3).

Step 4 — land TS-4 (delete the shortcut) only after Step 3 is clean

Small, mechanical diff once Step 3 validates it's safe (§4's port shape). Retire the TS-4 register row in the same commit.

Step 5 — AP-7, independently sequenced (no coupling to Steps 1-4)

AP-7 touches PhysicsBody.calc_friction only — a different file, no shared call path with the TS-1/TS-4 edge-response chain (friction runs on GROUNDED, already-resolved velocity; TS-1/TS-4 run during the collision SWEEP itself, upstream of where friction applies). Can be done in parallel with Steps 1-4. Required before any code change: re-run (or write, if it doesn't already isolate the right thing) a regression test capturing (a) graphical/animated local-player grounded walk speed (§1's finding: likely unaffected today, but UNVERIFIED — confirm before claiming victory), (b) headless/get_state_velocity-path grounded walk speed (§1's finding: likely STILL exposed to the original L.3c hammering mechanism — this is the one that needs the real fix, not just the threshold), and (c) a remote/NPC mover sample (RuntimeRemotePhysicsUpdater.cs/RemoteMotion.cs — not audited this pass for Velocity-zeroing behavter; check before assuming safety).

Step 6 — #166 visual check, after Steps 1-5

Per §3's verdict: #166 is very likely closed BY PROXY once AP-7 (Step 5) and TS-4 (Step 4) land — no new code needed beyond those two. Verify against the P2 visual-matrix item 5 (downhill jump landing) LAST, using a fresh ACDREAM_CAPTURE_RESOLVE trajectory. Only if it still visibly mismatches retail after Steps 1-5 does #166 need further work, and that further work should be capture-driven, not a speculative Sledding auto-toggle (§3's explicit recommendation).

Step 7 — #116, separately scoped, own research session

Per §5's verdict, this is not an implementation step at all yet — it needs its own instrumented-replay session (shape-1) and a Ghidra-availability-gated or live-cdb session (shape-2) before any pseudocode can be written. Sequence-independent of Steps 1-6 (different root cause layer — collision-normal recording, not response), but shares the same slide_sphere/validate_transition machinery TS-1 depends on, so land it AFTER Steps 1-4 settle to avoid two people touching TransitionTypes.cs's collision-normal plumbing at once.

Fixtures/tests that must exist BEFORE any behavior change lands (mission requirement, consolidated)

  • A back-probe-arm-specific unit test for TS-1 Step 1 (write if absent).
  • The steep-roof/wedge repro fixture for TS-4 Step 3 (locate or recapture).
  • AP-7's three-way regression test (graphical/animated, headless state-velocity, remote/NPC) for Step 5.
  • A fresh ACDREAM_CAPTURE_RESOLVE downhill-landing trajectory for #166 Step 6.
  • An instrumented DoorBugTrajectoryReplayTests capture for #116 shape-1 (Step 7) — extend the existing Diagnostic_Tick22760_DumpEngineInternals rather than writing a new harness.
  • Existing coverage that must NOT regress: SphereCollisionFamilyTests, Issue137CorridorSeamReplayTests, Issue137SlidingNormalLifecycleTests, WindowOpening_HeadCannotFit_EntryBlocked, and the full BSPStepUpTests/DoorBugTrajectoryReplayTests suites (D4 stays Skip-tagged until #116 shape-2 is actually resolved by evidence, not by this campaign's other changes incidentally shifting its numbers).

7. Open questions needing Ghidra/cdb (not guessed here)

Consolidated from all five sections above, each tagged with which item it blocks.

  1. [AP-7] Is the BN-rendered "two duplicated branches" in calc_friction (pseudo-C:276694-276822) a genuine BN decompiler artifact (single retail if (state & SLEDDING_PS) block misrendered), or does retail's actual source have two structurally separate paths? A live Ghidra decompile of 0050ee70 settles it. Low implementation risk either way (ACE's single-linear-function reading is adopted regardless), but affects how confidently the port shape can be described as "verified" vs. "ACE-derived."

  2. [AP-7] The p_5/p_1 polarity (does the friction-apply branch trigger on dot < 0.25 or dot >= 0.25?) and the p_2/p_3/p_4 velocity-magnitude-band/slope-flatness polarities are all test ah,0x5-class x87 flag tests, none independently Ghidra-verified this pass. ACE's clean reading is adopted as ACE-derived; a Ghidra decompile of 0050ee70 (same function as item 1) would settle all of these at once.

  3. [AP-7] The cos(10°) (raw decomp) vs. 0.99999536f (ACE) discrepancy in the Sledding slope-flatness test — genuinely different physical behaviors, not a rounding difference. Needs a Ghidra decompile of 0050ee70 checking whether the FCOS opcode is real or a BN misread of a raw float constant load. Currently unresolved; §1 provisionally recommends ACE's 0.99999536f (least churn from acdream's existing dead code) but flags this explicitly as unconfirmed.

  4. [TS-1 gap #1] SPHEREPATH::get_walkable_pos, cache_localspace_sphere, and set_walkable_check_pos were located (symbol exists, called at pseudo-C:274318-274326) but NOT read this pass (effort budget). A fresh grep-and-read of these three functions is needed before porting Step 1 — this is a same-tool (grep-named) follow-up, not a Ghidra/cdb blocker; flagged here only so it isn't lost.

  5. [TS-1 gaps #2, #3] Whether last_known_contact_plane maintenance and Path-4's LandingZ acceptance logic genuinely diverge from retail (justifying acdream's CliffSlide fallback chain and the walkable-steepness reroute as real compensating adaptations) or whether they're unnecessary inventions. Needs a fresh, focused named-decomp read (not Ghidra/cdb-gated — just not done this pass).

  6. [TS-4 / Step 3] ANSWERED 2026-07-30 (implementation session) — NOT sufficient; the wedge reproduces, and its mechanism is now precisely characterized. TS-1's gap #1 fix (this document's §2, landed the same session) does NOT unblock TS-4. A dat-free multi-frame capture (tests/AcDream.Core.Tests/Physics/Ts4SteepRoofWedgeCaptureTests.cs, using BSPStepUpFixtures.SlopedUnwalkable's 63.4° slope, PhysicsEngine.ResolveWithTransition replayed at 30 Hz with gravity integrated between resolves — the same replay idiom as Issue185OutdoorStairsSeamReplayTests) reproduces the EXACT historical shape with the Path-6 steep shortcut temporarily removed (both BSPQuery.cs sphere0/sphere1 branches): the body falls cleanly (30 ticks, position advancing every tick), lands at (0.500, 0.000, 1.247) at tick 17 with InContact=true, OnWalkable=false (confirming the steep polygon WAS accepted via the permissive CTransition::check_walkable(0.0871556997f) / LandingZ gate exactly as predicted from the retail source read below), then freezes at that exact position for the remaining 16+ ticks with zero movement — the test's own wedge-detection threshold (>15 consecutive frozen ticks =

    0.5s) trips at tick 33. With the shortcut restored, the same test is green (the shortcut's explicit AddOffsetToCheckPos keeps the body moving every tick by construction). The shortcut stays; TS-4 is NOT retired this session.

    Root-cause diagnosis (ACDREAM_DUMP_EDGE_SLIDE=1 capture against the scratch shortcut-removed build): the freeze is NOT inside EdgeSlideAfterStepDownFailed/CliffSlide at all — none of that dispatch's diagnostic lines (DumpEdgeSlideBranch, DumpStepDownBranchGate, the [steep-roof] PHASE3-RESET* lines) fire even once during the frozen ticks. Every frozen tick instead logs only edge-slide: phase2 attempt=0 env=OK obj=Adjusted followed by attempt=1 env=OK obj=Adjusted — i.e. TransitionalInsert's Phase 2 object-collision check (FindObjCollisionsInCell, TransitionTypes.cs:1572) returns Adjusted on BOTH retry attempts, and per TransitionTypes.cs:1591-1596 an Adjusted objState unconditionally continues (retries Phase 1/2 from the top) rather than falling through toward Phase 2.5/Phase 3. Phase 3 — the if (sp.Collide) { ... } block at TransitionTypes.cs:1625 that contains the DoCheckWalkable Placement re-test AND (on walkable failure) the reset-with-conditional- kill_velocity path — is gated on Phase 1 AND Phase 2 BOTH returning OK simultaneously (TransitionTypes.cs:1568-1621). Path 6's own unconditional SetCollide (the retail-faithful code path TS-4 would restore) returns Adjusted, not OK (matching retail's own return 3; // ADJUSTED_TS at pc:323783, quoted in §4 above) AND does NOT itself reposition the sphere — unlike the interim shortcut, which explicitly calls AddOffsetToCheckPos to push the sphere off the face every time it fires. With no repositioning, the SAME steep polygon at the SAME distance re-triggers Path 6 on the immediate retry, which again returns Adjusted, forever — an Adjusted↔retry oscillation at a fixed point that the loop's 2-attempt-per-resolve budget silently absorbs (returning the frozen position as though the resolve succeeded), repeating identically on every subsequent tick's fresh resolve call. Phase 3 (and therefore DoCheckWalkable, CliffSlide, and the TS-1 chain entirely) is structurally unreachable from this state — TS-1's completeness is moot here because the code path that would call into it never runs.

    What this means for a future attempt: the missing piece is NOT (only) in EdgeSlideAfterStepDownFailed/CliffSlide — it is in how TransitionalInsert's Phase 1/2/2.5/3 dispatch (TransitionTypes.cs :1568-1710) distinguishes "Phase 2 found a NEW collision, retry from the top" from "Phase 2 registered a touch via sp.Collide and should fall through toward Phase 3 regardless of its own Adjusted return." Retail's own transitional_insert (pc:273137, 0050b6f0) has NOT been read closely enough this pass to say definitively whether it treats a Path-6-sourced ADJUSTED_TS differently from an ordinary Adjusted result before this session's continue-on-Adjusted structure was written — that fresh, close read (specifically: does retail's loop check sphere_path.collide on EVERY iteration regardless of the latest Phase-2 return value, or only when Phase 2 returns OK?) is the concrete next step, not a second speculative code change. Do not retry the plain shortcut-deletion variant without that read; do not invent a third variant (e.g. teaching Path 6 to reposition the sphere itself) without confirming that's what retail actually does — that would be exactly the kind of guess CLAUDE.md's workflow forbids twice in a row on the same item.

  7. [#116 shape-1] Where exactly, in the BSP/environment hit-test dispatch (candidate: BSPTREE::find_collisions's PathClipped/ collide_with_pt arm, pseudo-C ~323700-323830, sibling to the set_collide arm read for TS-4), does acdream fail to record the door-face collision normal that retail records at tick-22760? Needs an instrumented replay (§5 Step 7), not Ghidra/cdb per se, but the candidate function itself would benefit from a clean Ghidra decompile alongside the BN pseudo-C already read.

  8. [#116 shape-2] Does retail's first airborne wall-contact frame reach CSphere::slide_sphere (→ in-frame slide, Z=1.92-style), or hard-stop upstream via some last_known_contact_plane-existence gate (→ Z=2.0-style, sliding deferred to frame 2)? This is the one item in this whole document that the digest, ISSUES.md, AND this research pass all independently converge on: it needs a live cdb trace of retail landing an airborne wall hit (toolchain: docs/architecture references the CLAUDE.md "Retail debugger toolchain" section; the digest already has a scoped cdb script sketch at memory/project_physics_collision_digest.md:8537-8543). A wrong guess here, per the digest's own words, "regresses ALL wall-slide behavior."


8. Campaign P Slice 1B closeout — exact response ordering (2026-07-31)

The follow-up read used the complete named-retail bodies, not the earlier excerpt summaries:

  • CTransition::transitional_insert at 0x0050B6F0 (pseudo-C:273137 onward) returns OK_TS as soon as contact_plane_valid != 0. Only an invalid contact reaches the ordinary StepDown tail, whose remaining gates are Contact, !sphere_path.step_down, a non-null check cell, and ObjectInfo.StepDown.
  • Its StepDown schedule is asymmetric by authored sphere count. For a one-sphere mover whose requested height exceeds the foot diameter, retail clamps the probe to half the foot radius and performs one probe. Otherwise a request within the diameter probes once; an over-diameter request on a two-sphere mover is halved and probes twice in sequence.
  • CTransition::edge_slide at 0x0050B3D0 (pseudo-C:273001-273090) runs !OnWalkable || !EdgeSlide restore-and-OK before its steep-contact CliffSlide branch. Any stored walkable polygon routes to PrecipiceSlide without a steepness test.
  • CTransition::cliff_slide at 0x0050A6D0 (pseudo-C:272397 onward) crosses the supplied contact normal only with collision_info.last_known_contact_plane.N. It has no remembered-walkable or world-up substitute. A default, invalid, parallel, or otherwise degenerate cross naturally returns OK_TS through the retail normalization guard.

TransitionTypes.cs now follows that order exactly. AP-3, AP-4, AD-53, and AD-54 are retired together. Slice 1B deliberately left the ordinary-tail runPlacement: false choice for the following AP-5-specific slice and did not alter TS-4's Path-6 steep-polygon shortcut. Slice 2A below closes AP-5.

RetailEdgeResponseOrderingTests pins every distinguishing branch: valid steep-contact early return, the one/two-sphere probe schedule, not-OnWalkable-before-CliffSlide, last-known-only source selection, degenerate last-known handling, and stored-steep-walkable-to-Precipice routing. It also runs multi-frame steep-roof and flat-roof-edge controls through both parsed graph and prepared-flat collision traversal, requires exact trace parity, and rejects a greater-than-15-tick frozen streak. The earlier dedicated Ts4SteepRoofWedgeCaptureTests remains green, so retiring these four compensations did not require weakening or deleting the TS-4 control.

Corrective review: edge_slide has two outputs

The first Slice 1B commit collapsed CTransition::edge_slide's function return into its out TransitionState. That loses a material retail case: the steep-contact branch writes the result of cliff_slide to the out state but returns false independently. A degenerate/parallel CliffSlide therefore writes OK_TS and still tells transitional_insert to continue its outer retry. Branch 1 and the contact-without-remembered-walkable branch stop with OK_TS; a COLLIDED_TS Precipice result stops; SLID_TS and ADJUSTED_TS retain their ordinary retry handling.

The corrective implementation preserves this exact bool-plus-out-state seam. Its end-to-end test drives an invalid-contact StepDown probe into a steep, parallel-last-known CliffSlide, then proves a second outer object pass occurs before success. The roof controls were also hardened: the flat-roof fixture must first land and publish its persistent contact/walkable chronology, fails its control arm when the roof is removed, and proves outward-X rejection plus continued edge tangency. The steep-roof fixture now rejects non-finite or oversized frame steps and signed-plane penetration until polygon exit. Parsed graph and prepared-flat runs compare every ResolveResult field and every persistent PhysicsBody field by raw float/double bits, including the ordered walkable vertex payload.

9. Campaign P Slice 2A closeout — mandatory StepDown placement (2026-07-31)

The complete CTransition::step_down body at 0x0050B2A0 (pseudo-C:272946272998) has no caller-controlled validation choice. It resets walk_interp to 1, performs the transitional downward/support probe, applies the EdgeSlide && !StepUp walkable-support gate, and then always:

  1. saves sphere_path.insert_type;
  2. installs PLACEMENT_INSERT;
  3. calls CTransition::transitional_insert(this, 1);
  4. restores the saved insert type; and
  5. returns true only for OK_TS.

This is the same tail for normal grounded contact maintenance, the edge_slide current-position back-probe, and StepUp. The former acdream runPlacement: false argument on the first two callers was therefore a real behavioral divergence, not a caller-specific retail mode.

The historical justification for the bypass was a wall-slide candidate that Placement rejected. The correct boundary is the Placement dispatcher, not StepDown. Retail's BSP, sphere, and cylinder Placement branches are pure occupancy tests and shave PhysicsGlobals.EPSILON from their effective reach. The current graph and prepared-flat dispatchers already implement that rule: an exactly tangent sphere remains valid while penetration beyond the retail epsilon is rejected. With the response-order fixes from §8 in place, restoring the mandatory placement tail does not reproduce the old wall stall.

RetailStepDownPlacementTests pins the mechanism end to end for one- and two-sphere movers, ordinary contact maintenance, StepUp, and a supported candidate that overlaps only during final Placement. It also compares graph and prepared-flat Placement at exact wall tangency and beyond-epsilon overlap. The existing multi-frame roof/edge fixtures continue to compare the complete ResolveResult and persistent PhysicsBody state by raw bits, and the #273, #271, #185, StepUp, transition-retry, and TS-4 controls remain unchanged. AP-5 is retired; TS-4 is intentionally untouched.