feat(motion): L.3 M3 — animation root motion fallback for idle queue
Restores PositionManager.ComputeOffset call in TickAnimations player-
remote branch. M2 was queue-only (body chases server but stops between
UPs after head reached); M3 adds the retail REPLACE behavior:
- Queue active and not reached → catch-up vector (REPLACES anim).
- Queue empty or head reached → anim root motion (seqVel × dt rotated
by body.Orientation) drives translation between UPs.
- Blip-to-tail still fires on fail_count > 3.
Mirrors retail UpdatePositionInternal @ 0x00512c30 per
docs/research/2026-05-04-l3-port/05-position-manager-and-partarray.md
§ 6: PositionManager::adjust_offset OVERWRITES local frame's origin
with catch-up when active; otherwise no-op (anim root motion stands).
User-verified 2026-05-05: "Best implementation we have had so far.
Running works, walking works, strafing works."
Closes #40 (env-var path regression — replaced wholesale).
Files #41 for residual sub-decimeter blips: velocity-synthesis magnitude
(RunAnimSpeed × adjustedSpeed) overshoots server pace slightly, queue
walks it back every UP. Within retail's DesiredDistance / MinDistance
tolerances; not a correctness bug. Fix path requires porting
add_motion @ 0x005224b0 and cdb-tracing retail's actual
CSequence::velocity magnitude.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
40d88b92ed
commit
2365c8cd6e
2 changed files with 151 additions and 21 deletions
115
docs/ISSUES.md
115
docs/ISSUES.md
|
|
@ -124,7 +124,120 @@ the same direction. Add a `LastUMUpdateTime` grace window (e.g.
|
||||||
- No spurious cycle thrashing during turning while running (ObservedOmega
|
- No spurious cycle thrashing during turning while running (ObservedOmega
|
||||||
doesn't trigger velocity-bucket changes).
|
doesn't trigger velocity-bucket changes).
|
||||||
|
|
||||||
## #40 — ACDREAM_INTERP_MANAGER=1 env-var path regressed (staircase + blips)
|
## #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 L614–679 (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` § 5–7
|
||||||
|
- `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)
|
**Status:** OPEN (do-not-enable; pending L.3 follow-up rebuild)
|
||||||
**Severity:** N/A (gated; default behavior unaffected)
|
**Severity:** N/A (gated; default behavior unaffected)
|
||||||
|
|
|
||||||
|
|
@ -6071,10 +6071,12 @@ public sealed class GameWindow : IDisposable
|
||||||
{
|
{
|
||||||
if (IsPlayerGuid(serverGuid) && !rm.Airborne)
|
if (IsPlayerGuid(serverGuid) && !rm.Airborne)
|
||||||
{
|
{
|
||||||
// ── L.3 M2 (2026-05-05): queue-only chase for grounded player remotes ──
|
// ── L.3 M2/M3 (2026-05-05): queue + anim chase for grounded player remotes ──
|
||||||
//
|
//
|
||||||
// Per retail spec (docs/research/2026-05-04-l3-port/01-per-tick.md +
|
// Per retail spec (docs/research/2026-05-04-l3-port/01-per-tick.md +
|
||||||
// 04-interp-manager.md):
|
// 04-interp-manager.md +
|
||||||
|
// 05-position-manager-and-partarray.md):
|
||||||
|
//
|
||||||
// - For a grounded REMOTE player, m_velocityVector stays at 0.
|
// - For a grounded REMOTE player, m_velocityVector stays at 0.
|
||||||
// - apply_current_movement is NEVER called per tick on remotes
|
// - apply_current_movement is NEVER called per tick on remotes
|
||||||
// (it's the local-player-only velocity feed).
|
// (it's the local-player-only velocity feed).
|
||||||
|
|
@ -6082,15 +6084,23 @@ public sealed class GameWindow : IDisposable
|
||||||
// velocity² > 0, so it's a no-op when body.Velocity = 0.
|
// velocity² > 0, so it's a no-op when body.Velocity = 0.
|
||||||
// - ResolveWithTransition is NOT called — the server already
|
// - ResolveWithTransition is NOT called — the server already
|
||||||
// collision-resolved the broadcast position.
|
// collision-resolved the broadcast position.
|
||||||
// - Per-tick body translation comes ENTIRELY from
|
// - Per-tick body translation per retail UpdatePositionInternal:
|
||||||
// InterpolationManager::adjust_offset's queue catch-up.
|
// 1. CPartArray::Update writes anim root motion (body-local
|
||||||
// When the queue is empty (head reached, between UPs), the
|
// seqVel × dt) into the local frame.
|
||||||
// body stays put. M3 will add animation root motion to fill
|
// 2. PositionManager::adjust_offset OVERWRITES the local
|
||||||
// the gap so legs match body pace; for M2 the body chases
|
// frame's origin with the queue catch-up vector when
|
||||||
// the server position without anim contribution.
|
// the queue is active and the head is not yet reached
|
||||||
|
// — REPLACE, not additive.
|
||||||
|
// 3. Frame::combine composes the local frame with the
|
||||||
|
// body's world pose.
|
||||||
|
// Net: catch-up replaces anim during the chase phase, anim
|
||||||
|
// stands when the queue is empty / head reached. PositionManager.
|
||||||
|
// ComputeOffset implements this exact REPLACE dichotomy.
|
||||||
//
|
//
|
||||||
// Airborne player remotes (rm.Airborne) and NPCs fall through to
|
// Airborne player remotes (rm.Airborne) and NPCs fall through to
|
||||||
// the legacy path below — unchanged from main per the M2 plan.
|
// the legacy path below — unchanged from main per the M2 plan.
|
||||||
|
System.Numerics.Vector3 seqVel = ae.Sequencer?.CurrentVelocity
|
||||||
|
?? System.Numerics.Vector3.Zero;
|
||||||
System.Numerics.Vector3 seqOmega = ae.Sequencer?.CurrentOmega
|
System.Numerics.Vector3 seqOmega = ae.Sequencer?.CurrentOmega
|
||||||
?? System.Numerics.Vector3.Zero;
|
?? System.Numerics.Vector3.Zero;
|
||||||
|
|
||||||
|
|
@ -6115,21 +6125,28 @@ public sealed class GameWindow : IDisposable
|
||||||
rm.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Active;
|
rm.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Active;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2 (M2): queue-only translation. Direct call to
|
// Step 2 (M3): queue + anim translation via PositionManager.
|
||||||
// InterpolationManager.AdjustOffset — no PositionManager
|
// ComputeOffset returns:
|
||||||
// mixing, no animation root motion. The InterpolationManager
|
// - Vector3.Zero when queue is empty AND seqVel is zero
|
||||||
// returns:
|
// (idle remote between UPs after head reached) — body
|
||||||
// - Vector3.Zero when the queue is empty OR the head is
|
// stays still.
|
||||||
// within DesiredDistance (0.05 m) — body stays still.
|
// - Direction × min(catchUpSpeed × dt, dist) when the
|
||||||
// - Direction × min(catchUpSpeed × dt, dist) — body chases
|
// queue is active and head is not reached — body chases
|
||||||
// the head waypoint at up to 2× motion-table max speed.
|
// the head waypoint at up to 2× motion-table max speed
|
||||||
// - tail − body when fail_count > 3 (stall blip; queue
|
// (REPLACES anim for this frame).
|
||||||
// cleared as a side effect).
|
// - Anim root motion (seqVel × dt rotated into world) when
|
||||||
|
// the queue is empty OR head is within DesiredDistance —
|
||||||
|
// body advances with the locomotion cycle's baked
|
||||||
|
// velocity, keeping legs and body pace synchronized.
|
||||||
|
// - Blip-to-tail (tail − body) when fail_count > 3.
|
||||||
float maxSpeed = rm.Motion.GetMaxSpeed();
|
float maxSpeed = rm.Motion.GetMaxSpeed();
|
||||||
System.Numerics.Vector3 offset = rm.Interp.AdjustOffset(
|
System.Numerics.Vector3 offset = rm.Position.ComputeOffset(
|
||||||
dt: (double)dt,
|
dt: (double)dt,
|
||||||
currentBodyPosition: rm.Body.Position,
|
currentBodyPosition: rm.Body.Position,
|
||||||
maxSpeedFromMinterp: maxSpeed);
|
seqVel: seqVel,
|
||||||
|
ori: rm.Body.Orientation,
|
||||||
|
interp: rm.Interp,
|
||||||
|
maxSpeed: maxSpeed);
|
||||||
rm.Body.Position += offset;
|
rm.Body.Position += offset;
|
||||||
|
|
||||||
// Step 2.5: angular velocity → body orientation. Prefer
|
// Step 2.5: angular velocity → body orientation. Prefer
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue