The round-2 cdb capture is decisive: during a live retail glide, edge_slide fired ~1.5 times per find_transitional_position — the arm/move alternation's exact signature (3 entries on the arming tick, 0 on the moving tick) — with cliff_slide in lockstep, step_down at 2.5x, step_up 0, and every stack sample on our identical call path. cliff_slide's bytes match our port and ACE's (compare constant at 0x794610 verified 0.0), and the user could not distinguish the two clients side by side. The "retail redirects within the tick" premise misread round-1's set_sliding_normal cadence (per-event, not per-tick, so its 1:1 ratio with edge never discriminated anything). The alternation-tolerant assertion in Issue345SteepSlopeGlideTests is therefore the CORRECT retail-shape pin from both sides; its comment now cites the capture instead of calling the shape a residual. The #269 note is honest the other way: the hope that a within-tick port would explain that feel residual is withdrawn with the premise. The temporary Scratch347 diagnostic is deleted. Capture evidence: 345-glide-stacks.cdb.log (repo root, untracked, cited from the contract's RESOLUTION section). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9.8 KiB
#347 fix contract — retail's within-tick slide continuation (full-rate glide)
Date: 2026-08-08. Implementer: Fable directly (user direction), dual
Opus review, then the user's slope feel gate. Predecessor: #345 landed at
ab89ebdf (validate_walkable return scoping); this contract closes AD-70.
The defect (measured, not hypothesized)
Post-#345, the glide alternates in a strict two-tick cycle (scratch trace, 2026-08-08, 45-degree diagonal fixture):
- arming tick: ZERO XY yield, sliding normal becomes (0.707,-0.707,0), transient gains Sliding;
- moving tick: +0.115/+0.115 (the along-crease component), sliding normal CLEARS;
- repeat. 14 of 30 ticks stuck; lateral rate is HALF the input's lateral component.
Retail delivers motion EVERY tick: cdb counters (345-retail-glide.cdb.log)
show edge_slide/cliff_slide 594 each over one ~15 s glide (~per 30 Hz tick),
set_sliding_normal 538, step_up 0.
Retail's mechanism (pinned from pc this session)
CTransition::edge_slide @0x0050b3d0 (pc:273001+), steep-contact branch
(contact valid AND N.z < allowance @0050b3f7-0050b441):
sphere_path.walkable = null; restore_check_pos()— back to the SAVED (advanced, pre-step-down) candidate;*outState = cliff_slide(&contact_plane)— see below;- clears contact plane validity/water;
- returns 0 (FALSE = do not stop) —
transitional_insert's attempt loop CONTINUES and retries at the position cliff_slide just produced.
CTransition::cliff_slide @0x0050a6d0 (pc:272397+):
- crease = cross(steepN, last_known_contact_plane.N) — NOT the current contact; horizontal-projected (Z forced 0 via *0f arithmetic in BN's rendering) and 90-degree-rotated: vector (-crossY, crossX, 0) = the horizontal PERPENDICULAR to the crease;
normalize_check_smalldegenerate -> return 1 (OK_TS);- proj = dot(check-minus-curr displacement + LandDefs::get_block_offset (curr cell vs check cell — the AD-69 seam family), perpVector);
- sign-dependent arm: adds
perpVector * (+-proj)to the CHECK POSITION (add_offset_to_check_pos @0050a804/0050a857) — REMOVING the into-face perpendicular component so the check pos keeps only the along-crease part — and sets the collision normal (one arm negates the normal: 0050a813-0050a827); - returns 3 (ADJUSTED).
Net: the SAME transitional_insert call retries at the slid position, the insert validates clean, step-down lands on the flat side, OK — motion delivered within the tick, every tick. The sliding normal is set as well, so next tick's AdjustOffset pre-projection ALSO applies (that projection producing an already-clean request is why retail still fires edge_slide per tick: the request keeps pressing into the face).
Our port today (TransitionTypes.cs)
TransitionalInsert(:2306-2335) already continues the loop whenEdgeSlideAfterStepDownFailedreturns false — the LOOP shape is ported.EdgeSlideAfterStepDownFailedbranch2 (:2534-2544) matches retail's steep-contact branch: restore, CliffSlide, clear, return false.CliffSlide(:2630-2672) computes the same perpVector and even callssp.AddOffsetToCheckPoswith a sign-dependent arm and returns Adjusted.
Yet the arming tick yields zero. So the divergence is INSIDE this chain, not its shape. Candidates, in test order:
- Sign-arm inversion (BN flag-test ambiguity). Ours adds
collideNormal*anglewhen angle<=0 andcollideNormal*(-angle)when angle>0 — BOTH are non-positive multiples. Removing the perpendicular component requires the CANCELLING sign in both arms: for angle>0 subtract (ours does), for angle<0 ADD the positive multiple (ours subtracts more — DOUBLING the into-face component instead of cancelling). Retail's two arms at 0050a7d6/0050a849 vs 0050a7f6 are sign-mushed in BN — byte-decode 0050a7a0-0050a870 (fchs placement) before concluding. If ours doubles the into-face component on one arm, the slid retry re-collides HARDER -> retries exhaust -> zero yield -> exactly the alternation (next tick's pre-projection is what moves). - Retry-after-CliffSlide dies. The Adjusted continue (:2323-2327) re-enters InsertIntoCell; if the slid check pos still validates against the steep triangle (e.g. because the offset was wrong per candidate 1, or because the retry re-runs the PRIMARY phase from the pre-advance position rather than the slid candidate), attempts exhaust. The transit-fail probe's buffered [transit-fail-insert] lines (19 per stuck tick in the scratch run) name each attempt's phase outcomes — READ THEM FIRST; they may settle candidates 1 and 2 in one look.
- restore_check_pos ordering. Retail restores BEFORE cliff_slide and cliff_slide then adds its offset to the RESTORED (advanced) candidate. Ours: branch2 restores then CliffSlide adds to... verify which position RestoreCheckPos leaves in GlobalSphere (SaveCheckPos at :2265 saved the ADVANCED candidate, so restore should equal retail). Confirm with the probe's per-attempt positions.
D1 — the minimal port
Fix ONLY what the instrumentation names (expected: the CliffSlide sign arm(s), possibly one retry-entry position). Do NOT touch: the loop shape, branch1/3/4 responses, PrecipiceSlide, the #331 sliding-normal persistence/clearing, AdjustOffset (AD-66!), ValidateWalkable (#345 just landed), DoStepDown internals.
D2 — tests
- Tighten
Issue345SteepSlopeGlideTests.Angled45Approach_GlidesAlongTheDiagonal: full-rate — stuckTicks small (crossing transient only, e.g. <= 3) and lateral advance ~= the input's lateral component times the post-crossing tick count (assert >= 85% of it, ordering-safe), replacing the alternation-tolerantTicks/2 + 2bound and its comment (and drop the #347-residual comment). - The 30/60-degree ordering pin and perpendicular stop stay green unchanged.
- AD-65/AD-66 conformance, the #331 absorb pin, RetailEdgeResponseOrdering, Issue265, TransitFailProbe, EdgeSlideBackProbePrecipiceSlideTests, and the #271 staircase guard ALL stay green untouched.
- Sabotage: revert the sign/continuation fix -> the tightened full-rate assertion reds with the alternation numbers; restore.
Acceptance
Clean-room complete suite; dual Opus review (conformance byte-check of cliff_slide's fchs arms + blast radius over every CliffSlide caller); retire AD-70 in the same commit; user slope feel gate (~2 min: glide speed now matches retail side-by-side; downhill/uphill/hover unchanged).
PREMISE REVISION (2026-08-08, same session — before any code change)
Three findings force the "retail = full-rate within-tick" premise back to OPEN:
- cliff_slide's arms are conformant in all three sources. ACE
(
Transition.cs:242-266), our port, and the byte decode agree on the crease vector, the sign arms, and the compare-vs-0.0 (double at 0x794610, verified zero). For the conformance fixture's geometry those arms move the check position INTO the face (the arming tick's probe trace shows primary dist -0.056 -> -0.200 -> -0.487 across the three attempts) — the "dig" is what the code as written does, in retail's bytes too. - The round-1 counters refute a per-tick retry storm in retail. slidn:edge = 538:594 ~ 1:1.1. Our alternation shape produces THREE edge entries per sliding-normal set (the dig-retries); retail produces ~ONE. The glide window in the counter progression (edge 0 -> 594 across six 1280-vwalk blocks, then frozen) also shows edge growth stopping the moment the hold ended.
- The user's side-by-side observation ("I cant detect any speed change from retail", #345 gate, 2026-08-08) is consistent with retail ALTERNATING exactly as we do — arm one tick, move the next — and inconsistent with retail moving at double our rate.
Competing hypotheses now:
- H-A (retail alternates too): retail's arming tick runs ONE edge entry (no retry after the cliff-slide Adjusted) and yields nothing; the next tick's pre-projection moves. Then our ONLY divergence is the two extra futile dig-retries per arming tick (invisible — the tick's output is discarded), #347 closes as measured-identical, and AD-70 is RETIRED as a wrong inference, not fixed.
- H-B (retail yields within the tick): the single edge entry precedes a successful same-tick commit. Requires the retried/continued insert to land clean — mechanism unknown given the dig direction — and would keep AD-70 open as written.
The discriminator is one number in the round-2 capture:
CTransition::find_transitional_position (~1/tick for the player) vs
edge_slide during the glide window. H-A predicts ftp:edge ~ 2:1
(edge every other tick); H-B predicts ~ 1:1 (edge every tick). The
round-2 script (tools/cdb/345-glide-stacks.cdb) now counts ftp, prints
the periodic progression, samples 6 stacks each for edge/cliff/step_down,
and auto-detaches at 300 edge hits via the fall-through-then-top-level-qd
recipe. NO code changes until this capture runs.
RESOLUTION (2026-08-08, round-2 capture): H-A confirmed in its strong form — CLOSED, no code change
Round-2 (345-glide-stacks.cdb.log, auto-detached at 768 edge hits):
glide-window steady state per 1280-vwalk block: edge +133..158, ftp
+93..105, stepdown +351..376, cliff lockstep with edge, stepup 0. edge/ftp
~ 1.45 with ftp INCLUDING background movers — the player's true ratio is
~1.5, which is precisely the alternation's signature (3 arming-tick
entries, 0 moving-tick entries, averaged). All six edge_slide stack
samples: transitional_insert -> find_transitional_position ->
CPhysicsObj::transition -> UpdateObjectInternal — our exact path. Retail
performs the same dig-retries and the same alternate-tick yield. #347
closed; AD-70 retired; the D2 "tighten to full-rate" plan is CANCELLED —
the existing alternation-tolerant assertion is the correct retail pin.
The H-B mechanism does not exist in the binary's behavior.