Merge L.3 motion port — queue-only chase for grounded player remotes

Brings the elated-aryabhata-208d5e branch into main. 7 commits implementing
the L.3 retail-faithful remote-entity motion port:

  de129bc  M1  Fresh InterpolationManager port + retail spec
  40d88b9  M2  Queue routing for player-remote UPs + entity-position sync
  2365c8c  M3  Animation root motion fallback for idle queue
  d57ace0  M6  Cleanup — dead fields + stale env-var references
  c26bbbb  M4  Jump-CellId fix + #42 filed
  b37b713      #42 root cause confirmed via A/B test
  5cc2812      Handoff prompt for #42 PhysicsEngine investigation

User-verified visual checks: smooth body chase on running/walking/strafing,
no per-UP rubber-band, no slope staircase, NPCs pathing correctly, jumps
land cleanly. Two follow-up issues filed:

  #41  sub-decimeter steady-state blips (velocity-synthesis residual; LOW)
  #42  airborne XY drift on jumps (PhysicsEngine.ResolveWithTransition
       depenetration; root cause confirmed; deep-dive prompt at
       docs/research/2026-05-05-issue-42-handoff.md)

Replaces the env-var-gated experimental path (ACDREAM_INTERP_MANAGER=1)
which was marked DO-NOT-ENABLE — the env-var no longer toggles anything.
NPCs and airborne player remotes still use the legacy path; only grounded
player remotes route through the new retail-faithful queue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-05 15:51:29 +02:00
commit 086e65dfe6
22 changed files with 11199 additions and 459 deletions

View file

