fix(physics): #345 — a grounded mover glides along a too-steep face; validate_walkable's return is scoped as retail's bytes scope it
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

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>
This commit is contained in:
Erik 2026-08-07 13:32:51 +02:00
parent 7542cfd3c2
commit ab89ebdf92
5 changed files with 413 additions and 5 deletions

View file

@ -158,9 +158,76 @@ resolution.
---
## #345 — Walking angled into a too-steep slope: 100% of input eaten with an UP collision normal, no slide
## #347 — Steep-slope glide runs at HALF retail's rate: arm/move alternation instead of within-tick redirect
**Status:** OPEN — HIGH (user-felt, capture-backed). **A/B COMPLETE, same
**Status:** OPEN — MEDIUM (behavioral residual of the #345 fix, trace-backed).
With #345 fixed, the glide works but alternates in a strict two-tick cycle:
the arming tick absorbs the whole request while the edge response sets the
sliding normal ((0.707,-0.707,0) on the conformance fixture), and only the
NEXT tick's `AdjustOffset` pre-projection consumes it and moves (+0.115,
+0.115 per moving tick on the 45-degree fixture) — then the clean move
clears the normal and the cycle repeats. Fixture trace: 14 of 30 post-
crossing ticks stuck, positions advancing every other tick
(`Issue345SteepSlopeGlideTests` + the deleted Scratch345 dump, 2026-08-08).
Retail instead redirects WITHIN the tick: the live cdb profile
(`345-retail-glide.cdb.log`) fired `edge_slide`/`cliff_slide` 594 times
EACH in lockstep with `set_sliding_normal` 538 over a ~15 s glide — every
30 Hz tick, which an alternation would halve — so retail's
`transitional_insert` re-enters the redirected offset in the SAME
transition and yields motion every tick. Ours ends the transition on the
arming tick with zero yield. The fix lives in the edge-family response
semantics (`EdgeSlideAfterStepDownFailed` and the insert's continuation
after an applied edge constraint) — deliberately NOT touched by the #345
landing (fresh AD-66 in the same block; the Campaign S response-layer
landings all user-gated). Net user-visible effect: gliding along a steep
face at ~half retail's lateral speed; direction and angle-scaling correct.
If the #345 in-game gate reports "slides but slower than retail," this is
the mechanism, already filed.
## #345 — Walking angled into a too-steep slope: 100% of input eaten with an UP collision normal, no slide — CLOSED 2026-08-08 (in-game glide gate PASSED: "Well it works, we are sliding. I cant detect any speed change from retail")
**Dual Opus review verdicts: CONFIRMED-FAITHFUL (conformance, independent
byte re-decode incl. the stack-slot frame arithmetic and every ret site) and
SAFE (blast radius, truth-table instrumented: exactly one row moves; the
placement/teleport family proven immune via mover-flag grep + the
RuntimeSetPositionMoverPreparationTests pin; independent five-angle sabotage
table, monotone angle→lateral 10°85°). Named non-blocking residuals, all
retail-consistent or filed: the placement-arm REJECT→ACCEPT flip at
DoStepDown's final insert and the Collide-branch re-test (retail's
validate_walkable is insert-type-agnostic — same behavior, untested
topology); the other-cell ValidateWalkable site has zero fixture coverage
(a neighbouring-cell steep fixture would close it); projectiles (no
EdgeSlide flag) resting on a steep face commit below-plane instead of
pushing out (retail-mover-agnostic, narrow); and ACE SHARES the misport
server-side, so NPCs/remotes may show lateral drift-then-snap at steep
terrain until the next UpdatePosition — name that mechanism before
misdiagnosing any future remote-prediction report. AD-71 (mutable
WalkableAllowance operand) filed the same session.**
**FIX (one conditional return, byte-proven):** retail's
`OBJECTINFO::validate_walkable` @0x0050d010 initializes its return slot to
OK (`0x0050d025: mov [esp+0xc], 1`) and assigns ADJUSTED only INSIDE the
below-plane guard, immediately after the push executes (`0x0050d249`,
after `add_offset_to_check_pos`). The guard-fail path — grounded mover,
OnWalkable, plane too steep (`0x0050d1b9 je 0x50d251`) — skips the
contact write, the push, AND the Adjusted assignment: retail returns OK
and simply IGNORES the steep plane at primary validation, letting the
insert proceed so the step-down phase fails and the edge family produces
the glide. ACE flattened this into an unconditional `return Adjusted`
(`ObjectInfo.cs:169`) and we inherited it — the dead loop was our
`TransitionalInsert` retrying the byte-identical Adjusted forever.
`ValidateWalkable`'s below-plane arm now scopes the return exactly as the
bytes do. Evidence chain: the live cdb glide profile (594 edge/cliff
lockstep, step_up=0), the capstone byte-decode
(`docs/research/2026-08-08-345-d0-branch-pin.md`), the D0 implementer's
correct STOP (synthetic fixtures reproduced the stuck fingerprint while
faithfully executing the ACE-shaped reading — refuting the reading, not
the code), and the discriminating conformance fixture (in-cell diagonal
split, flat+steep triangles sharing one cell — sabotage red with the
exact stuck position; the cell-boundary topology does NOT discriminate
and is pinned as supplementary). Residual: #347 (half-rate alternation).
**Original report:** OPEN — HIGH (user-felt, capture-backed). **A/B COMPLETE, same
morning: PRE-EXISTING, not a campaign regression.** The pre-campaign binary
(d4e956b4, built in a throwaway worktree, same recorder, same slope, same
protocol) shows the identical signature — eleven consecutive 2-second windows

