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 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>
182 lines
9.8 KiB
Markdown
182 lines
9.8 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.
|
|
|
|
## 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.
|