fix(anim): Phase L.1c seed move-to locomotion

Retail MoveToManager::BeginMoveForward calls MovementParameters::get_command (0x0052AA00) and then _DoMotion/adjust_motion, so a server-controlled MoveTo begins visible forward locomotion before the next UpdatePosition echo. Seed RunForward for MoveTo packets that omit ForwardCommand, while preserving active locomotion and letting position velocity refine walk/run/stop.
This commit is contained in:
Erik 2026-04-28 19:48:12 +02:00
parent 7656fe0970
commit 4dd8d4b46e
3 changed files with 36 additions and 7 deletions

View file

@ -2273,16 +2273,30 @@ public sealed class GameWindow : IDisposable
// command == null → retail stop signal → Ready // command == null → retail stop signal → Ready
// command.Value == 0 → explicit 0 (rare) → Ready // command.Value == 0 → explicit 0 (rare) → Ready
// otherwise → resolve class byte and use full cmd // otherwise → resolve class byte and use full cmd
float speedMod = update.MotionState.ForwardSpeed ?? 1f;
uint fullMotion; uint fullMotion;
if ((!command.HasValue || command.Value == 0) if ((!command.HasValue || command.Value == 0)
&& update.MotionState.IsServerControlledMoveTo) && update.MotionState.IsServerControlledMoveTo)
{ {
// MoveTo packets preserve the current cycle until velocity
// chooses the visible walk/run/ready state.
uint current = ae.Sequencer.CurrentMotion; uint current = ae.Sequencer.CurrentMotion;
fullMotion = (current & 0xFF000000u) != 0 if (IsRemoteLocomotion(current))
? current {
: AcDream.Core.Physics.MotionCommand.Ready; // 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;
}
} }
else if (!command.HasValue || command.Value == 0) else if (!command.HasValue || command.Value == 0)
{ {
@ -2313,8 +2327,6 @@ public sealed class GameWindow : IDisposable
// apply_run_to_command). Treating zero as "unspecified / 1.0" // apply_run_to_command). Treating zero as "unspecified / 1.0"
// produces "slow walk that never stops" — exactly what the // produces "slow walk that never stops" — exactly what the
// stop bug looked like. // stop bug looked like.
float speedMod = update.MotionState.ForwardSpeed ?? 1f;
if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1" if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1"
&& update.Guid != _playerServerGuid) && update.Guid != _playerServerGuid)
Console.WriteLine( Console.WriteLine(

View file

@ -32,6 +32,13 @@ public static class ServerControlledLocomotion
public const float MinSpeedMod = 0.25f; public const float MinSpeedMod = 0.25f;
public const float MaxSpeedMod = 3.00f; public const float MaxSpeedMod = 3.00f;
// Retail MoveToManager::BeginMoveForward -> MovementParameters::get_command
// (0x0052AA00) seeds forward motion before the next position update.
public static LocomotionCycle PlanMoveToStart()
{
return new LocomotionCycle(MotionCommand.RunForward, 1f, true);
}
public static LocomotionCycle PlanFromVelocity(Vector3 worldVelocity) public static LocomotionCycle PlanFromVelocity(Vector3 worldVelocity)
{ {
float horizontalSpeed = MathF.Sqrt( float horizontalSpeed = MathF.Sqrt(

View file

@ -6,6 +6,16 @@ namespace AcDream.Core.Tests.Physics;
public sealed class ServerControlledLocomotionTests public sealed class ServerControlledLocomotionTests
{ {
[Fact]
public void PlanMoveToStart_SeedsImmediateRunCycle()
{
var plan = ServerControlledLocomotion.PlanMoveToStart();
Assert.True(plan.IsMoving);
Assert.Equal(MotionCommand.RunForward, plan.Motion);
Assert.Equal(1.0f, plan.SpeedMod);
}
[Fact] [Fact]
public void PlanFromVelocity_StopsBelowRetailNoiseThreshold() public void PlanFromVelocity_StopsBelowRetailNoiseThreshold()
{ {