docs(motion): #42 root cause confirmed — ResolveWithTransition airborne drift

A/B-tested 2026-05-05 with user observing retail-controlled remote:

  - With CellId fix removed: jumps render with geometrically-correct
    XY (no drift) but body falls through the floor.
  - With CellId fix applied: jumps land cleanly but arc shows ~1 m
    horizontal offset; snaps back on next UM.

Confirms the drift originates inside ResolveWithTransition, not from
wire data, local Euler error, or stale velocity. CellId fix kept in
place because falling through the floor is more disruptive than
~1 m visual jitter that resolves on next input.

#42 updated with the verified diagnosis, three ranked-by-probability
hypotheses for the in-sweep mechanism (initial-overlap depenetration
along non-+Z terrain normal is the leading candidate), three matching
fix paths, and a deterministic repro recipe for the next session.

The right next step is investigating PhysicsEngine.ResolveWithTransition
and comparing against retail's CTransition::find_valid_position
(docs/research/named-retail/) — out of scope for the L.3 motion port,
files as a follow-up PhysicsEngine bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-05 15:47:40 +02:00
parent c26bbbb84e
commit b37b7137f6
2 changed files with 97 additions and 44 deletions

View file

@ -127,9 +127,30 @@ the same direction. Add a `LastUMUpdateTime` grace window (e.g.
## #42 — Airborne XY drift on observed player remote jumps (~1 m horizontal offset over arc)
**Status:** OPEN
**Severity:** MEDIUM (pre-existing; exposed by L.3 M2 airborne UP no-op)
**Filed:** 2026-05-05
**Component:** physics / motion (airborne local-integration)
**Severity:** MEDIUM (pre-existing PhysicsEngine bug; exposed by L.3 M2 airborne UP no-op + M4 CellId fix)
**Filed:** 2026-05-05 (root cause confirmed same day)
**Component:** physics (`PhysicsEngine.ResolveWithTransition` airborne behavior)
**Root cause (verified 2026-05-05 via A/B test):**
`ResolveWithTransition` running per-tick during the airborne arc is the
source of the drift. Verified by A/B-toggling the M4 CellId fix
(`rmState.CellId = p.LandblockId`) which is the gate that lets the
sweep run for player-remote jumps:
- **CellId line removed** → sweep skipped → jumps render with
geometrically-correct XY (no drift) but body falls through the
floor (no terrain catch).
- **CellId line present** → sweep runs → jumps land correctly but
arc shows ~1 m horizontal offset from actor's actual XY; body
snaps back on next inbound UM.
So the drift originates inside `ResolveWithTransition` itself, not
from wire data, not from local Euler integration, not from stale
velocity. Decision recorded in commit history: kept CellId fix in
production code so jumps land (`fall-through-floor` is more disruptive
to gameplay than `~1m visual jitter that resolves on next input`).
This issue tracks the proper fix.
**Description:** When observing a retail-controlled remote that jumps
in place (no horizontal input), the visible jump arc renders with
@ -175,57 +196,91 @@ accumulated local-integration drift. The drift is pre-existing — the
user reports having seen it before — but is now visible for the
full arc duration instead of being corrected every ~200 ms.
**Suspected sources of XY drift on a stationary jump:**
**Likely mechanism (ranked by probability):**
1. **ACE wire VectorUpdate may have non-trivial XY components** even
when the actor is standing still. `OnLiveVectorUpdated` (line
3235) sets `rm.Body.Velocity = update.Velocity` verbatim; no
filtering. Worth instrumenting `[VU.WIRE]` to confirm wire XY for
stationary jumps.
1. **Initial-overlap depenetration along non-+Z terrain normal** — at
jump start the collision sphere is touching the floor at body Z.
Most outdoor terrain triangles are not perfectly horizontal — their
normals have a small horizontal component. The sweep's first action
each tick is to resolve overlap by separating the sphere along the
contact normal; on a tilted terrain triangle that separation has
horizontal magnitude. The body gets shoved sideways the first frame
of the jump and the rest of the arc carries that initial drift.
Direction-correlation with terrain orientation would confirm
(test in different landblocks; if drift direction varies with the
slope of the launch tile, this is it).
2. **Pre-jump `rm.Body.Velocity` residual** — should be zero for the
M3 grounded path (cleared each tick at line 6118 area), but worth
confirming via diag.
2. **Step-down probe firing despite `isOnGround: false`** — sweep's
internal "search for nearest walkable surface" might still scan
horizontally during airborne ticks even when we pass `isOnGround:
!rm.Airborne` (= false for airborne). Check whether the
`stepUpHeight` / `stepDownHeight` parameters are unconditionally
used inside `ResolveWithTransition` regardless of the
`isOnGround` flag.
3. **EdgeSlide during sphere sweep** — if `ResolveWithTransition`
catches an edge mid-arc (unlikely for a stationary in-place jump
but possible), the sweep could push the body horizontally.
4. **Render-rate-dependent dt** — local Euler integration uses
`Silk.NET.OnRender(double deltaSeconds)` raw; retail clamps to
30 Hz. Sub-tick error accumulates over a 2 s arc.
3. **EdgeSlide on near-vertical motion against a near-vertical
surface** — if the sphere even slightly grazes a wall while
ascending or descending, EdgeSlide projects motion tangent to the
wall, redirecting some Z velocity into XY. Less likely for
open-ground stationary jumps but could explain drift near
buildings.
**Fix paths:**
a. **Pragmatic** (revert to legacy behavior): hard-snap
`rm.Body.Position = worldPos` on airborne UPs too. Diverges from
retail spec, but ACE behavior diverges from retail too (ACE
broadcasts mid-arc UPs while retail apparently doesn't). Masks
the drift identically to pre-M2. Lowest-risk visual fix.
a. **Skip initial-overlap depenetration when airborne** — gate the
"separate from initial contact plane" step inside
`ResolveWithTransition` on `isOnGround: true`. Trusts the previous
tick's resolve to have left the body in a non-overlapping position.
This is the most likely-correct fix if hypothesis (1) is right.
b. **Investigate** the actual XY source via VU/UP instrumentation
and `body.Velocity` snapshots, then fix the root cause. May be
an ACE-specific velocity-quirk we should clamp at
`OnLiveVectorUpdated`, or a clock-source mismatch in our Euler.
b. **Zero step-up/down for airborne sweeps** — pass
`stepUpHeight: 0f, stepDownHeight: 0f` when `rm.Airborne`. Kills
hypothesis (2) without other side effects (airborne bodies don't
step anyway).
c. **Hybrid**: keep airborne no-op for body.Position translation but
re-introduce a soft-correction on mid-arc UPs (server-position-
biased lerp) — slowly pulls body toward server truth without
the rubber-band.
c. **Stripped airborne sweep** — replace the full sphere sweep with
a simpler vertical sphere-vs-terrain intersection + wall-collision
stop. Loses some retail fidelity but eliminates all three
mechanisms. Probably overkill if (a) or (b) suffices.
**Files:**
- `src/AcDream.App/Rendering/GameWindow.cs` `OnLiveVectorUpdated`
L3228+ (sets velocity from wire verbatim)
- `src/AcDream.App/Rendering/GameWindow.cs` `OnLivePositionUpdated`
player-remote airborne no-op L3502+ (the no-op that exposed this)
- `src/AcDream.App/Rendering/GameWindow.cs` legacy airborne TickAnimations
L6478+ (gravity integration via UpdatePhysicsInternal)
- `src/AcDream.Core/Physics/PhysicsEngine.cs`
`ResolveWithTransition` and any internal `CTransition` /
`find_valid_position` helpers. The initial-overlap depenetration
path is the primary investigation target.
- `src/AcDream.App/Rendering/GameWindow.cs:6478+` (legacy airborne
TickAnimations, the call site) — reference only; not the bug.
**Reference:**
Retail equivalent at
`docs/research/named-retail/acclient_2013_pseudo_c.txt`:
- `CTransition::find_valid_position` (called from `transition()`)
- `SpherePath` initialization
- The verbatim retail depenetration logic for airborne bodies
If our port differs from retail in this region, that diff is likely
the bug.
**Repro:**
1. Launch acdream + retail client side-by-side connected to local ACE.
2. Have retail char stand still on outdoor terrain at any position X.
3. Jump in place.
4. Observe acdream window: arc renders ~1 m offset from X, lands
offset, snaps back on next UM.
To verify the depenetration hypothesis specifically, repeat the jump
in different landblock spots — drift direction should correlate with
the local terrain normal, not the actor's facing.
**Acceptance:**
- Visual jump arc + landing render at the actor's actual XY position,
no perceptible horizontal offset, no snap-back on next UM/UP.
no perceptible horizontal offset, no snap-back on next UM.
- Wall-collision airborne (jumping into building doorways, jumping
puzzles) still works — fix must not strip collision wholesale.
---