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

142 lines
7.8 KiB
Markdown

# 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:
```csharp
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.