@ -124,7 +124,280 @@ the same direction. Add a `LastUMUpdateTime` grace window (e.g.
- No spurious cycle thrashing during turning while running (ObservedOmega
doesn't trigger velocity-bucket changes).
## #40 — ACDREAM_INTERP_MANAGER=1 env-var path regressed (staircase + blips)
## #42 — Airborne XY drift on observed player remote jumps (~1 m horizontal offset over arc)
**Status:** OPEN
**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
a small horizontal offset from the actor's actual position — typically
~1 m to one side and slightly forward. Body lands at offset position
(~X+1m). On the next inbound UM/UP from the actor (e.g., turning or
moving), the body snaps back to the server's authoritative X.
User report 2026-05-05 (after M4 CellId fix): "I stand at position X
and jump, it looks like im jumping slightly to the left of X like
1m-ish (if I observe jumping char from behind). It also lands at
X + 1m-ish. Position resets to X when I issue some other command
to the client like turning."
**Why it surfaced now:**
Pre-M2 (legacy path), `OnLivePositionUpdated` hard-snapped
`rmState.Body.Position = worldPos` on EVERY UP including mid-arc
airborne ones. ACE broadcasts intermediate UPs at ~510 Hz during
the jump arc with the actor's authoritative mid-arc position;
each snap kept our local body close to server, masking
local-integration error.
L.3 M2 (commit 40d88b9) implemented the retail-spec airborne no-op
in `OnLivePositionUpdated`:
```csharp
if (!update.IsGrounded) {
entity.Position = rmState.Body.Position;
return;
}
```
Per `docs/research/2026-05-04-l3-port/03-up-routing.md` § 3:
> Air branch (`has_contact == 0`): the function falls through to
> `return 0`. This is the "AIRBORNE NO-OP" … The body keeps
> integrating gravity locally; received position is discarded.
This matches retail `MoveOrTeleport @ 0x00516330` semantics. But it
removes the periodic server snapping that was masking ~1 m of
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.
**Likely mechanism (ranked by probability):**
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. **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 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. **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. **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. **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.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.
- Wall-collision airborne (jumping into building doorways, jumping
puzzles) still works — fix must not strip collision wholesale.
---
## #41 — Residual sub-decimeter blips on observed player remotes (M3 baseline)
**Status:** OPEN
**Severity:** LOW (within retail's own DesiredDistance / MinDistance tolerances; visible only on close inspection)
**Filed:** 2026-05-05
**Component:** physics / motion / animation (per-tick remote prediction)
**Description:** With the L.3 M3 path live (queue catch-up + animation
root motion fallback), observed player remotes chase server position
smoothly with NO staircase on slopes and NO per-UP rubber-band. However
small position blips remain — sub-decimeter amplitude, periodic with
the server's UP cadence (~1 Hz). User report 2026-05-05: "I get very
small blips now. Running works, walking works, strafing works."
The blips fall well within retail's own tolerances:
- `DesiredDistance` (queue head reach radius) = 0.05 m
- `MinDistanceToReachPosition` (primary stall threshold) = 0.20 m
So they are NOT a stall trigger and NOT a correctness bug. They're a
visible artifact of the velocity-synthesis residual: anim root motion
(`AnimationSequencer.CurrentVelocity = RunAnimSpeed × adjustedSpeed`)
slightly overshoots server pace between UPs, then queue catch-up walks
the body back toward the server position on the next UP — a small
rubber-band that's smaller than M2's pre-fix version but still
perceptible.
**Root cause hypothesis (untested):**
The L.3 handoff explicitly flagged this. From `06-acdream-audit.md` § 9
and `05-position-manager-and-partarray.md` § 7:
> Our `CurrentVelocity` carries only the steady-state component of the
> cycle's intent; the per-frame stride wobble is gone… For Humanoid
> the dat ships `MotionData.Velocity = 0` so the multiply is a no-op
> anyway — but the synth uses `RunAnimSpeed × adjustedSpeed` directly.
ACE's wire `ForwardSpeed` for a running player is the **server runRate**
(~2.94 for skill 200), not a unit multiplier. Our synth multiplies
`RunAnimSpeed` (4.0) by `adjustedSpeed` (~2.94) = ~11.76 m/s, which
the queue catch-up clamps via `min(catchUp × dt, dist)` but the anim
fallback applies in full when the queue is idle. If the actual
server-broadcast pace is closer to 4.0 m/s (RunAnimSpeed alone, with
runRate as a *frame-rate* multiplier rather than a velocity scalar),
our fallback overshoots by ~3× and the queue walks it back every UP.
Per the handoff: **don't normalize at the wire boundary** (prior
session tried this, called it a hack). The right fix is porting
retail's actual behavior in `add_motion @ 0x005224b0` and
`apply_run_to_command` to determine the correct `CSequence::velocity`
magnitude.
**Files:**
- `src/AcDream.Core/Physics/AnimationSequencer.cs``CurrentVelocity`
synthesis at L614679 (RunAnimSpeed=4.0, WalkAnimSpeed=3.12,
SidestepAnimSpeed=1.25 × adjustedSpeed)
- `src/AcDream.Core/Physics/PositionManager.cs``ComputeOffset`
applies `seqVel × dt × orientation` as fallback when queue is idle
**Research:**
- `docs/research/2026-05-04-l3-port/05-position-manager-and-partarray.md` § 57
- `docs/research/2026-05-04-l3-port/06-acdream-audit.md` § 9 (AnimationSequencer)
- `docs/research/named-retail/acclient_2013_pseudo_c.txt` line 298437
(`add_motion @ 0x005224b0`) — `CSequence::velocity = style_speed × MotionData.velocity`
**Fix path (research first, then port):**
1. cdb-trace retail to capture `CSequence::velocity` and
`MotionData::velocity` for a Humanoid running cycle. Compare against
our synth (4.0 × 2.94 = 11.76 m/s) to determine the actual retail
magnitude.
2. Port `add_motion`'s `style_speed × MotionData.velocity` chain
verbatim. For Humanoid where `MotionData.Velocity = 0`, port the
fallback retail uses (likely a separate code path through
`apply_run_to_command` that derives velocity from the cycle's
framerate, not a constant).
3. Remove the `RunAnimSpeed × adjustedSpeed` synth in
`AnimationSequencer.SetCycle`.
**Acceptance:**
- Visual blips disappear on flat-ground steady-state running.
- Side-by-side acdream-as-observer vs retail-as-observer of the same
server-controlled toon: indistinguishable body trajectory.
---
## #40 — [DONE 2026-05-05 · 40d88b9] ACDREAM_INTERP_MANAGER=1 env-var path regressed (staircase + blips)
**Status:** DONE — closed by L.3 M2 (`feat(motion): L.3 M2 — queue-only chase for grounded player remotes`, commit 40d88b9)
**Resolution:** The env-var gate was retired entirely. Both
`OnLivePositionUpdated` and `TickAnimations` now use
`IsPlayerGuid(serverGuid)` to route player-remote UPs through the
retail-faithful queue path (formerly the env-var path, but with two
key fixes per the L.3 spec):
1. `PositionManager.ComputeOffset` is the per-tick translation source
(REPLACE semantics: queue catch-up overrides anim root motion when
active, anim stands when queue is idle / head reached). Mirrors
retail `UpdatePositionInternal @ 0x00512c30`.
2. `ResolveWithTransition` is **not** called for grounded player
remotes — server already collision-resolved the broadcast position,
and sweeping per-tick on tiny queue catch-up deltas amplified
micro-bounces into visible blips. This was the staircase + blip
regression. Trade-off documented in audit § 6.
User-verified 2026-05-05: smooth body chase, no staircase on slopes,
no per-UP rubber-band on flat ground. Residual sub-decimeter blips
filed separately as #41 (velocity-synthesis magnitude).
**Filed-original-context (for archive):**
**Status:** OPEN (do-not-enable; pending L.3 follow-up rebuild)
**Severity:** N/A (gated; default behavior unaffected)