using System;
using System.Buffers.Binary;
using AcDream.Core.Net.Messages;
using Xunit;
namespace AcDream.Core.Net.Tests.Messages;
///
/// Covers — 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.
///
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()));
}
[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|0x10 = 0x13)
// 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), 0x13u); 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 we don't parse.
// 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++] = 1; // movementType = MoveToObject (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);
}
}