acdream/docs/research/2026-08-04-remote-landing-investigation.md
Erik eeec4fb42a diag(physics): remote landing-edge probe; record the two live jump defects
The user live-tested route 4a and reported two defects on player remotes: a
remote holds the falling animation after landing before finally landing, and a
remote jumping onto a house plants on the roof where retail slides off, then
blips to the slid-down position.

Neither is a route 4a regression. Do NOT revert 44830a0e — reverting would
restore the per-packet render slam 4a removed without touching either defect.

Bug B's root cause is identified and already covered by open issue #32, whose
text names both symptoms in one sentence. Both landing sites assert
TransientState |= Contact | OnWalkable unconditionally, where retail derives it
from the contact plane — CPhysicsObj::SetPositionInternal @0x00515330
(`if (contact_plane.N.z < floor_z) set_on_walkable(0) else set_on_walkable(1)`).
A steep roof is contact but NOT on_walkable; asserting both suppresses the slide
response, so the body sits until the server's positions walk 4 m away and
AP-87's threshold snaps it. That is the blip. Verified byte-identical pre-4a via
`git show 19d95094:`.

Bug B's *visible shape* IS 4a's: pre-4a every packet slammed the render entity
to the wire pose, so a stuck body flickered toward the true sliding position
5-10x per second — jitter rather than a clean hold.

Bug A stops at the goal's stop-condition rather than getting a speculative fix.
Three hypotheses with non-overlapping fixes; picking wrong means changing a
retail-ported gate on a guess. Retail's mechanism is already fully decoded, so
what is missing is OUR runtime state — no cdb trace against retail is needed.

