fix(anim): Phase L.1c animate server-controlled chase
This commit is contained in:
parent
b96b680a20
commit
7656fe0970
6 changed files with 224 additions and 13 deletions
|
|
@ -536,6 +536,7 @@ public sealed class GameWindow : IDisposable
|
|||
string? Name,
|
||||
AcDream.Core.Items.ItemType ItemType);
|
||||
private static bool IsPlayerGuid(uint guid) => (guid & 0xFF000000u) == 0x50000000u;
|
||||
private const double ServerControlledVelocityStaleSeconds = 0.60;
|
||||
private int _liveSpawnReceived; // diagnostics
|
||||
private int _liveSpawnHydrated;
|
||||
private int _liveDropReasonNoPos;
|
||||
|
|
@ -2037,8 +2038,22 @@ public sealed class GameWindow : IDisposable
|
|||
if (mtable is not null)
|
||||
{
|
||||
sequencer = new AcDream.Core.Physics.AnimationSequencer(setup, mtable, _animLoader);
|
||||
uint seqStyle = stanceOverride is > 0 ? (uint)stanceOverride.Value : (uint)mtable.DefaultStyle;
|
||||
uint seqMotion = commandOverride is > 0 ? (uint)commandOverride.Value : 0x41000003u;
|
||||
uint seqStyle = stanceOverride is > 0
|
||||
? (0x80000000u | (uint)stanceOverride.Value)
|
||||
: (uint)mtable.DefaultStyle;
|
||||
uint seqMotion;
|
||||
if (commandOverride is > 0)
|
||||
{
|
||||
uint resolved = AcDream.Core.Physics.MotionCommandResolver
|
||||
.ReconstructFullCommand(commandOverride.Value);
|
||||
seqMotion = resolved != 0
|
||||
? resolved
|
||||
: (0x40000000u | (uint)commandOverride.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
seqMotion = AcDream.Core.Physics.MotionCommand.Ready;
|
||||
}
|
||||
sequencer.SetCycle(seqStyle, seqMotion);
|
||||
}
|
||||
}
|
||||
|
|
@ -2217,7 +2232,7 @@ public sealed class GameWindow : IDisposable
|
|||
uint seqStyle = ae.Sequencer?.CurrentStyle ?? 0;
|
||||
uint seqMotion = ae.Sequencer?.CurrentMotion ?? 0;
|
||||
Console.WriteLine(
|
||||
$"UM guid=0x{update.Guid:X8} stance=0x{stance:X4} cmd={cmdStr} spd={spd:F2} " +
|
||||
$"UM guid=0x{update.Guid:X8} mt=0x{update.MotionState.MovementType:X2} stance=0x{stance:X4} cmd={cmdStr} spd={spd:F2} " +
|
||||
$"| seq now style=0x{seqStyle:X8} motion=0x{seqMotion:X8}");
|
||||
}
|
||||
|
||||
|
|
@ -2259,9 +2274,18 @@ public sealed class GameWindow : IDisposable
|
|||
// command.Value == 0 → explicit 0 (rare) → Ready
|
||||
// otherwise → resolve class byte and use full cmd
|
||||
uint fullMotion;
|
||||
if (!command.HasValue || command.Value == 0)
|
||||
if ((!command.HasValue || command.Value == 0)
|
||||
&& update.MotionState.IsServerControlledMoveTo)
|
||||
{
|
||||
// MoveTo packets preserve the current cycle until velocity
|
||||
// chooses the visible walk/run/ready state.
|
||||
uint current = ae.Sequencer.CurrentMotion;
|
||||
fullMotion = (current & 0xFF000000u) != 0
|
||||
? current
|
||||
: AcDream.Core.Physics.MotionCommand.Ready;
|
||||
}
|
||||
else if (!command.HasValue || command.Value == 0)
|
||||
{
|
||||
// Stop — return to the style's default substate (Ready).
|
||||
fullMotion = 0x41000003u;
|
||||
}
|
||||
else
|
||||
|
|
@ -2619,6 +2643,34 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private static bool IsRemoteLocomotion(uint motion)
|
||||
{
|
||||
uint low = motion & 0xFFu;
|
||||
return low is 0x05 or 0x06 or 0x07 or 0x0F or 0x10;
|
||||
}
|
||||
|
||||
private void ApplyServerControlledVelocityCycle(
|
||||
uint serverGuid,
|
||||
AnimatedEntity ae,
|
||||
RemoteMotion rm,
|
||||
System.Numerics.Vector3 velocity)
|
||||
{
|
||||
if (IsPlayerGuid(serverGuid)) return;
|
||||
if (rm.Airborne) return;
|
||||
if (ae.Sequencer is null) return;
|
||||
|
||||
var plan = AcDream.Core.Physics.ServerControlledLocomotion
|
||||
.PlanFromVelocity(velocity);
|
||||
uint currentMotion = ae.Sequencer.CurrentMotion;
|
||||
if (!plan.IsMoving && !IsRemoteLocomotion(currentMotion))
|
||||
return;
|
||||
|
||||
uint style = ae.Sequencer.CurrentStyle != 0
|
||||
? ae.Sequencer.CurrentStyle
|
||||
: 0x8000003Du;
|
||||
ae.Sequencer.SetCycle(style, plan.Motion, plan.SpeedMod);
|
||||
}
|
||||
|
||||
private void OnLivePositionUpdated(AcDream.Core.Net.WorldSession.EntityPositionUpdate update)
|
||||
{
|
||||
// Phase A.1: track the most recently updated entity's landblock so the
|
||||
|
|
@ -2789,6 +2841,17 @@ public sealed class GameWindow : IDisposable
|
|||
rmState.Body.Velocity = rmState.ServerVelocity;
|
||||
}
|
||||
|
||||
if (!IsPlayerGuid(update.Guid)
|
||||
&& rmState.HasServerVelocity
|
||||
&& _animatedEntities.TryGetValue(entity.Id, out var aeForVelocity))
|
||||
{
|
||||
ApplyServerControlledVelocityCycle(
|
||||
update.Guid,
|
||||
aeForVelocity,
|
||||
rmState,
|
||||
rmState.ServerVelocity);
|
||||
}
|
||||
|
||||
entity.Position = rmState.Body.Position;
|
||||
entity.Rotation = rmState.Body.Orientation;
|
||||
}
|
||||
|
|
@ -4937,9 +5000,28 @@ public sealed class GameWindow : IDisposable
|
|||
| AcDream.Core.Physics.TransientStateFlags.OnWalkable
|
||||
| AcDream.Core.Physics.TransientStateFlags.Active;
|
||||
if (!IsPlayerGuid(serverGuid) && rm.HasServerVelocity)
|
||||
rm.Body.Velocity = rm.ServerVelocity;
|
||||
{
|
||||
double velocityAge = nowSec - rm.LastServerPosTime;
|
||||
if (velocityAge > ServerControlledVelocityStaleSeconds)
|
||||
{
|
||||
rm.ServerVelocity = System.Numerics.Vector3.Zero;
|
||||
rm.HasServerVelocity = false;
|
||||
rm.Body.Velocity = System.Numerics.Vector3.Zero;
|
||||
ApplyServerControlledVelocityCycle(
|
||||
serverGuid,
|
||||
ae,
|
||||
rm,
|
||||
System.Numerics.Vector3.Zero);
|
||||
}
|
||||
else
|
||||
{
|
||||
rm.Body.Velocity = rm.ServerVelocity;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
rm.Motion.apply_current_movement(cancelMoveTo: false, allowJump: false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -139,7 +139,17 @@ public static class CreateObject
|
|||
ushort? SideStepCommand = null,
|
||||
float? SideStepSpeed = null,
|
||||
ushort? TurnCommand = null,
|
||||
float? TurnSpeed = null);
|
||||
float? TurnSpeed = null,
|
||||
byte MovementType = 0)
|
||||
{
|
||||
/// <summary>
|
||||
/// ACE/retail movement types 6 and 7 are server-controlled
|
||||
/// MoveToObject/MoveToPosition packets. Their union body does not
|
||||
/// carry an InterpretedMotionState.ForwardCommand, so command absence
|
||||
/// is not a stop signal.
|
||||
/// </summary>
|
||||
public bool IsServerControlledMoveTo => MovementType is 6 or 7;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One entry in the InterpretedMotionState's Commands list (MotionItem).
|
||||
|
|
@ -648,7 +658,8 @@ public static class CreateObject
|
|||
|
||||
return new ServerMotionState(
|
||||
currentStyle, forwardCommand, forwardSpeed, commands,
|
||||
sidestepCommand, sidestepSpeed, turnCommand, turnSpeed);
|
||||
sidestepCommand, sidestepSpeed, turnCommand, turnSpeed,
|
||||
movementType);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ public static class UpdateMotion
|
|||
// MovementInvalid branch, just reached via the header'd path.
|
||||
// Includes the Commands list (MotionItem[]) that carries
|
||||
// Actions, emotes, and other one-shots not in ForwardCommand.
|
||||
if (body.Length - pos < 4) return new Parsed(guid, new CreateObject.ServerMotionState(currentStyle, null));
|
||||
if (body.Length - pos < 4) return new Parsed(guid, new CreateObject.ServerMotionState(currentStyle, null, MovementType: movementType));
|
||||
uint packed = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
uint flags = packed & 0x7Fu;
|
||||
|
|
@ -158,13 +158,13 @@ public static class UpdateMotion
|
|||
|
||||
if ((flags & 0x1u) != 0)
|
||||
{
|
||||
if (body.Length - pos < 2) return new Parsed(guid, new CreateObject.ServerMotionState(currentStyle, null));
|
||||
if (body.Length - pos < 2) return new Parsed(guid, new CreateObject.ServerMotionState(currentStyle, null, MovementType: movementType));
|
||||
currentStyle = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
||||
pos += 2;
|
||||
}
|
||||
if ((flags & 0x2u) != 0)
|
||||
{
|
||||
if (body.Length - pos < 2) return new Parsed(guid, new CreateObject.ServerMotionState(currentStyle, null));
|
||||
if (body.Length - pos < 2) return new Parsed(guid, new CreateObject.ServerMotionState(currentStyle, null, MovementType: movementType));
|
||||
forwardCommand = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
||||
pos += 2;
|
||||
}
|
||||
|
|
@ -224,7 +224,8 @@ public static class UpdateMotion
|
|||
|
||||
return new Parsed(guid, new CreateObject.ServerMotionState(
|
||||
currentStyle, forwardCommand, forwardSpeed, commands,
|
||||
sidestepCommand, sidestepSpeed, turnCommand, turnSpeed));
|
||||
sidestepCommand, sidestepSpeed, turnCommand, turnSpeed,
|
||||
movementType));
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
|
|||
63
src/AcDream.Core/Physics/ServerControlledLocomotion.cs
Normal file
63
src/AcDream.Core/Physics/ServerControlledLocomotion.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Chooses the visible locomotion cycle for server-controlled remotes whose
|
||||
/// UpdateMotion packet is a MoveToObject/MoveToPosition union rather than an
|
||||
/// InterpretedMotionState.
|
||||
///
|
||||
/// Retail references:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>
|
||||
/// <c>MovementManager::PerformMovement</c> (0x00524440) dispatches movement
|
||||
/// types 6/7 into <c>MoveToManager::MoveToObject/MoveToPosition</c> instead
|
||||
/// of unpacking an InterpretedMotionState.
|
||||
/// </description></item>
|
||||
/// <item><description>
|
||||
/// <c>MovementParameters::UnPackNet</c> (0x0052AC50) shows MoveTo packets
|
||||
/// carry movement params + run rate, not a ForwardCommand field.
|
||||
/// </description></item>
|
||||
/// <item><description>
|
||||
/// ACE <c>MovementData.Write</c> uses the same movement type union; holtburger
|
||||
/// documents the matching <c>MovementType::MoveToPosition = 7</c>.
|
||||
/// </description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static class ServerControlledLocomotion
|
||||
{
|
||||
public const float StopSpeed = 0.20f;
|
||||
public const float RunThreshold = 1.25f;
|
||||
public const float MinSpeedMod = 0.25f;
|
||||
public const float MaxSpeedMod = 3.00f;
|
||||
|
||||
public static LocomotionCycle PlanFromVelocity(Vector3 worldVelocity)
|
||||
{
|
||||
float horizontalSpeed = MathF.Sqrt(
|
||||
worldVelocity.X * worldVelocity.X +
|
||||
worldVelocity.Y * worldVelocity.Y);
|
||||
|
||||
if (horizontalSpeed < StopSpeed)
|
||||
return new LocomotionCycle(MotionCommand.Ready, 1f, false);
|
||||
|
||||
if (horizontalSpeed < RunThreshold)
|
||||
{
|
||||
float speedMod = Math.Clamp(
|
||||
horizontalSpeed / MotionInterpreter.WalkAnimSpeed,
|
||||
MinSpeedMod,
|
||||
MaxSpeedMod);
|
||||
return new LocomotionCycle(MotionCommand.WalkForward, speedMod, true);
|
||||
}
|
||||
|
||||
return new LocomotionCycle(
|
||||
MotionCommand.RunForward,
|
||||
Math.Clamp(horizontalSpeed / MotionInterpreter.RunAnimSpeed, MinSpeedMod, MaxSpeedMod),
|
||||
true);
|
||||
}
|
||||
|
||||
public readonly record struct LocomotionCycle(
|
||||
uint Motion,
|
||||
float SpeedMod,
|
||||
bool IsMoving);
|
||||
}
|
||||
|
|
@ -194,7 +194,7 @@ public class UpdateMotionTests
|
|||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x99999999u); p += 4;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||||
p += 6;
|
||||
body[p++] = 1; // movementType = MoveToObject (non-Invalid)
|
||||
body[p++] = 7; // movementType = MoveToPosition (non-Invalid)
|
||||
body[p++] = 0;
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x00CC); p += 2;
|
||||
|
||||
|
|
@ -202,5 +202,7 @@ public class UpdateMotionTests
|
|||
Assert.NotNull(result);
|
||||
Assert.Equal((ushort)0x00CC, result!.Value.MotionState.Stance);
|
||||
Assert.Null(result.Value.MotionState.ForwardCommand);
|
||||
Assert.Equal((byte)7, result.Value.MotionState.MovementType);
|
||||
Assert.True(result.Value.MotionState.IsServerControlledMoveTo);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
public sealed class ServerControlledLocomotionTests
|
||||
{
|
||||
[Fact]
|
||||
public void PlanFromVelocity_StopsBelowRetailNoiseThreshold()
|
||||
{
|
||||
var plan = ServerControlledLocomotion.PlanFromVelocity(
|
||||
new Vector3(0.10f, 0.12f, 3.0f));
|
||||
|
||||
Assert.False(plan.IsMoving);
|
||||
Assert.Equal(MotionCommand.Ready, plan.Motion);
|
||||
Assert.Equal(1.0f, plan.SpeedMod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlanFromVelocity_WalksForSlowServerControlledMotion()
|
||||
{
|
||||
var plan = ServerControlledLocomotion.PlanFromVelocity(
|
||||
new Vector3(0.0f, 0.80f, 0.0f));
|
||||
|
||||
Assert.True(plan.IsMoving);
|
||||
Assert.Equal(MotionCommand.WalkForward, plan.Motion);
|
||||
Assert.InRange(plan.SpeedMod, 0.25f, 0.27f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlanFromVelocity_RunsAtRetailRunScale()
|
||||
{
|
||||
var plan = ServerControlledLocomotion.PlanFromVelocity(
|
||||
new Vector3(0.0f, MotionInterpreter.RunAnimSpeed, 0.0f));
|
||||
|
||||
Assert.True(plan.IsMoving);
|
||||
Assert.Equal(MotionCommand.RunForward, plan.Motion);
|
||||
Assert.Equal(1.0f, plan.SpeedMod, precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlanFromVelocity_ClampsVeryFastSnapshots()
|
||||
{
|
||||
var plan = ServerControlledLocomotion.PlanFromVelocity(
|
||||
new Vector3(0.0f, 30.0f, 0.0f));
|
||||
|
||||
Assert.True(plan.IsMoving);
|
||||
Assert.Equal(MotionCommand.RunForward, plan.Motion);
|
||||
Assert.Equal(ServerControlledLocomotion.MaxSpeedMod, plan.SpeedMod);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue