feat(anim): full retail remote-entity motion port — walk/run/strafe/turn/stop
Ports the retail client's client-side remote-entity motion pipeline
verbatim per the decompile research. Every remote now runs its own
PhysicsBody + MotionInterpreter + AnimationSequencer stack — retail has
no special "interpolator" for remotes, it runs the full motion state
machine on every entity. Now we do too.
## What changed
### Parser fixes (CreateObject, UpdateMotion)
Wire flag bits for InterpretedMotionState (per ACE MovementStateFlag enum):
CurrentStyle=0x01, ForwardCommand=0x02, ForwardSpeed=0x04,
SideStepCommand=0x08, SideStepSpeed=0x10, TurnCommand=0x20, TurnSpeed=0x40
Previously we only extracted CurrentStyle + ForwardCommand + ForwardSpeed
and SKIPPED the side/turn fields entirely. Result: we had zero rotation-
or strafe-intent data from the server — impossible to render turn or
sidestep animations. Now ServerMotionState carries all 7 fields and the
parser reads the bytes in ACE's write order (style, fwd, side, turn, then
fwdSpd, sideSpd, turnSpd).
### RemoteMotion (new per-remote struct in GameWindow)
Each remote gets its own PhysicsBody + MotionInterpreter + observed
angular velocity. Replaces the earlier shortcut RemoteInterpolator
(deleted — retail has no such thing).
On UpdateMotion:
- ForwardCommand flag absent → stop signal (reset to Ready) per
retail FUN_0051F260 bulk-copy semantics (absent = Invalid = default).
- Forward + sidestep + turn each route through DoInterpretedMotion,
exactly as retail FUN_00528F70 does.
- Animation cycle selection: forward wins if active, else sidestep,
else turn, else Ready. Matches the user's observation that retail
plays turn animation when only turning.
- Turn command seeds ObservedOmega = π/2 × turnSpeed (from Humanoid
MotionData.Omega.Z ≈ π/2 per decompile).
- Turn absent → ObservedOmega = 0 (stops rotation immediately).
On UpdatePosition:
- Hard-snap Body.Position + Body.Orientation per retail FUN_00514b90
set_frame (direct assignment, no slerp — retail does not soft-snap).
- HasVelocity + |v| < 0.2 → StopCompletely + SetCycle(Ready).
- ForwardSpeed=0 on wire is a VALID stop signal (ACE sends this when
alt releases W); previously we defaulted to 1.0, causing the "slow
walk that never stops" symptom.
Per-tick:
- apply_current_movement → Body.Velocity via get_state_velocity
(retail FUN_00528960: RunAnimSpeed × ForwardSpeed in body-local,
rotated by orientation).
- Manual omega integration: Orientation *= quat(ObservedOmega × dt).
Bypasses PhysicsBody.update_object's MinQuantum=1/30s gate that
was eating every-other-tick rotation updates at our 60fps render
rate — the cause of the persistent "rotation snaps every UP" bug.
- update_object still called for position integration and the motion
subsystem it drives.
### AnimationSequencer synthesis extension
Added omega synthesis for TurnRight/TurnLeft cycles (same pattern as
the earlier velocity synthesis): when the Humanoid dat leaves HasOmega
clear, SetCycle synthesizes CurrentOmega = ±π/2 × speedMod on Z so
dead-reckoning and stop detection can read a non-zero omega for turn
cycles.
### Stop-detection heuristic removed
No more 300ms/2000ms/5000ms idle timers. Retail's stop signal is
explicit (UpdateMotion with ForwardCommand flag absent → Ready); we
handle it directly. Client-side timers were a source of flicker during
normal running.
## Confirmed working
- Walking (matches retail speed + leg cadence)
- Running (matches retail speed + leg cadence)
- Strafing (body moves sideways + strafe animation plays)
- Turning while stationary (body rotates smoothly + turn animation plays)
- Turning while running (body rotates + leg anim continues)
- Stopping (instant stop, no slow-walk tail)
All 717 tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
00c8a4feb5
commit
340dabbc72
5 changed files with 506 additions and 303 deletions
|
|
@ -486,6 +486,34 @@ public sealed class AnimationSequencer
|
|||
if (yvel != 0f || xvel != 0f)
|
||||
CurrentVelocity = new Vector3(xvel, yvel, 0f);
|
||||
}
|
||||
|
||||
// ── Synthesize CurrentOmega for turn cycles ───────────────────────
|
||||
// Same story as velocity synthesis above: Humanoid turn MotionData
|
||||
// often ships without HasOmega. Retail clients turn the body via
|
||||
// the baked omega, but if the dat is silent we fall back to the
|
||||
// retail turn-rate constant. Decompile references:
|
||||
// FUN_00529210 apply_current_movement (writes Omega)
|
||||
// chunk_00520000.c TurnRate globals (~π/2 rad/s for speed=1)
|
||||
// The ACE port uses `omega.z = ±(π/2) × turnSpeed` for right/left
|
||||
// turns (holtburger confirms the same via motion_resolution.rs).
|
||||
if (CurrentOmega.LengthSquared() < 1e-9f)
|
||||
{
|
||||
float zomega = 0f;
|
||||
uint low = motion & 0xFFu;
|
||||
switch (low)
|
||||
{
|
||||
case 0x0D: // TurnRight — clockwise from above = -Z in right-handed.
|
||||
zomega = -(MathF.PI / 2f) * adjustedSpeed;
|
||||
break;
|
||||
case 0x0E: // TurnLeft — counter-clockwise = +Z. adjust_motion
|
||||
// may have remapped 0x0E → 0x0D with negated speed;
|
||||
// in that case the negation preserves correct sign.
|
||||
zomega = (MathF.PI / 2f) * adjustedSpeed;
|
||||
break;
|
||||
}
|
||||
if (zomega != 0f)
|
||||
CurrentOmega = new Vector3(0f, 0f, zomega);
|
||||
}
|
||||
}
|
||||
|
||||
// Retail locomotion constants — mirror of MotionInterpreter.RunAnimSpeed
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue