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>
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.