acdream/docs/research/2026-08-08-345-d0-branch-pin.md
Erik ab89ebdf92
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
fix(physics): #345 — a grounded mover glides along a too-steep face; validate_walkable's return is scoped as retail's bytes scope it
Retail's OBJECTINFO::validate_walkable @0x0050d010 initializes its
return slot to OK (0x0050d025) and assigns ADJUSTED only inside the
below-plane guard, immediately after the push executes (0x0050d249).
The guard-fail path — grounded, OnWalkable, plane too steep — jumps
past the contact write, the push, and the assignment (0x0050d1b9 ->
0x0050d251): retail deliberately IGNORES the steep plane at primary
validation so the insert proceeds, the step-down phase fails on the
steep landing, and the edge family produces the per-tick lateral
glide. ACE flattened this into an unconditional return Adjusted
(ObjectInfo.cs:169) and we inherited it; our TransitionalInsert then
retried the byte-identical Adjusted forever — the user's
stop-instead-of-slide.

Evidence chain: the user's retail observation (the axiom), the live
cdb glide profile (edge_slide/cliff_slide 594 each in lockstep,
step_up 0), the D0 implementer's correct STOP (fixtures reproduced
the stuck fingerprint while faithfully executing the ACE-shaped
reading — refuting the reading, not the code), and the capstone
byte-decode both Opus reviewers re-derived independently, including
the stack-slot frame arithmetic and every ret site's eax.

The conformance fixture is the live topology: flat and steep terrain
triangles sharing ONE cell's diagonal (a cell-boundary face does NOT
reproduce the loop — the cell-scoped primary sample never validates a
neighbour's triangle — and is pinned as supplementary). Sabotage:
restoring the unconditional Adjusted reds the discriminator with the
exact stuck position (0.325 m lateral, 28/30 stuck ticks vs 2.602 m /
14/30 fixed; reviewer B's independent five-angle table is monotone
10-85 degrees). Stuck ticks are counted from positions so the
assertion survives the eventual probe strip.

In-game glide gate PASSED 2026-08-08: "Well it works, we are sliding.
I cant detect any speed change from retail."

Filed alongside: #347 + AD-70 (our glide alternates arm/move at half
retail's per-tick rate — retail redirects within the tick; next up by
user direction), AD-71 (the guard's mutable WalkableAllowance operand
vs retail's fixed is_valid_walkable global — now return-value-bearing),
and the reviewers' named residuals in the #345 closure entry
(placement-arm flip, other-cell coverage gap, EdgeSlide-less
projectiles, ACE's server-side shared misport predicting remote
drift-then-snap on steep terrain). The unported IsViewer arm of
validate_walkable is noted in the D0 doc.

Suite: clean-room complete solution 11,271 passed / 4 skipped / 0
failed; Core assembly re-run green after the review-driven test
hardening.

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

276 lines
17 KiB
Markdown
Raw 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.

# #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.
## D0 RESOLVED (2026-08-08, parent session): the missed branch is validate_walkable's RETURN SCOPING — byte-proven
Capstone disassembly of the PDB-paired binary (`C:\Users\erikn\Downloads\
acclient.exe`, v11.4186) at `OBJECTINFO::validate_walkable` @0x0050d010:
```
0050d020 mov edi, 1
0050d025 mov [esp+0xc], edi ; var_1c = 1 = OK_TS (return-value slot)
...below-plane arm...
0050d1a9 test ecx, ecx ; sp->step_down
0050d1af jne 0x50d1bf ; set → guard body
0050d1b1 test byte [ebp+4], 2 ; oi->state & OnWalkable
0050d1b5 je 0x50d1bf ; clear → guard body
0050d1b7 test eax, eax ; is_valid_walkable(N)
0050d1b9 je 0x50d251 ; TOO STEEP → SKIP guard body ENTIRELY
0050d1bf ...set_contact_plane, step-down interp (fail: mov eax,2 → COLLIDED),
0050d244 call add_offset_to_check_pos ; the push
0050d249 mov dword [esp+0x10], 3 ; var_1c = ADJUSTED — ONLY after the push
0050d251 ...collision-normal tail (Contact set → skipped)...
0050d271 mov eax, [esp+0x10] ; return var_1c
```
**The guard-fail path (grounded mover, OnWalkable, too-steep plane) never
touches var_1c: retail returns OK.** The steep below-plane is deliberately
ignored by primary walkable validation; the insert proceeds, the OK-arm
step-down phase runs at the advanced position, its walkable probe fails on
the steep landing (`check_walkable` early-out: `0050d187 mov eax,2`
COLLIDED, also byte-confirmed), `StepDown` fails, and `EdgeSlide` /
`CliffSlide` produce the per-tick lateral glide. Every round-1 counter
(594 edge/cliff lockstep, step_up=0, walkable_hits_sphere=0,
adjust_sphere_to_plane=0, vwalk high) is reproduced by this reading.
**ACE misported this** (`ObjectInfo.cs:169` returns Adjusted
unconditionally after the guard block) and acdream inherited the shape.
Binary Ninja had `var_1c = 3` correctly scoped inside the guard all along
(pc:274525+ region) — but its `void` return typing and bare `return;`
statements hid the return-value mechanics from every prior reading.
**The fix (supersedes the contract's D1 scope, per its own "find the real
branch" clause):** in our `ValidateWalkable` below-plane arm
(`TransitionTypes.cs:3744-3785`), return `Adjusted` only when the guard
passed and the push executed; return `OK` when the guard fails. No routing
change in `TransitionalInsert` is needed — the existing OK-arm step-down
block + edge family already produce the cascade (proven live-healthy by
this morning's 18 clean edge-family entries).
Also byte-confirmed while here: the step-down interp failure returns
COLLIDED (`0050d28b mov eax,2`) — ACE and our port are RIGHT there; BN's
bare `return` hid it. And the check_walkable early-out returns COLLIDED
(`0050d187`) — all three agree.
**Flagged secondary (file, don't chase):** retail's guard calls
`CPhysicsObj::is_valid_walkable(N)` (fixed retail threshold) at BOTH the
resting and below-plane sites; ours tests `N.z >= sp.WalkableAllowance`.
In this scenario both reject N.z=0.6, so it is not #345's cause — but the
operand difference needs its own conformance check.