Adds ACDREAM_PROBE_REMOTE_LANDING (PhysicsDiagnostics, read once at startup per
the diagnostic-owner rule, one bool check when off). It logs both landing sites
immediately before HitGround, and — the most diagnostic signal — emits a
separate line when a site is reached but the gravity gate is about to no-op,
which is hypothesis 1 (a wholesale Body.State write wiping the transient Gravity
bit mid-air, exactly AP-81's stated risk). Temporary instrumentation, marked for
stripping once the evidence is in.

Evidence recorded rather than new bugs filed: #32 gains the observation, the
root cause and the #173/AD-10 dependency caveat; AP-87 gains a live instance of
its stated risk; AD-10's stale file:line is corrected to RemoteMotionCombiner
with a note that its terrain-only normal cannot see a house roof at all.

Also files #308 — a SECOND flaky test, distinct from #302, which was twice
misattributed to it before being written down. #302 is a GC-allocation assertion
in App.Tests; #308 is a wall-clock deadline loop in Core.Net.Tests that fails
only under full-suite CPU contention (0 failures in 4 isolated runs). Conflating
them hides one, and an agent told to "ignore the known flake" would wave through
a real transport regression.

Gates: complete Release solution 10,938 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 01:10:03 +02:00

7.8 KiB

2026-08-04 — Remote landing investigation (Bug A / Bug B, route 4a live test)

Status: Report-only. No fix applied. Companion to docs/ISSUES.md #32 (the two symptoms below were both already named by that row's "lands on roof in falling animation, can't slide off" line) and to the ACDREAM_PROBE_REMOTE_LANDING probe added in PhysicsDiagnostics.cs.

Background

The user's live two-client route 4a test surfaced two PLAYER-remote defects:

  • Bug A — a remote stays in the falling animation after landing, then visibly lands (the pose clears) only after a delay.
  • Bug B — a remote jumping onto a house plants on the roof where retail slides off, then blips to the slid-down position.

Neither is a route 4a regression (44830a0e, 2026-08-04). The landing block in LiveEntityNetworkUpdateController.cs that both bugs touch is byte-identical to the pre-4a version — verified via git show 19d95094:src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs, which shows the same unconditional Body.TransientState |= Contact | OnWalkable at the same landing site before route 4a existed. Do not revert 44830a0e.

Bug B — root-caused, not yet fixed

See docs/ISSUES.md #32's 2026-08-04 addendum and docs/architecture/retail-divergence-register.md rows AD-10 and AP-87 for the full citation chain. Summary: the landing block force-sets OnWalkable unconditionally, where retail derives it from the contact plane (CPhysicsObj::SetPositionInternal @0x00515330, pseudo-C :283501-283509 — on_walkable = contact_plane.N.z >= floor_z). A steep roof is Contact but not on_walkable; forcing both suppresses the slide, and the body sits until AP-87's 4 m drift-snap backstop blips it to the server's already-slid position. A correct fix additionally depends on #173's remote collision-velocity reflect (shipped, its dedicated gate folded into the unrun Campaign P matrix scenario 8) and on AD-10's slope projection, which is terrain-only and cannot see building/EnvCell geometry at all.

Bug A — three hypotheses, no overlapping fix

The falling-pose-lingers symptom has three candidate root causes. Each points at a different code path with a non-overlapping fix, so guessing which one applies risks fixing the wrong thing (or "fixing" all three and losing track of which one mattered). The ACDREAM_PROBE_REMOTE_LANDING=1 probe (added 2026-08-04, src/AcDream.Core/Physics/PhysicsDiagnostics.cs: LogRemoteLanding/LogRemoteLandingGateNoOp) was built specifically to discriminate them from one live capture at both landing-detection sites: LiveEntityNetworkUpdateController.cs's UpdatePosition-driven landing block (site=controller) and RuntimeRemotePhysicsUpdater.cs's per-tick VectorUpdate landing branch (site=per-tick, ~:493-551).

H1 — Gravity state bit wiped mid-air, HitGround's gate silently no-ops (CONFIRMED mechanism, occurrence unconfirmed)

MotionInterpreter.HitGround() (MotionInterpreter.cs:2426, retail CMotionInterp::HitGround 0x00528ac0) starts with:

if (!PhysicsObj.State.HasFlag(PhysicsStateFlags.Gravity))
    return;

This mirrors retail's own gate (state & 0x400, decomp raw 305996-306014) — retail requires Gravity still set at landing, same as we do. If something clears the Gravity bit on a remote's body BEFORE its landing edge fires, HitGround() returns immediately: no RemoveLinkAnimations, no apply_current_movement re-dispatch, and the sequencer never receives the command that would swap Falling → the landing link → the grounded cycle. The falling pose then only clears once some LATER event forces a cycle (a subsequent UpdateMotion, or the generic per-tick funnel eventually reasserting a grounded stance by a different path) — matching the observed "delay."

Both known Gravity-clear sites in the codebase are the POST-HitGround "DR bookkeeping" clears (LiveEntityNetworkUpdateController.cs and RuntimeRemotePhysicsUpdater.cs, both guarded by IsCurrentStateAuthority/IsCurrentOwner version checks) — i.e. the intended clear happens AFTER HitGround, not before. No third site was found that clears Gravity early in this pass; if H1 is the live culprit, the probe should catch a case where an inbound authority-version race (a second UP superseding the landing packet, or the state-authority version check failing) caused the clear to land ahead of a re-entrant landing detection. Discriminator: gravitySet=false in the [remote-landing] line, paired with a [remote-landing-gate] NOOP line at the same site.

H2 — No DefaultSink bound at the landing edge (hypothesized, plausible)

HitGround()'s apply_current_movement dispatches through Motion.DefaultSink. The controller site only calls EnsureRemoteMotionBindings (which creates the sink) when _animatedEntities.TryGetValue(entity.Id, out var aeForLand) finds an entry AND aeForLand.Sequencer is not null (LiveEntityNetworkUpdateController.cs, right before the probe call added 2026-08-04). A remote whose LiveEntityAnimationState/Sequencer hasn't been created yet at the exact frame its landing UP arrives (freshly streamed-in, or a presentation race) would reach HitGround() with Gravity still set but no sink to drive — the re-apply computes correctly but has nowhere to write, so nothing visible changes until the sink is bound on a later frame and something else (the stale VU.land per-tick branch, or the next ordinary UpdateMotion) catches the pose up. Discriminator: gravitySet=true, hasDefaultSink=false in the [remote-landing] line.

H3 — HitGround dispatches correctly; the delay is downstream in animation-scheduler consumption (hypothesized, weakest evidence)

If both Gravity and the sink are fine at the landing edge, the failure (if it still reproduces) is not in the physics/motion-interpreter layer at all — HitGround() successfully queues the landing link, but the render-side animation scheduler (LiveEntityAnimationScheduler/ LiveEntityAnimationPresenter) doesn't drain and apply that queued transition for several frames, so the falling pose visibly persists even though the underlying MotionInterpreter state is already correct. Discriminator: gravitySet=true, hasDefaultSink=true, sequencer seqStyle/seqMotion at the landing edge necessarily still reads the pre-landing (Falling) values (the read happens immediately before HitGround runs, so this alone doesn't distinguish success from H3) — confirming/refuting H3 requires cross-referencing the SAME guid's subsequent [remote-landing]/VU.land/ACDREAM_DUMP_MOTION SetCycle lines over the next several frames to see whether the cycle swap is applied promptly or lags.

Decision table for tomorrow's capture

Run with ACDREAM_PROBE_REMOTE_LANDING=1 (pair with ACDREAM_DUMP_MOTION=1 for the existing VU.land/SetCycle lines) across a route 4a session that reproduces Bug A, then read the first [remote-landing] line for the affected guid at its landing edge:

gravitySet hasDefaultSink Falling pose clears next frame? Implicates
false (+ [remote-landing-gate] NOOP) H1 — fix where Gravity gets cleared/never-set before this landing edge
true false H2 — fix the sink-binding race (bind before the landing block runs, or defer the landing block until a sink exists)
true true No — lags several frames in the VU.land/SetCycle trail H3 — fix the animation-scheduler consumption path, not physics
true true Yes Landing worked correctly on this instance — Bug A did not reproduce here; re-run to catch the failing case

Whichever row fires, the fix belongs in a DIFFERENT file/method than the other two rows, so this table should be read for exactly one row per capture before deciding where to touch code — the whole point of the probe was to avoid fixing all three speculatively.