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
The cliff_slide arms are conformant in ACE, our port, and the bytes (compare constant at 0x794610 verified 0.0), the round-1 slidn:edge ratio (538:594) refutes a retail retry storm, and the user's side-by-side speed observation fits alternation. Round-2 cdb script now counts find_transitional_position; H-A (identical, retire AD-70) vs H-B (within-tick yield) resolves on one ratio. The temporary Scratch347 diagnostic test rides along until #347 closes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
167 lines
8.9 KiB
Markdown
167 lines
8.9 KiB
Markdown
# #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):
|
|
1. `sphere_path.walkable = null; restore_check_pos()` — back to the SAVED
|
|
(advanced, pre-step-down) candidate;
|
|
2. `*outState = cliff_slide(&contact_plane)` — see below;
|
|
3. clears contact plane validity/water;
|
|
4. **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+):
|
|
1. 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;
|
|
2. `normalize_check_small` degenerate -> return 1 (OK_TS);
|
|
3. proj = dot(check-minus-curr displacement + LandDefs::get_block_offset
|
|
(curr cell vs check cell — the AD-69 seam family), perpVector);
|
|
4. 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);
|
|
5. **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 when
|
|
`EdgeSlideAfterStepDownFailed` returns false — the LOOP shape is ported.
|
|
- `EdgeSlideAfterStepDownFailed` branch2 (:2534-2544) matches retail's
|
|
steep-contact branch: restore, CliffSlide, clear, return false.
|
|
- `CliffSlide` (:2630-2672) computes the same perpVector and even calls
|
|
`sp.AddOffsetToCheckPos` with 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:
|
|
|
|
1. **Sign-arm inversion (BN flag-test ambiguity).** Ours adds
|
|
`collideNormal*angle` when angle<=0 and `collideNormal*(-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).
|
|
2. **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.
|
|
3. **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
|
|
|
|
1. 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-tolerant `Ticks/2 + 2` bound and its comment (and drop the
|
|
#347-residual comment).
|
|
2. The 30/60-degree ordering pin and perpendicular stop stay green
|
|
unchanged.
|
|
3. AD-65/AD-66 conformance, the #331 absorb pin, RetailEdgeResponseOrdering,
|
|
Issue265, TransitFailProbe, EdgeSlideBackProbePrecipiceSlideTests, and
|
|
the #271 staircase guard ALL stay green untouched.
|
|
4. 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:
|
|
|
|
1. **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.
|
|
2. **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.
|
|
3. **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.
|