Port retail's three-stamp inbound gate for 0xF74C UpdateMotion: - MotionSequenceGate (Core/Physics): CPhysicsObj::is_newer (0x00451ad0) wraparound u16 compare, verbatim per ACE PhysicsObj.is_newer (the BN pseudo-C setcc returns are garbled; ACE + branch structure are the oracle). Gates: INSTANCE_TS at dispatch (stale incarnation drops before any stamp is touched), MOVEMENT_TS strictly-newer (stamped BEFORE the server-control check, per CPhysics::SetObjectMovement 0x00509690), SERVER_CONTROLLED_MOVE_TS drop-when-stored-newer. - Seed from CreateObject's PhysicsDesc timestamp block (index 1 = ObjectMovement now parsed; ACE WorldObject_Networking.cs:411-420 order) — without seeding, entities whose movement sequence is past 0x8000 at spawn would drop every UM against a zero stamp. Adopt-on-first / advance-only-after, so the #138 rehydrate replay of retained spawns cannot regress live stamps. - UpdateMotion + EntitySpawn now carry instance/movement/serverControl sequences + isAutonomous (was parsed-past; isAutonomous feeds the S2 funnel's last_move_was_autonomous). Gate wired at the top of OnLiveMotionUpdated before any state mutation; [UM_STALE] diag under ACDREAM_DUMP_MOTION / ACDREAM_REMOTE_VEL_DIAG; gate dropped with the entity on DeleteObject. Register: AD-32 added (adopt-newer-incarnation instead of retail's QueueBlobForObject); TS-26 updated (UM side closed, UP side open). Deviation map: docs/research/2026-07-02-inbound-motion-deviation-map.md. 19 new gate tests + parser coverage; full suite 3276 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
385 lines
19 KiB
C#
385 lines
19 KiB
C#
using System;
|
||
using System.Buffers.Binary;
|
||
using AcDream.Core.Net.Messages;
|
||
using Xunit;
|
||
|
||
namespace AcDream.Core.Net.Tests.Messages;
|
||
|
||
/// <summary>
|
||
/// Covers <see cref="UpdateMotion.TryParse"/> — the 0xF74C GameMessage the
|
||
/// server sends when an entity's motion state changes (NPC starts walking,
|
||
/// creature enters combat, door opens, etc). The parser shares the inner
|
||
/// MovementData decoder with CreateObject but reaches it through a
|
||
/// different outer layout, so we need standalone coverage.
|
||
/// </summary>
|
||
public class UpdateMotionTests
|
||
{
|
||
[Fact]
|
||
public void RejectsWrongOpcode()
|
||
{
|
||
var body = new byte[32];
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body, 0xDEADBEEFu);
|
||
Assert.Null(UpdateMotion.TryParse(body));
|
||
}
|
||
|
||
[Fact]
|
||
public void RejectsTruncated()
|
||
{
|
||
Assert.Null(UpdateMotion.TryParse(new byte[3]));
|
||
Assert.Null(UpdateMotion.TryParse(Array.Empty<byte>()));
|
||
}
|
||
|
||
[Fact]
|
||
public void ParsesStanceOnly_WhenForwardCommandFlagUnset()
|
||
{
|
||
// Layout:
|
||
// u32 opcode = 0xF74C
|
||
// u32 guid
|
||
// u16 instanceSeq
|
||
// u16 movementSeq + u16 serverControlSeq + u8 isAutonomous + 1 pad (= 6 bytes total header, per ACE Align())
|
||
// u8 movementType = 0 (Invalid)
|
||
// u8 motionFlags = 0
|
||
// u16 currentStyle (outer MovementData field) = 0x0042
|
||
// u32 packed = CurrentStyle flag (0x1) only
|
||
// u16 inner currentStyle = 0x0005 (overrides outer per InterpretedMotionState semantics)
|
||
var body = new byte[4 + 4 + 2 + 6 + 4 + 4 + 2];
|
||
int p = 0;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xF74Cu); p += 4;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x12345678u); p += 4;
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x0001); p += 2;
|
||
// 8-byte header slot — leave zero
|
||
p += 6;
|
||
body[p++] = 0; // movementType = Invalid
|
||
body[p++] = 0; // motionFlags
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x0042); p += 2;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x1u); p += 4; // flags = CurrentStyle only
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x0005); p += 2;
|
||
|
||
var result = UpdateMotion.TryParse(body);
|
||
Assert.NotNull(result);
|
||
Assert.Equal(0x12345678u, result!.Value.Guid);
|
||
Assert.Equal((ushort)0x0005, result.Value.MotionState.Stance);
|
||
Assert.Null(result.Value.MotionState.ForwardCommand);
|
||
}
|
||
|
||
[Fact]
|
||
public void ParsesStanceAndForwardCommand()
|
||
{
|
||
// Flags = CurrentStyle (0x1) | ForwardCommand (0x2)
|
||
var body = new byte[4 + 4 + 2 + 6 + 4 + 4 + 2 + 2];
|
||
int p = 0;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xF74Cu); p += 4;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xABCDEF01u); p += 4;
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x0010); p += 2;
|
||
p += 6; // MovementData header slot
|
||
body[p++] = 0;
|
||
body[p++] = 0;
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x0000); p += 2; // outer style = 0
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x3u); p += 4; // CurrentStyle + ForwardCommand
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x000D); p += 2; // stance = 0xD
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x0007); p += 2; // forward command = 0x7 (Run)
|
||
|
||
var result = UpdateMotion.TryParse(body);
|
||
Assert.NotNull(result);
|
||
Assert.Equal(0xABCDEF01u, result!.Value.Guid);
|
||
Assert.Equal((ushort)0x000D, result.Value.MotionState.Stance);
|
||
Assert.Equal((ushort)0x0007, result.Value.MotionState.ForwardCommand);
|
||
}
|
||
|
||
[Fact]
|
||
public void ParsesNoFlagsSet_KeepsOuterStance()
|
||
{
|
||
// When the InterpretedMotionState flags are zero, neither the inner
|
||
// currentStyle nor the forward command are present in the payload,
|
||
// so the parser should fall back to the MovementData outer stance
|
||
// field and leave ForwardCommand null.
|
||
var body = new byte[4 + 4 + 2 + 6 + 4 + 4];
|
||
int p = 0;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xF74Cu); p += 4;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x55555555u); p += 4;
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||
p += 6;
|
||
body[p++] = 0;
|
||
body[p++] = 0;
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x00AA); p += 2;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0u); p += 4; // no flags
|
||
|
||
var result = UpdateMotion.TryParse(body);
|
||
Assert.NotNull(result);
|
||
Assert.Equal((ushort)0x00AA, result!.Value.MotionState.Stance);
|
||
Assert.Null(result.Value.MotionState.ForwardCommand);
|
||
}
|
||
|
||
[Fact]
|
||
public void ParsesForwardSpeed_WhenSpeedFlagSet()
|
||
{
|
||
// Flags = CurrentStyle | ForwardCommand | ForwardSpeed
|
||
// = 0x1 | 0x2 | 0x4 = 0x7
|
||
// (Per ACE MovementStateFlag enum — ForwardSpeed is bit 0x4,
|
||
// NOT 0x10. The earlier test had the wrong mapping; see
|
||
// references/ACE/Source/ACE.Entity/Enum/MovementStateFlag.cs)
|
||
// Test value: 1.5× speed — matches a typical RunRate broadcast.
|
||
var body = new byte[4 + 4 + 2 + 6 + 4 + 4 + 2 + 2 + 4];
|
||
int p = 0;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xF74Cu); p += 4;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x1A2B3C4Du); p += 4;
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||
p += 6; // MovementData header
|
||
body[p++] = 0;
|
||
body[p++] = 0;
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x7u); p += 4;
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x003D); p += 2; // NonCombat
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x0007); p += 2; // RunForward
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 1.5f); p += 4; // speed
|
||
|
||
var result = UpdateMotion.TryParse(body);
|
||
Assert.NotNull(result);
|
||
Assert.Equal((ushort)0x003D, result!.Value.MotionState.Stance);
|
||
Assert.Equal((ushort)0x0007, result.Value.MotionState.ForwardCommand);
|
||
Assert.Equal(1.5f, result.Value.MotionState.ForwardSpeed);
|
||
}
|
||
|
||
[Fact]
|
||
public void ParsesCommandsList_Wave()
|
||
{
|
||
// A typical NPC wave broadcast:
|
||
// - stance NonCombat (0x003D)
|
||
// - ForwardCommand flag set, command = 0x0003 (Ready)
|
||
// - numCommands = 1, with a single MotionItem{ cmd=0x0087 Wave, seq=0, speed=1.0 }
|
||
//
|
||
// Packed u32 = (flags | numCommands << 7)
|
||
// flags = 0x01 (CurrentStyle) | 0x02 (ForwardCommand) = 0x03
|
||
// numCommands << 7 = 1 << 7 = 0x80
|
||
// total = 0x83
|
||
var body = new byte[4 + 4 + 2 + 6 + 4 + 4 + 2 + 2 + 8];
|
||
int p = 0;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xF74Cu); p += 4;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xDEADBEEFu); p += 4;
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||
p += 6;
|
||
body[p++] = 0;
|
||
body[p++] = 0;
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x83u); p += 4; // flags=0x3 + numCommands=1
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x003D); p += 2; // stance
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x0003); p += 2; // fwd cmd = Ready
|
||
|
||
// MotionItem: u16 command + u16 packedSeq + f32 speed
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x0087); p += 2; // Wave
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x0001); p += 2;
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 1.0f); p += 4;
|
||
|
||
var result = UpdateMotion.TryParse(body);
|
||
Assert.NotNull(result);
|
||
Assert.Equal((ushort)0x003D, result!.Value.MotionState.Stance);
|
||
Assert.Equal((ushort)0x0003, result.Value.MotionState.ForwardCommand);
|
||
|
||
Assert.NotNull(result.Value.MotionState.Commands);
|
||
Assert.Single(result.Value.MotionState.Commands!);
|
||
var wave = result.Value.MotionState.Commands![0];
|
||
Assert.Equal((ushort)0x0087, wave.Command);
|
||
Assert.Equal(1.0f, wave.Speed);
|
||
}
|
||
|
||
[Fact]
|
||
public void HandlesNonInvalidMovementType_GracefullyReturnsOuterStance()
|
||
{
|
||
// movementType != 0 means one of the Move* variants; a truncated
|
||
// non-Invalid payload still returns the outer state.
|
||
// The parser must still return a valid Parsed with the outer stance
|
||
// and a null ForwardCommand rather than failing the whole message.
|
||
var body = new byte[4 + 4 + 2 + 6 + 4];
|
||
int p = 0;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xF74Cu); p += 4;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x99999999u); p += 4;
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||
p += 6;
|
||
body[p++] = 7; // movementType = MoveToPosition (non-Invalid)
|
||
body[p++] = 0;
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x00CC); p += 2;
|
||
|
||
var result = UpdateMotion.TryParse(body);
|
||
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);
|
||
}
|
||
|
||
[Fact]
|
||
public void ParsesMoveToPositionSpeedAndRunRate()
|
||
{
|
||
// Layout after MovementData's movementType/motionFlags/currentStyle:
|
||
// Origin: cell + xyz (16 bytes)
|
||
// MoveToParameters: flags, distance, min, fail, speed,
|
||
// walk/run threshold, desired heading (28 bytes)
|
||
// runRate: f32
|
||
var body = new byte[4 + 4 + 2 + 6 + 4 + 16 + 28 + 4];
|
||
int p = 0;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xF74Cu); p += 4;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x80001234u); p += 4;
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||
p += 6;
|
||
body[p++] = 7; // MoveToPosition
|
||
body[p++] = 0;
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x003D); p += 2;
|
||
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xA8B4000Eu); p += 4;
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 10f); p += 4;
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 20f); p += 4;
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 30f); p += 4;
|
||
|
||
const uint canWalkCanRunMoveTowards = 0x1u | 0x2u | 0x200u;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), canWalkCanRunMoveTowards); p += 4;
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 0.6f); p += 4;
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 0.0f); p += 4;
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), float.MaxValue); p += 4;
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 1.25f); p += 4;
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 15.0f); p += 4;
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 90.0f); p += 4;
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 1.5f); p += 4;
|
||
|
||
var result = UpdateMotion.TryParse(body);
|
||
|
||
Assert.NotNull(result);
|
||
Assert.Equal((byte)7, result!.Value.MotionState.MovementType);
|
||
Assert.True(result.Value.MotionState.IsServerControlledMoveTo);
|
||
Assert.Equal((ushort)0x003D, result.Value.MotionState.Stance);
|
||
Assert.Null(result.Value.MotionState.ForwardCommand);
|
||
Assert.Equal(canWalkCanRunMoveTowards, result.Value.MotionState.MoveToParameters);
|
||
Assert.Equal(1.25f, result.Value.MotionState.MoveToSpeed);
|
||
Assert.Equal(1.5f, result.Value.MotionState.MoveToRunRate);
|
||
Assert.True(result.Value.MotionState.MoveToCanRun);
|
||
Assert.True(result.Value.MotionState.MoveTowards);
|
||
|
||
// Phase L.1c (2026-04-28): full path payload retained.
|
||
Assert.NotNull(result.Value.MotionState.MoveToPath);
|
||
var path = result.Value.MotionState.MoveToPath!.Value;
|
||
Assert.Null(path.TargetGuid);
|
||
Assert.Equal(0xA8B4000Eu, path.OriginCellId);
|
||
Assert.Equal(10f, path.OriginX);
|
||
Assert.Equal(20f, path.OriginY);
|
||
Assert.Equal(30f, path.OriginZ);
|
||
Assert.Equal(0.6f, path.DistanceToObject);
|
||
Assert.Equal(0.0f, path.MinDistance);
|
||
Assert.Equal(float.MaxValue, path.FailDistance);
|
||
Assert.Equal(15.0f, path.WalkRunThreshold);
|
||
Assert.Equal(90.0f, path.DesiredHeading);
|
||
}
|
||
|
||
[Fact]
|
||
public void ParsesAttackHigh1_AsActionForwardCommand()
|
||
{
|
||
// Phase L.1c followup (2026-04-28): regression that verifies the
|
||
// wire-format ACE uses for melee swings — mt=0 with
|
||
// ForwardCommand=AttackHigh1 (0x0062 in low 16 bits) and
|
||
// ForwardSpeed (typically the animSpeed). The receiver in
|
||
// GameWindow.OnLiveMotionUpdated relies on this layout to bulk-copy
|
||
// ForwardCommand into the body's InterpretedState so that
|
||
// get_state_velocity returns 0 (gate is RunForward||WalkForward).
|
||
var body = new byte[4 + 4 + 2 + 6 + 4 + 4 + 2 + 4];
|
||
int p = 0;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xF74Cu); p += 4;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x800003B5u); p += 4;
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||
p += 6; // header padding
|
||
|
||
body[p++] = 0; // mt = Invalid (interpreted)
|
||
body[p++] = 0; // motion_flags
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x003C); p += 2; // stance: HandCombat
|
||
|
||
// InterpretedMotionState: flags = ForwardCommand (0x02) | ForwardSpeed (0x04)
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x06u); p += 4;
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x0062); p += 2; // AttackHigh1 low bits
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 1.25f); p += 4; // animSpeed
|
||
|
||
var result = UpdateMotion.TryParse(body);
|
||
|
||
Assert.NotNull(result);
|
||
Assert.Equal((byte)0, result!.Value.MotionState.MovementType);
|
||
Assert.False(result.Value.MotionState.IsServerControlledMoveTo);
|
||
Assert.Equal((ushort)0x0062, result.Value.MotionState.ForwardCommand);
|
||
Assert.Equal(1.25f, result.Value.MotionState.ForwardSpeed);
|
||
}
|
||
|
||
[Fact]
|
||
public void ParsesSequenceNumbersAndAutonomyFlag()
|
||
{
|
||
// L.2g S1 (DEV-6): the three staleness stamps + autonomy flag must
|
||
// survive parsing — retail gates every 0xF74C on them
|
||
// (INSTANCE_TS at dispatch, MOVEMENT_TS + SERVER_CONTROLLED_MOVE_TS
|
||
// in CPhysics::SetObjectMovement 0x00509690, which also stores
|
||
// last_move_was_autonomous).
|
||
var body = new byte[4 + 4 + 2 + 6 + 4 + 4];
|
||
int p = 0;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xF74Cu); p += 4;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x50001234u); p += 4;
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x0102); p += 2; // instanceSeq
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x0304); p += 2; // movementSeq
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x0506); p += 2; // serverControlSeq
|
||
body[p++] = 1; // isAutonomous
|
||
p += 1; // Align(4) pad
|
||
body[p++] = 0; // movementType = Invalid
|
||
body[p++] = 0; // motionFlags
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x003D); p += 2; // outer stance
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0u); p += 4; // no IMS flags
|
||
|
||
var result = UpdateMotion.TryParse(body);
|
||
|
||
Assert.NotNull(result);
|
||
Assert.Equal((ushort)0x0102, result!.Value.InstanceSequence);
|
||
Assert.Equal((ushort)0x0304, result.Value.MovementSequence);
|
||
Assert.Equal((ushort)0x0506, result.Value.ServerControlSequence);
|
||
Assert.True(result.Value.IsAutonomous);
|
||
}
|
||
|
||
[Fact]
|
||
public void ParsesMoveToObjectTargetGuidAndOrigin()
|
||
{
|
||
// Type 6 (MoveToObject) prepends a u32 target guid before the
|
||
// standard Origin + MovementParameters + runRate payload.
|
||
// Body size: 20 (header) + 4 (guid) + 16 (origin) + 28 (params) + 4 (runRate) = 72.
|
||
var body = new byte[20 + 4 + 16 + 28 + 4];
|
||
int p = 0;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xF74Cu); p += 4;
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x80004321u); p += 4;
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0); p += 2;
|
||
p += 6; // MovementData header padding
|
||
|
||
body[p++] = 6; // MoveToObject
|
||
body[p++] = 0;
|
||
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(p), 0x003D); p += 2;
|
||
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0x80001234u); p += 4; // target guid
|
||
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), 0xA8B4000Eu); p += 4; // cell
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 5f); p += 4; // origin x
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 6f); p += 4; // origin y
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 7f); p += 4; // origin z
|
||
|
||
const uint flags = 0x1u | 0x2u | 0x200u; // can_walk | can_run | move_towards
|
||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), flags); p += 4;
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 0.6f); p += 4;
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 0.0f); p += 4;
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), float.MaxValue); p += 4;
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 1.0f); p += 4;
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 15.0f); p += 4;
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 1.57f); p += 4;
|
||
BinaryPrimitives.WriteSingleLittleEndian(body.AsSpan(p), 1.25f); p += 4; // runRate
|
||
|
||
var result = UpdateMotion.TryParse(body);
|
||
|
||
Assert.NotNull(result);
|
||
Assert.Equal((byte)6, result!.Value.MotionState.MovementType);
|
||
Assert.True(result.Value.MotionState.IsServerControlledMoveTo);
|
||
Assert.NotNull(result.Value.MotionState.MoveToPath);
|
||
var path = result.Value.MotionState.MoveToPath!.Value;
|
||
Assert.Equal(0x80001234u, path.TargetGuid);
|
||
Assert.Equal(0xA8B4000Eu, path.OriginCellId);
|
||
Assert.Equal(5f, path.OriginX);
|
||
Assert.Equal(6f, path.OriginY);
|
||
Assert.Equal(7f, path.OriginZ);
|
||
Assert.Equal(1.25f, result.Value.MotionState.MoveToRunRate);
|
||
}
|
||
}
|