diff --git a/docs/research/2026-08-08-345-d0-branch-pin.md b/docs/research/2026-08-08-345-d0-branch-pin.md new file mode 100644 index 00000000..da549707 --- /dev/null +++ b/docs/research/2026-08-08-345-d0-branch-pin.md @@ -0,0 +1,219 @@ +# #345 D0 — branch pin, round 2: the routing is retail-faithful; the profile contradiction stands unresolved + +**Order:** implementation session, per `docs/research/2026-08-08-345-fix-contract.md` and +`docs/research/2026-08-08-345-mechanism-contract.md`. This session's mandate was to +find the branch the *first* D0 pass (`docs/research/2026-08-08-345-pseudocode.md`) +missed — the fix contract's premise, from the live cdb profile, is that retail's +`transitional_insert` reaches `edge_slide`/`cliff_slide` every tick during the glide +while ours dead-loops without ever reaching it. + +**Verdict: the premise's specific mechanism ("Adjusted lets the insert proceed to +step-down, ours treats it as retry-from-scratch") is REFUTED by a clean re-trace and +by two independent, empirical, byte-for-byte reproductions in a synthetic fixture. +Our `TransitionalInsert`/`InsertIntoCell`/`CheckOtherCells` routing is faithful to +the decompiled retail control flow for this exact topology (grounded-on-flat, +step crosses into too-steep terrain). The contradiction against the live retail +profile is real and unresolved — this session SOPS per the fix contract's explicit +fallback rather than making an unproven change.** + +## What this session did beyond the first D0 pass + +The first pass (`2026-08-08-345-pseudocode.md`) traced `ValidateWalkable`, +`TransitionalInsert`'s switch, `CheckOtherCells`, `ValidateTransition`, and +`SetSlidingNormal`, and concluded retail's own algorithm converges to the same +zero-yield stop. It left one explicit open question: does retail's +`CObjCell::find_cell_list`/our `CellTransit.FindCellSet` broadphase actually +include the neighboring too-steep cell as an "other cell" to test, or does the +narrower real geometry not reach it at all — a question it said needed a live +cdb trace, not more code reading. + +This session did three additional things the first pass did not: + +1. **Read `CTransition::step_down` (0x0050b2a0) in full** — the function + `transitional_insert`'s step-down/edge-slide block actually calls before + `edge_slide`. This is the literal mechanism the fix contract's hypothesis + named. Confirmed: `step_down` sets `sphere_path.step_down=1`, moves + `check_pos.z -= arg2`, and calls `transitional_insert(this, 5)` recursively + at the LOWERED position — but this whole apparatus is gated behind + `transitional_insert`'s own `if (edi == OK_TS)` block (pc:273191, + 0x0050b787), which is reached **only** when both the primary + `insert_into_cell` **and** `check_other_cells` return `OK_TS` for the + CURRENT (un-lowered) attempt. +2. **Read `CTransition::check_other_cells`'s switch (0x0050ae50, pc:272733-272753) + byte-for-byte** — case `COLLIDED_TS`/`ADJUSTED_TS` (2/3) both `return result` + immediately, no further cells tried, no state reset. This is a clean 4-way + integer switch on a virtual call's return value — no x87, no missing flag + tests, no BN garbling. High confidence in this reading. +3. **Built two independent synthetic terrain fixtures** (not committed — thrown + away after use, see below) and ran them through the *real* + `PhysicsEngine.ResolveWithTransition` → `Transition.FindTransitionalPosition` + → `TransitionalInsert` pipeline with the project's own instrumentation + (`ACDREAM_PROBE_STEP_WALK`, `ACDREAM_DUMP_TRANSIT_FAIL`, + `ACDREAM_PROBE_INDOOR_BSP`, `ACDREAM_DUMP_EDGE_SLIDE`) to get ground truth + instead of continuing to hand-derive float comparisons. + +## The two empirical fixtures + +Both: a grounded player-flagged mover (radius 0.48, `ObjectInfoState.IsPlayer | +EdgeSlide`, `oi.OnWalkable=true`, `oi.Contact=true`), resting on flat terrain +(N=(0,0,1)), given a diagonal (45°-ish) horizontal-only request that lands on a +terrain triangle with `N.z=0.6` (< `PhysicsGlobals.FloorZ`=0.6642, i.e. +genuinely too steep — matching the fix contract's "N.z≈0.6" scenario). + +**Fixture A — steep triangle in a NEIGHBORING outdoor cell** (request crosses a +24 m terrain-cell boundary). Result: the PRIMARY cell's terrain query is +out-of-bounds for the new XY (`SampleTerrainWalkableInCell`'s fixed-cell bounds +check, `PhysicsEngine.cs:1076-1080` — itself a documented, intentional retail +mirror: "retail continues dispatching the captured `CObjCell*`") and passes +through as `OK`. `CheckOtherCells` (`TransitionTypes.cs:2999-3010`) then finds +the true neighboring cell, calls `ValidateWalkable`, gets the below-surface +"too steep" branch (`dist=-0.04`, `sp.StepDown=false` at this point since we're +still in the un-lowered outer attempt, `walkable=false` since `N.z(0.6) < +WalkableAllowance(FloorZ=0.6642)`), returns `TransitionState.Adjusted` +unconditionally (the branch always returns Adjusted regardless of whether the +push guard fired — `ValidateWalkable`, `TransitionTypes.cs:3785`). +`ApplyOtherCellResult` (`TransitionTypes.cs:3316-3333`) halts on Adjusted. +`RunCheckOtherCellsAndAdvance` returns Adjusted. `TransitionalInsert` +(`TransitionTypes.cs:2055-2065`) sees `otherState != OK`, sets +`transitState = otherState`, and `continue`s the outer retry — **never +reaching the `ci.ContactPlaneValid`/step-down gate at line 2248/2252 at all.** +Three outer attempts, byte-identical `dist=-0.04000` each time (the query +never changes because nothing in the ADJUSTED path perturbs `sp.CheckPos`). +`ValidateTransition` then forces `OK`, reverts to the start position: **0% +XY yield**, `[transit-fail]` STUCK-TICK fires. + +**Fixture B — steep triangle WITHIN the SAME primary cell** (both start and +target inside one 24 m cell's own quad; the too-steep triangle is one of the +cell's own two split triangles, `N=(-0.8,0,0.6)`, reached via the diagonal +split so no other-cell dispatch is needed at all). Result: even cleaner — +`InsertIntoCell`'s OWN inner retry loop (`TransitionTypes.cs:2383-2390`, up to +3 attempts) calls `FindPrimaryCellCollisions` → `FindEnvCollisions` → +`ValidateWalkable` directly, gets `Adjusted` on every one of its 3 attempts +(`dist=-9.74400` identical all 3, deep penetration by design), and returns +`Adjusted` from `InsertIntoCell` itself — before `check_other_cells` is even +called. `TransitionalInsert`'s outer switch, `case ADJUSTED_TS: { neg_poly_hit += 0; break; }` (`TransitionTypes.cs:2030-2037`), skips the entire `if (edi == +OK_TS)` block. 3 outer attempts × 3 inner attempts = 9 total +`[transit-fail-insert]` lines, every one `env=Adjusted`, never reaching +`environment=OK`. `DoStepDown`/`EdgeSlideAfterStepDownFailed` are **never +called** — zero `edge-slide:` diagnostic lines fire in this fixture's log, +confirmed by grep. **0% XY yield**, STUCK-TICK fires, identical fingerprint to +Fixture A and to the original captured bug (`spStepDown=False`, +`guardPassed=False`, `outcome=Adjusted`, byte-identical `dist` every attempt). + +Both fixtures independently, by two structurally different routes (neighbor +cell vs. same cell), reproduce the exact captured #345 fingerprint from +`docs/ISSUES.md` — and both do so by faithfully executing the documented +retail control flow (`check_other_cells`'s unconditional early return on +Adjusted; `transitional_insert`'s post-switch `edi==OK_TS` gate). Neither +fixture ever reaches `DoStepDown`/`EdgeSlideAfterStepDownFailed`/`CliffSlide` — +which the trap-list machinery (`RetailEdgeResponseOrderingTests.cs`, +already green) proves is itself faithful and reachable once a *clean* `OK` +attempt occurs. **The problem, if it is a problem in acdream's code at all, is +never reaching a clean `OK` attempt in the first place — which is exactly what +retail's own decompiled `insert_into_cell`/`check_other_cells` say should +happen for this topology too.** + +## Why this is a STOP, not a D1 fix + +The fix contract's own hypothesis was specific: *"does retail's grounded step +run a step-down phase per sub-step such that validate_walkable's Adjusted-on- +steep lets the insert PROCEED to that phase ... where ours treats the +primary's Adjusted as retry-the-insert?"* This session traced the exact +function (`step_down`) and the exact gate (`transitional_insert`'s +`if (edi == OK_TS)`) the hypothesis named, byte-for-byte, and found retail's +own code does **not** let an Adjusted result proceed to the step-down phase — +it retries from scratch, identically to acdream's port. The hypothesis, taken +literally, is refuted, not confirmed. Two independently-constructed fixtures +running acdream's real `TransitionalInsert`/`InsertIntoCell`/`CheckOtherCells` +production code confirm this refutation empirically, not just on paper. + +This leaves the contradiction between: +- the pseudo-C (hand-traced + empirically reproduced twice): this topology + should dead-loop, in BOTH retail and acdream, and +- the live cdb profile (`345-retail-glide.cdb.log`): retail's glide fires + `edge_slide`/`cliff_slide` 594 times each, in lockstep, `step_up` zero, + +genuinely unresolved. Per the fix contract ("if the hypothesis is wrong, keep +going until you find the branch that produces the profile") this session kept +going — through `step_down`, `check_other_cells`, `InsertIntoCell`'s own +retry loop, `CheckOtherCells`'s cell-set dispatch (`RunCheckOtherCellsAndAdvance`, +`TransitionTypes.cs:3562-3636`, `CellTransit.FindCellSet`) — and found every +one of those to be a faithful, citable, non-garbled port. No garbled +Binary-Ninja decision point was found in the functions actually exercised by +this topology (`insert_into_cell`, `transitional_insert`'s switch, +`check_other_cells`'s switch, `step_down`) — all were clean integer +dispatches, not x87/flag-test mush, so this is not a "note the address and +STOP for byte-decode" situation either. + +**What remains unresolved, and is out of this session's scope to fix by +guessing:** +- Whether retail's *actual* Rithwic terrain topology differs structurally from + both synthetic fixtures here in some way that changes which branch fires — + e.g. a shallower approach that keeps the primary/other-cell query in the + "above" branch for more of the glide, letting more attempts reach a clean + `OK` and hence `step_down`/`edge_slide` (this is plausible in principle: the + live profile's `set_sliding_normal` count, 538, is *lower* than `edge_slide`/ + `cliff_slide`'s 594, meaning not every `edge_slide` call reaches + `cliff_slide`'s effective branch — consistent with a live approach that + spends *some* attempts in "above" and only *some* in "below-and-caught"). +- Whether `CellTransit.FindCellSet`'s broadphase (feeding `CheckOtherCells`'s + `cellSet`) is over- or under-inclusive relative to retail's real + `CObjCell::find_cell_list` for a moving sphere near a terrain-cell boundary + — the same open question the first D0 pass flagged, still open, still + requiring the live cdb trace named in that pass's own recommendation + (`validate_walkable @0x0050d010` / `transitional_insert @0x0050b6f0` / + `adjust_sphere_to_plane @0x00538210` while the glide happens), which this + text-only session cannot execute. + +Per the fix contract's acceptance rule #4 ("Any contradiction between the +profile, the pseudo-C, and our code that you cannot resolve → STOP and report +with the evidence. A correct STOP is a success."), this session stops here. +**No production code was changed.** The two throwaway diagnostic fixtures used +to gather this evidence were deleted before this document was written; they +are reproducible from the exact parameters quoted above if a future session +wants to re-verify or extend them (in particular: a graduated multi-triangle +terrain fixture that keeps some attempts in the "above" branch, to test the +"live approach spends only some attempts below" theory above). + +## Trap list compliance + +No change was made to `ValidateWalkable`'s math, `AdjustOffset`, the #331 +absorb, the #32 setter split, `DoStepUp`/`DoStepDown` internals, the +edge-family response bodies, or the `stepDownHeight = oi.StepUpHeight` +oddity. `RetailEdgeResponseOrderingTests.cs` (unmodified) already pins that +`DoStepDown`→`EdgeSlideAfterStepDownFailed`→`CliffSlide` chain as faithful and +reachable from a clean `OK` attempt — this session's fixtures corroborate that +finding rather than contradicting it; the gap is entirely upstream of it. + +## Parent addendum (2026-08-08, post-STOP): the ACE cross-check sharpens the question + +`references/ACE/Source/ACE.Server/Physics/Transition.cs:779-934` +(`TransitionalInsert`) — an independent port of the same retail loop — shows +`EdgeSlide` is reachable ONLY from the `transitState == OK` arm: insert +succeeds → the grounded step-down block runs → `StepDown` fails → +`EdgeSlide(ref transitState, ...)`. On `Adjusted` the loop merely clears +`NegPolyHit` and retries, exactly as our port and this doc's fixtures show. + +Therefore the live profile (594 `edge_slide`/`cliff_slide` lockstep per run, +`step_up`=0) FORCES the conclusion that retail's primary insert RETURNS OK +per tick in the glide scenario, while ours returns `Adjusted` 3×3 per tick. +The missed branch is not routing-after-Adjusted; it is whatever prevents +retail's per-tick check position from tripping `validate_walkable`'s +below-plane push (`@0x0050d010` mover arm, `return ADJUSTED_TS`) in the +first place — most plausibly the interplay of the previous tick's +edge/cliff slide having already moved the request, or a check-position +seeding difference at insert entry. + +**Falsifiable predictions for the round-2 capture +(`tools/cdb/345-glide-stacks.cdb`):** +1. `tins` (transitional_insert) counts ~1 per tick — NO retry storm in + retail (ours: 9 attempts/tick). +2. `stepdown` runs in lockstep with `edge` (≈594 scale). +3. The `edge_slide` stacks show `transitional_insert` (or + `find_transitional_position`) directly — the OK-arm step-down block — + with NO intervening validate/adjust retry frames. + +If prediction 1 fails (retail also retry-storms), the divergence is inside +the retry's convergence instead, and the Adjusted-production question +reopens. Either way the capture discriminates. diff --git a/tools/cdb/345-glide-stacks.cdb b/tools/cdb/345-glide-stacks.cdb new file mode 100644 index 00000000..94442936 --- /dev/null +++ b/tools/cdb/345-glide-stacks.cdb @@ -0,0 +1,22 @@ +.logopen C:\Users\erikn\source\repos\acdream\345-glide-stacks.cdb.log +.sympath C:\Users\erikn\source\repos\acdream\refs +.symopt+ 0x40 +.reload /f acclient.exe + +r $t0 = 0 +r $t1 = 0 +r $t2 = 0 +r $t3 = 0 +r $t4 = 0 +r $t5 = 0 + +bp acclient!OBJECTINFO::validate_walkable "r $t0 = @$t0 + 1; .if (@$t0 < 100000) { gc }" +bp acclient!CTransition::transitional_insert "r $t1 = @$t1 + 1; gc" +bp acclient!CTransition::edge_slide "r $t2 = @$t2 + 1; .if (@$t2 <= 6) { .printf \"[edge_slide %d]\\n\", @$t2; kc 14 }; .if (@$t2 < 300) { gc }" +bp acclient!CTransition::cliff_slide "r $t3 = @$t3 + 1; .if (@$t3 <= 6) { .printf \"[cliff_slide %d]\\n\", @$t3; kc 14 }; gc" +bp acclient!CTransition::step_down "r $t4 = @$t4 + 1; .if (@$t4 <= 6) { .printf \"[step_down %d]\\n\", @$t4; kc 14 }; gc" +bp acclient!CTransition::step_up "r $t5 = @$t5 + 1; gc" + +g +.printf "FINAL vwalk=%d tins=%d edge=%d cliff=%d stepdown=%d stepup=%d\n", @$t0, @$t1, @$t2, @$t3, @$t4, @$t5 +qd