fix(anim): Phase L.1c match MoveTo run speed

Retail MovementManager::PerformMovement (0x00524440) reads MoveTo speed and runRate from the packet, MovementParameters::UnPackNet (0x0052AC50) defines the layout, and CMotionInterp::apply_run_to_command (0x00527BE0) multiplies RunForward by runRate. Parse those fields for UpdateMotion/CreateObject, seed server-controlled MoveTo locomotion with the retail speed multiplier, and avoid overriding active monster MoveTo with sparse UpdatePosition-derived velocity.
This commit is contained in:
Erik 2026-04-28 20:58:22 +02:00
parent 4dd8d4b46e
commit 9812965183
6 changed files with 246 additions and 26 deletions

View file

@ -224,6 +224,13 @@ public sealed class GameWindow : IDisposable
public System.Numerics.Vector3 ServerVelocity;
public bool HasServerVelocity;
/// <summary>
/// True while a server MoveToObject/MoveToPosition packet is the
/// active locomotion source. Retail runs these through MoveToManager
/// and CMotionInterp using the packet's runRate; deriving velocity
/// from sparse UpdatePosition deltas under-speeds combat chases.
/// </summary>
public bool ServerMoveToActive;
/// <summary>
/// Legacy field — no longer used for slerp (retail hard-snaps
/// per FUN_00514b90 set_frame). Kept to avoid churn.
/// </summary>
@ -2228,7 +2235,9 @@ public sealed class GameWindow : IDisposable
&& update.Guid != _playerServerGuid)
{
string cmdStr = command.HasValue ? $"0x{command.Value:X4}" : "null";
float spd = update.MotionState.ForwardSpeed ?? 0f;
float spd = update.MotionState.ForwardSpeed
?? ((update.MotionState.MoveToSpeed ?? 0f)
* (update.MotionState.MoveToRunRate ?? 0f));
uint seqStyle = ae.Sequencer?.CurrentStyle ?? 0;
uint seqMotion = ae.Sequencer?.CurrentMotion ?? 0;
Console.WriteLine(
@ -2278,25 +2287,19 @@ public sealed class GameWindow : IDisposable
if ((!command.HasValue || command.Value == 0)
&& update.MotionState.IsServerControlledMoveTo)
{
uint current = ae.Sequencer.CurrentMotion;
if (IsRemoteLocomotion(current))
{
// MoveTo packets preserve an active locomotion cycle;
// position velocity will refine the speed.
fullMotion = current;
}
else
{
// Retail MoveToManager::BeginMoveForward calls
// MovementParameters::get_command (0x0052AA00), then
// _DoMotion -> adjust_motion. With default CanRun and
// enough distance, WalkForward + HoldKey_Run becomes
// RunForward immediately, before the next position echo.
var seed = AcDream.Core.Physics.ServerControlledLocomotion
.PlanMoveToStart();
fullMotion = seed.Motion;
speedMod = seed.SpeedMod;
}
// Retail MoveToManager::BeginMoveForward calls
// MovementParameters::get_command (0x0052AA00), then
// _DoMotion -> adjust_motion. With CanRun and enough
// distance, WalkForward + HoldKey_Run becomes RunForward,
// and CMotionInterp::apply_run_to_command (0x00527BE0)
// multiplies speed by the packet's runRate.
var seed = AcDream.Core.Physics.ServerControlledLocomotion
.PlanMoveToStart(
update.MotionState.MoveToSpeed ?? 1f,
update.MotionState.MoveToRunRate ?? 1f,
update.MotionState.MoveToCanRun);
fullMotion = seed.Motion;
speedMod = seed.SpeedMod;
}
else if (!command.HasValue || command.Value == 0)
{
@ -2448,6 +2451,18 @@ public sealed class GameWindow : IDisposable
// FUN_00529210 apply_current_movement
if (_remoteDeadReckon.TryGetValue(update.Guid, out var remoteMot))
{
remoteMot.ServerMoveToActive = update.MotionState.IsServerControlledMoveTo;
if (remoteMot.ServerMoveToActive && !IsPlayerGuid(update.Guid))
{
// Retail MoveTo packets already carry enough state
// for CMotionInterp to drive velocity. A velocity
// inferred from sparse UpdatePosition packets lags
// during combat chases and visibly under-speeds the
// run cycle until the next hard snap.
remoteMot.HasServerVelocity = false;
remoteMot.ServerVelocity = System.Numerics.Vector3.Zero;
}
// Forward axis (Ready / WalkForward / RunForward / WalkBackward).
remoteMot.Motion.DoInterpretedMotion(
fullMotion, speedMod, modifyInterpretedState: true);
@ -2756,6 +2771,7 @@ public sealed class GameWindow : IDisposable
System.Numerics.Vector3? serverVelocity = update.Velocity;
if (serverVelocity is null
&& !IsPlayerGuid(update.Guid)
&& !rmState.ServerMoveToActive
&& rmState.LastServerPosTime > 0.0)
{
double elapsed = nowSec - rmState.LastServerPosTime;
@ -2836,6 +2852,7 @@ public sealed class GameWindow : IDisposable
// carries no stop information for our ACE.
if (svel.LengthSquared() < 0.04f)
{
rmState.ServerMoveToActive = false;
rmState.Motion.StopCompletely();
if (_animatedEntities.TryGetValue(entity.Id, out var aeForStop)
&& aeForStop.Sequencer is not null)