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

@ -140,7 +140,10 @@ public static class CreateObject
float? SideStepSpeed = null,
ushort? TurnCommand = null,
float? TurnSpeed = null,
byte MovementType = 0)
byte MovementType = 0,
uint? MoveToParameters = null,
float? MoveToSpeed = null,
float? MoveToRunRate = null)
{
/// <summary>
/// ACE/retail movement types 6 and 7 are server-controlled
@ -149,6 +152,9 @@ public static class CreateObject
/// is not a stop signal.
/// </summary>
public bool IsServerControlledMoveTo => MovementType is 6 or 7;
public bool MoveToCanRun => !MoveToParameters.HasValue
|| (MoveToParameters.Value & 0x2u) != 0;
}
/// <summary>
@ -553,6 +559,9 @@ public static class CreateObject
float? sidestepSpeed = null;
ushort? turnCommand = null;
float? turnSpeed = null;
uint? moveToParameters = null;
float? moveToSpeed = null;
float? moveToRunRate = null;
List<MotionItem>? commands = null;
// 0 = Invalid is the only union variant we care about for static
@ -655,15 +664,62 @@ public static class CreateObject
}
done:;
}
else if (movementType is 6 or 7)
{
TryParseMoveToPayload(
mv,
p,
movementType,
out moveToParameters,
out moveToSpeed,
out moveToRunRate);
}
return new ServerMotionState(
currentStyle, forwardCommand, forwardSpeed, commands,
sidestepCommand, sidestepSpeed, turnCommand, turnSpeed,
movementType);
movementType,
moveToParameters,
moveToSpeed,
moveToRunRate);
}
catch
{
return null;
}
}
private static bool TryParseMoveToPayload(
ReadOnlySpan<byte> body,
int pos,
byte movementType,
out uint? movementParameters,
out float? speed,
out float? runRate)
{
movementParameters = null;
speed = null;
runRate = null;
if (movementType == 6)
{
if (body.Length - pos < 4) return false;
pos += 4; // target guid
}
if (body.Length - pos < 16 + 28 + 4) return false;
pos += 16; // Origin
movementParameters = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
pos += 4;
pos += 4; // distanceToObject
pos += 4; // minDistance
pos += 4; // failDistance
speed = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
pos += 4;
pos += 4; // walkRunThreshold
pos += 4; // desiredHeading
runRate = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
return true;
}
}

View file

@ -127,6 +127,9 @@ public static class UpdateMotion
float? sidestepSpeed = null;
ushort? turnCommand = null;
float? turnSpeed = null;
uint? moveToParameters = null;
float? moveToSpeed = null;
float? moveToRunRate = null;
List<CreateObject.MotionItem>? commands = null;
if (movementType == 0)
@ -221,15 +224,69 @@ public static class UpdateMotion
}
done:;
}
else if (movementType is 6 or 7)
{
TryParseMoveToPayload(
body,
pos,
movementType,
out moveToParameters,
out moveToSpeed,
out moveToRunRate);
}
return new Parsed(guid, new CreateObject.ServerMotionState(
currentStyle, forwardCommand, forwardSpeed, commands,
sidestepCommand, sidestepSpeed, turnCommand, turnSpeed,
movementType));
movementType,
moveToParameters,
moveToSpeed,
moveToRunRate));
}
catch
{
return null;
}
}
private static bool TryParseMoveToPayload(
ReadOnlySpan<byte> body,
int pos,
byte movementType,
out uint? movementParameters,
out float? speed,
out float? runRate)
{
movementParameters = null;
speed = null;
runRate = null;
// Retail MovementManager::PerformMovement (0x00524440) consumes
// MoveToObject/MoveToPosition as:
// [object guid, for MoveToObject only]
// Origin(cell + xyz)
// MovementParameters::UnPackNet (0x0052AC50): flags, distance,
// min, fail, speed, walk/run threshold, desired heading
// f32 runRate copied into CMotionInterp::my_run_rate.
if (movementType == 6)
{
if (body.Length - pos < 4) return false;
pos += 4; // target guid
}
if (body.Length - pos < 16 + 28 + 4) return false;
pos += 16; // Origin
movementParameters = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
pos += 4;
pos += 4; // distanceToObject
pos += 4; // minDistance
pos += 4; // failDistance
speed = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
pos += 4;
pos += 4; // walkRunThreshold
pos += 4; // desiredHeading
runRate = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
return true;
}
}