acdream/docs/research/2026-07-30-response-layer-edge-family-pseudocode.md
Erik 3a4782048e docs(research): Campaign P P2 response-layer edge family - AP-7 resolved, TS-1 mostly ported, #166 reattributed
AP-7's gate is the Sledding branch; ACE's linear calc_friction (0.25 dot
threshold, unconditional small-angle subtraction, Sledding overrides) is
the correct reading and the L.3c walking regression is architecturally
moot for the root-motion path. TS-1's register cite is stale dead code -
the PrecipiceSlide/CliffSlide/EdgeSlide chain is substantially ported
with one precise back-probe re-cache gap. #166 is a composite of
AD-25+AP-7+TS-4, not a missing Sledding auto-toggle (no client write site
exists). TS-4 removal is sequenced AFTER the TS-1 gap closes with
captured fixtures. #116 stays oracle-first. Port order + 8 open
Ghidra-verify questions recorded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 07:54:32 +02:00

55 KiB
Raw Blame History

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

Status: RESEARCH PASS COMPLETE (2026-07-30). Research-only doc for Campaign P Slice P2 (docs/plans/2026-07-29-physics-parity-campaign.md §P2). Retires TS-1, TS-4, AP-7; closes #166, #116. No source changes made by this doc; it is the pre-port research artifact for a future implementation session. Headline findings that change the plan's assumptions: TS-1 is already substantially ported (the register row and plan phrasing are stale — see §2); #166 is very likely NOT about a literal PhysicsState.Sledding auto-toggle at all (see §3); AP-7's L.3c regression may no longer reproduce under the post-R6 animation-root-motion architecture for the graphical path, but likely still reproduces for the headless/test path (see §1); TS-4's shortcut removal is coupled to TS-1's completion and must not be done independently (see §6 Step 3-4); #116 remains a genuine oracle-first research item needing live cdb/Ghidra, not an implementation item (see §5). Read §6 (port order) before starting implementation — the safe sequence is not the plan's listed item order.

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. AP-4 (CliffSlide check moved before retail's Branch-1 gate) is a live, load-bearing reordering compensating for acdream's incomplete OnWalkable bookkeeping — touches the same code region as TS-1. Do not revert AP-4's reordering without re-verifying OnWalkable is complete; read AP-4's full row before changing TransitionTypes.cs:1316 control flow.
  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] Whether TS-1's Step 1 fix alone is sufficient to let TS-4's shortcut be safely removed, or whether the 2026-04-30 L.4 session's wedge had additional causes not yet identified. Answerable only by the capture in §6 Step 3 — not a Ghidra/cdb question, but listed here because it's the single highest-risk unresolved item in this document (deleting a load-bearing shortcut based on an unproven assumption).
  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."