# P2 — Collision response-layer edge family: port-ready pseudocode **Status: RETAIL RESPONSE ORDER COMPLETE (2026-07-31).** Originally a research-only doc for Campaign P Slice P2 (`docs/plans/2026-07-29-physics-parity-campaign.md` §P2). The first TS-4 attempt reproduced a steep-roof fixed point because its fixture discarded the accepted contact state between frames; §7 item 6 preserves that useful failure analysis. Slice 1B then completed the exact nested edge/StepDown dispatcher, Slice 2A restored StepDown's mandatory Placement tail, and Slice 2B repeated the removal with production-shaped state carry and the complete direction matrix. TS-4 is now retired; see §10. #166 got a reattribution note in ISSUES.md rather than new code (per §3). #116's former skipped wall control is now active with the retail Path-6 first-frame hard stop and next-frame slide chronology. **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). The apparent TS-4 wedge was a harness-state defect, not a reason to retain a product compensation: production-shaped vertical, inward, tangential, uphill, and downhill histories now pass graph/flat raw-bit parity without the shortcut. Campaign P Slice 1B removed AP-3, AP-4, AD-53, and AD-54 (§8); Slice 2A retired AP-5 (§9); Slice 2B retired TS-4 (§10). 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`) ```csharp 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 **FACT** — `acclient.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`: ```csharp 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, `00539300` — `BSPQuery.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.EdgeSlideAfterStepDownFailed` — `TransitionTypes.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: ```csharp // 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 former shortcut (HISTORICAL FACT; removed by Slice 2B) Before Slice 2B, Path-6 (the default `sphere_intersects_poly → collide_with_pt / SetCollide` dispatch) was not symmetrically wrong. The parsed-graph primary/foot branch tested the hit polygon's world-space normal and, below `PhysicsGlobals.FloorZ`, projected the move along the face, wrote both collision and sliding normals, and returned `Slid`. Its secondary/head branch had already been corrected by #116 to retail's unconditional `CollisionNormal` + `Collided` response. The prepared-flat port still applied the steep shortcut to both spheres and sent both shallow cases through `SetCollide` + `Adjusted`; its head branch was therefore wrong for every slope. This distinction matters: retail does not apply one common response to both spheres. 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 in either Path-6 sphere response (FACT, `0053a730` region) Read directly from the named decomp (the `sphere_intersects_poly` / `set_collide` dispatch for the primary/foot sphere 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 in this foot branch.** It calls `set_collide` and returns `ADJUSTED_TS` for a steep roof exactly as for a shallow ramp. `walkable_allowance` is always set to `LandingZ` (the permissive landing threshold), regardless of polygon slope. If the foot is clear but the secondary/head sphere hits, retail instead performs the other slope-agnostic response: ``` localtoglobalvec(sphere_path.localspace_pos, &normal, &head_poly->plane.N); COLLISIONINFO::set_collision_normal(&collision_info, &normal); return 2; // COLLIDED_TS ``` The head branch neither calls `set_collide` nor returns `ADJUSTED_TS`. Neither sphere response has a steepness branch. 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 — `EdgeSlideAfterStepDownFailed` → `CliffSlide`/`PrecipiceSlide` (TS-1's domain, §2 above). ### TS-4 port shape (COMPLETED 2026-07-31) Delete the primary/foot steep shortcut in both representations. Every foot hit must call `SetCollide`, set `WalkableAllowance=LandingZ`, and return `Adjusted`. Preserve the parsed-graph head branch corrected by #116, and replace the prepared-flat head steep/shallow split with the same unconditional `SetCollisionNormal` + `Collided` response. No Path-6 branch writes a sliding normal. This is the exact retail foot/head split, not a shared fallback to the foot behavior. ### Port-order coupling with TS-1 (historical guard, now satisfied) **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 (`EpsilonSq` → `F_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 `continue`s (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:272946–272998) 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`. The `walk_interp = 1` assignment occurs once at `step_down` entry. Retail does not reset it again before the final Placement insertion: that insertion sees and carries the exact interpolation value left by the support probe. Likewise, `CTransition::check_walkable` (`0x0050AFF0`, pseudo-C:272811–272856) saves its temporary `check_pos` and cell in stack locals. It does not overwrite `SPHEREPATH::backup_check_pos`/`backup_cell`, because the enclosing edge-slide still needs that outer failed candidate after the nested support probe. 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, including raw-bit `walk_interp` carry, nested `check_walkable` backup preservation, 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 full-engine floor-plus-wall replay drives ten grounded maintenance frames for both sphere counts, requires one Placement for every successful support maintenance, proves no freeze or penetration plus tangential progress, and compares complete `ResolveResult` and persistent `PhysicsBody` state by raw bits between parsed-graph and prepared-flat paths. The #273, #271, #185, StepUp, transition-retry, and TS-4 controls remain unchanged. AP-5 is retired; TS-4 is intentionally untouched. --- ## 10. Campaign P Slice 2B closeout — TS-4 retired (2026-07-31) A fresh read of `BSPTREE::find_collisions` (`0x0053A440`) confirms the exact Path-6 split. A primary/foot-sphere polygon hit transforms the polygon normal, calls `SPHEREPATH::set_collide`, writes `LandingZ`, and returns `ADJUSTED_TS` (`0x0053A7B3..0x0053A7DC`) regardless of steepness. Only when that sphere is clear does a secondary/head-sphere hit write `collision_normal` and return `COLLIDED_TS` (`0x0053A793..0x0053A7A4`). Neither branch writes `sliding_normal`. Both parsed-graph and prepared-flat Path-6 implementations now follow that split exactly. The steepness branches, in-place tangent projection, and BSP- layer `SetSlidingNormal` writes are deleted. Exact site tests compare the two representations by raw bits and pin every mutated and preserved field: - foot: `SetCollide`, candidate backup, transformed `StepUpNormal`, `WalkInterp=1`, `WalkableAllowance=LandingZ`, `Adjusted`; - head: `CollisionNormal`, `Collided`, with no `SetCollide` state mutation; - both: a pre-existing sliding normal is preserved byte-for-byte. The failed first removal was a test-harness lesson, not a retail exception. Its gravity-only replay always passed `isOnGround:false`, manually zeroed vertical velocity, and only copied part of the accepted contact state, so it could not reproduce the live caller's next-frame chronology. The replacement `Ts4ProductionQuantumConformanceTests` replay executes each already-airborne, zero-root-motion 30 Hz Core collision quantum in the production order: `calc_acceleration`, `UpdatePhysicsInternal`, `ResolveWithTransition`, exact body/cell commit, then `PhysicsObjUpdate.CommitSetPositionTransition` (the Core owner of retail's Contact/OnWalkable replacement plus `handle_all_collisions`). It carries the same body, cell, contact plane, walkable plane, sliding state, stationary-fall counter, cached velocity, and transient flags for all 90 ticks. PositionManager root composition, animation hooks, movement callbacks, and fresh-airborne `LeaveGround`/`HitGround` edges are deliberately outside this already-airborne dat-free fixture; it does not claim to replay those presentation/motion stages. Parsed graph and prepared flat match by raw result/body bits for vertical, inward, tangential, and downhill roof motion plus a genuine positive-Z uphill jump. The foot-contact jump's apex precedes roof contact and every later candidate velocity remains non-positive in Z. A separate elevated head-only collision reaches the wall while Z velocity is still positive and pins the valid-normal, inward-dot retail 5%-elastic reflection without adding vertical velocity. Every direction rejects a half-second fixed point and signed-plane penetration, while exact terminal velocity, Contact/OnWalkable/Sliding bits, sliding normal, and contact plane are fixed by raw float bits. The older resolver-only wedge capture remains a deliberately weaker historical control and is restored to its original three-second bound. The former #116 D4 control remains active: its primary-sphere hit hard-stops frame one through SetCollide, then the accepted persistent normal permits the downward slide on frame two. The complete historical #273/#271/#269/#265/#185/#137/#116/cellar/roof matrix and the full Core Release suite pass without a replacement compensation. TS-4 is retired. ---