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

1246 lines
70 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:272946272998) has no caller-controlled validation choice. It resets
`walk_interp` to 1, performs the transitional downward/support probe, applies
the `EdgeSlide && !StepUp` walkable-support gate, and then always:
1. saves `sphere_path.insert_type`;
2. installs `PLACEMENT_INSERT`;
3. calls `CTransition::transitional_insert(this, 1)`;
4. restores the saved insert type; and
5. returns true only for `OK_TS`.
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:272811272856) 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.
---