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

17 KiB
Raw Permalink Blame History

#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.ResolveWithTransitionTransition.FindTransitionalPositionTransitionalInsert 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 continues 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 FindPrimaryCellCollisionsFindEnvCollisionsValidateWalkable 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 DoStepDownEdgeSlideAfterStepDownFailedCliffSlide 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.