View file

@ -164,6 +164,8 @@ readiness/requeue adaptation. See
| AD-67 | **Filed 2026-08-07 at the #32 closeout.** The narrowed `CollisionInfo.SetContactPlane` still writes `ContactPlaneCellId`, which retail's `COLLISIONINFO::set_contact_plane` @0x00509d80 does not — retail writes the cell id only in `CTransition::init_contact_plane` (@0x0050e8ca). Kept deliberately at the #32 fix on the research doc's own advice: acdream's consumers (the `[support]` probe's provenance, water-plane bookkeeping, `AdjustOffset`'s `ContactPlaneCellId != 0` gate) rely on the cell id being current per contact write, and retail's equivalent state travels a different route the port has not needed. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`SetContactPlane`, the `ContactPlaneCellId = cellId` line) | The #32 fix removed the four LAST-KNOWN writes — the defect — and deliberately did not also change this contact-group field in the same commit; two behaviour changes in one fix would have made the user's cliff gate ambiguous. | A consumer that assumes the cell id changes ONLY at transition seed time (retail's timing) would observe it changing per contact write instead. No such consumer is known; `AdjustOffset`'s gate wants the current value. | `COLLISIONINFO::set_contact_plane` 0x00509d80 (22 bytes, no cell-id write); `CTransition::init_contact_plane` 0x0050e850 (cell id at 0x0050e8ca) |
| AD-68 | **Filed 2026-08-07 at the #338 closure.** During an entity's ASYNC-RESIDENCY window — its flat Setup collision not yet resident — `LiveEntityMotionRuntimeController.GetSetupMoverShape` returns a placeholder mover shape: empty sphere list (falling back to the legacy 0.48/1.835 capsule reconstruction) and step heights **0.4/0.4**, values that appear nowhere in retail (authored human values are 0.600/1.500; retail's not-on-walkable fallback is 0.04). The local player has the same window between controller construction (0.4f defaults) and the publication candidate's adoption. Retail loads Setups synchronously and has no such window at all. Measured scale: 358 placeholder resolves vs 111,248 authored-pair resolves across one long session — seconds per entity, once. | `src/AcDream.App/Physics/LiveEntityMotionRuntimeController.cs` (`GetSetupMoverShape`, the `setup is null` and `<= 0f` arms); `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs` (0.4f field defaults, adopted-over at publication) | An adaptation to async residency, not a wiring defect — #338's live probe proved prepare/publish/resolve all carry the authored values in steady state. Left as-is deliberately: shrinking the window is streaming work, not physics work. | A remote moving DURING its residency window steps 0.4 instead of its authored heights, and collides as a capsule instead of its sphere list — briefly, once per entity. If a future report says "an NPC stumbled on a stair right as it appeared", this row is the first suspect. | `CTransition::step_up` 0x0050b610 (0.04 fallback at 0x0050b655); `CPartArray::GetStepUpHeight` 0x005180d0; issue #338 |
| AD-69 | **Filed 2026-08-07 at the S4 pseudocode pass (implementer finding, verified against the decomp).** `Transition.AdjustOffset`'s safety push-out computes `dist` WITHOUT the cell-relative correction retail applies: retail's `adjust_offset` (and ACE's port, independently) run the sphere centre through `LandDefs::get_block_offset` against the contact plane's own cell before the plane-distance dot, so a contact plane owned by a DIFFERENT landblock than the mover's current cell measures in the plane's frame. acdream dots the raw world-space centre against the stored plane. Same-landblock contact (the overwhelming case) is identical; a landblock-SEAM contact measures dist offset by the block delta, mis-firing or mis-suppressing the push-out at seams. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`AdjustOffset`, the dist computation ahead of the push-out block) | Discovered during S4 but deliberately not folded in: S4's own AD-66 half was withheld the same night (#341), and a third change in the same block would have made the anomaly investigation unattributable. Fix alongside the AD-66 relanding. | A mover resting on a contact plane owned by the neighbouring landblock (seam walking) gets a push-out computed against a dist that is wrong by the block offset — either a spurious lift or a missed penetration correction, exactly at landblock seams, the #176/#177 symptom neighbourhood. | `CTransition::adjust_offset` 0x0050a370 (pc:272271-272393); `LandDefs::get_block_offset`; ACE `Transition.AdjustOffset` (cross-check); issue #341 (sequencing) |
| AD-70 | **Filed 2026-08-08 with the #345 fix.** Our steep-slope glide alternates: the edge-family arming tick absorbs the request (zero yield) and only the next tick's `AdjustOffset` pre-projection moves, then the clean move clears the sliding normal — a strict two-tick cycle. Retail redirects WITHIN the tick (`edge_slide`/`cliff_slide` 594 each over a ~15 s live glide — every 30 Hz tick, lockstep with `set_sliding_normal` 538) and yields motion every tick. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`EdgeSlideAfterStepDownFailed` + the insert's post-constraint continuation) | The #345 landing deliberately touched only `validate_walkable`'s return scoping; the response bodies were freshly user-gated (Campaign S) and AD-66 had just relanded in the same block. | Gliding along a too-steep face at ~half retail's lateral speed; direction and angle-scaling correct. Visible as "slides but slower than retail" in a side-by-side. | `345-retail-glide.cdb.log` counters; `Issue345SteepSlopeGlideTests` tick trace; issue #347 |
| AD-71 | **Filed 2026-08-08 (reviewer finding on the #345 fix).** `ValidateWalkable`'s walkable test uses the MUTABLE `sp.WalkableAllowance` where retail's `validate_walkable` calls `CPhysicsObj::is_valid_walkable` @0x0050f530 — a FIXED global threshold (N.z >= [0x8ede5c], the walkable constant; the function reads no object state). Several code paths write `WalkableAllowance = LandingZ` (0.0871557 — TransitionTypes.cs:1688,2264, BSPQuery.cs:2330, FlatBspQuery.cs:2085) and `ClearWalkable()` does not restore it, so a stale-permissive value entering a grounded `!StepDown && OnWalkable` validate makes the guard PASS where retail's fails. Every override is permissive, so the #345 fix cannot REGRESS through this path — but for planes with N.z in (0.0872, 0.6642) a stale allowance leaves the old Adjusted-without-push dead loop reachable. The #345 landing GREW this row's blast radius: the operand now gates the return value (OK vs Adjusted), not merely the push (reviewer B, 2026-08-08). Also folds in: our `FloorZ = 0.6642f` vs ACE's 0.66417414f flips OK/Adjusted in a ~0.002-degree band. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`ValidateWalkable`, the `walkable` guard operand) | Deliberately not folded into the #345 landing: the allowance plumbing is shared with the step-down family and needs its own conformance pass over every WalkableAllowance write/restore site. | A too-steep plane between LandingZ and FloorZ validated right after a placement/landing path that left the allowance permissive: the guard pushes+Adjusts where retail returns OK — the #345 stop, in a narrower band. | capstone decode of 0x0050f530 (reviewer A, 2026-08-08); `docs/research/2026-08-08-345-d0-branch-pin.md` flagged-secondary section |
---

View file

@ -217,3 +217,60 @@ seeding difference at insert entry.
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.