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:
parent
4dd8d4b46e
commit
9812965183
6 changed files with 246 additions and 26 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,9 +34,21 @@ public static class ServerControlledLocomotion
|
|||
|
||||
// Retail MoveToManager::BeginMoveForward -> MovementParameters::get_command
|
||||
// (0x0052AA00) seeds forward motion before the next position update.
|
||||
public static LocomotionCycle PlanMoveToStart()
|
||||
public static LocomotionCycle PlanMoveToStart(
|
||||
float moveToSpeed = 1f,
|
||||
float runRate = 1f,
|
||||
bool canRun = true)
|
||||
{
|
||||
return new LocomotionCycle(MotionCommand.RunForward, 1f, true);
|
||||
moveToSpeed = SanitizePositive(moveToSpeed);
|
||||
runRate = SanitizePositive(runRate);
|
||||
|
||||
if (!canRun)
|
||||
return new LocomotionCycle(MotionCommand.WalkForward, moveToSpeed, true);
|
||||
|
||||
return new LocomotionCycle(
|
||||
MotionCommand.RunForward,
|
||||
moveToSpeed * runRate,
|
||||
true);
|
||||
}
|
||||
|
||||
public static LocomotionCycle PlanFromVelocity(Vector3 worldVelocity)
|
||||
|
|
@ -67,4 +79,9 @@ public static class ServerControlledLocomotion
|
|||
uint Motion,
|
||||
float SpeedMod,
|
||||
bool IsMoving);
|
||||
|
||||
private static float SanitizePositive(float value)
|
||||
{
|
||||
return float.IsFinite(value) && value > 0f ? value : 1f;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue