diff --git a/src/AcDream.Core.Net/Messages/AutonomousPosition.cs b/src/AcDream.Core.Net/Messages/AutonomousPosition.cs
new file mode 100644
index 0000000..dae8c5e
--- /dev/null
+++ b/src/AcDream.Core.Net/Messages/AutonomousPosition.cs
@@ -0,0 +1,88 @@
+using System.Numerics;
+using AcDream.Core.Net.Packets;
+
+namespace AcDream.Core.Net.Messages;
+
+///
+/// Outbound GameAction(AutonomousPosition) message — opcode
+/// 0xF753. Sent roughly every 200ms while the player is moving to
+/// give the server a periodic ground-truth position heartbeat. Simpler than
+/// : no RawMotionState, just the GameAction
+/// envelope, a WorldPosition, sequence numbers, a contact byte, and 4-byte
+/// alignment padding.
+///
+///
+/// Wire layout (ported from
+/// references/holtburger/crates/holtburger-protocol/src/messages/movement/actions.rs
+/// AutonomousPositionActionData::pack):
+///
+///
+/// - GameAction envelope: u32 0xF7B1, u32 sequence, u32 0xF753
+/// - WorldPosition: u32 cellId, f32 x, f32 y, f32 z,
+/// f32 rotW, f32 rotX, f32 rotY, f32 rotZ
+/// - Sequences: u16 instance, u16 serverControl,
+/// u16 teleport, u16 forcePosition
+/// - Contact byte: u8 (1 = on ground, 0 = airborne)
+/// - Align to 4 bytes
+///
+///
+public static class AutonomousPosition
+{
+ public const uint GameActionOpcode = 0xF7B1u;
+ public const uint AutonomousPositionAction = 0xF753u;
+
+ ///
+ /// Build an AutonomousPosition GameAction body.
+ ///
+ /// Monotonically increasing counter from
+ /// .
+ /// Landblock cell ID (u32).
+ /// World-space position relative to the landblock.
+ /// Rotation quaternion. AC wire order is W, X, Y, Z.
+ /// Instance sequence number from the server.
+ /// Server-control sequence number.
+ /// Teleport sequence number.
+ /// Force-position sequence number.
+ /// 1 if the character was last on the ground, 0 if airborne.
+ public static byte[] Build(
+ uint gameActionSequence,
+ uint cellId,
+ Vector3 position,
+ Quaternion rotation,
+ ushort instanceSequence,
+ ushort serverControlSequence,
+ ushort teleportSequence,
+ ushort forcePositionSequence,
+ byte lastContact = 1)
+ {
+ var w = new PacketWriter(64);
+
+ // --- GameAction envelope ---
+ w.WriteUInt32(GameActionOpcode);
+ w.WriteUInt32(gameActionSequence);
+ w.WriteUInt32(AutonomousPositionAction);
+
+ // --- WorldPosition (32 bytes) ---
+ w.WriteUInt32(cellId);
+ w.WriteFloat(position.X);
+ w.WriteFloat(position.Y);
+ w.WriteFloat(position.Z);
+ // Quaternion wire order: W, X, Y, Z
+ w.WriteFloat(rotation.W);
+ w.WriteFloat(rotation.X);
+ w.WriteFloat(rotation.Y);
+ w.WriteFloat(rotation.Z);
+
+ // --- Sequence numbers ---
+ w.WriteUInt16(instanceSequence);
+ w.WriteUInt16(serverControlSequence);
+ w.WriteUInt16(teleportSequence);
+ w.WriteUInt16(forcePositionSequence);
+
+ // --- Contact byte + 4-byte align ---
+ w.WriteByte(lastContact);
+ w.AlignTo4();
+
+ return w.ToArray();
+ }
+}
diff --git a/src/AcDream.Core.Net/Messages/MoveToState.cs b/src/AcDream.Core.Net/Messages/MoveToState.cs
new file mode 100644
index 0000000..5569d25
--- /dev/null
+++ b/src/AcDream.Core.Net/Messages/MoveToState.cs
@@ -0,0 +1,151 @@
+using System.Numerics;
+using AcDream.Core.Net.Packets;
+
+namespace AcDream.Core.Net.Messages;
+
+///
+/// Outbound GameAction(MoveToState) message — opcode 0xF61C.
+/// Sent whenever the client's motion state changes: starting or stopping
+/// walking, switching direction, changing speed, entering or leaving combat
+/// stance. The server uses this to update the player's authoritative motion
+/// state and to drive interpolated position for other nearby clients.
+///
+///
+/// Wire layout (ported from
+/// references/holtburger/crates/holtburger-protocol/src/messages/movement/actions.rs
+/// MoveToStateActionData::pack and
+/// types.rs RawMotionState::pack):
+///
+///
+/// - GameAction envelope: u32 0xF7B1, u32 sequence, u32 0xF61C
+/// - RawMotionState: u32 packed_flags (bits 0-10 = flag bits,
+/// bits 11-31 = command list length), then conditional u32/f32 fields
+/// in flag-bit order (see RawMotionFlags in types.rs)
+/// - WorldPosition: u32 cellId, f32 x, f32 y, f32 z,
+/// f32 rotW, f32 rotX, f32 rotY, f32 rotZ
+/// - Sequences: u16 instance, u16 serverControl,
+/// u16 teleport, u16 forcePosition
+/// - Contact byte: u8 (1 = on ground, 0 = airborne)
+/// - Align to 4 bytes
+///
+///
+///
+/// The command list length is packed into bits 11-31 of the flags dword.
+/// We always send 0 commands (no discrete motion events), so those bits stay 0.
+///
+///
+public static class MoveToState
+{
+ public const uint GameActionOpcode = 0xF7B1u;
+ public const uint MoveToStateAction = 0xF61Cu;
+
+ // RawMotionFlags bit positions (from holtburger types.rs)
+ private const uint FlagCurrentHoldKey = 0x001u;
+ private const uint FlagCurrentStyle = 0x002u;
+ private const uint FlagForwardCommand = 0x004u;
+ private const uint FlagForwardHoldKey = 0x008u;
+ private const uint FlagForwardSpeed = 0x010u;
+ private const uint FlagSidestepCommand = 0x020u;
+ private const uint FlagSidestepHoldKey = 0x040u;
+ private const uint FlagSidestepSpeed = 0x080u;
+ private const uint FlagTurnCommand = 0x100u;
+ private const uint FlagTurnHoldKey = 0x200u;
+ private const uint FlagTurnSpeed = 0x400u;
+
+ ///
+ /// Build a MoveToState GameAction body.
+ ///
+ /// Monotonically increasing counter from
+ /// .
+ /// Raw motion command (u32 in AC's command space),
+ /// e.g. 0x45000005 = WalkForward. Null = no forward command.
+ /// Normalised speed scalar (0.0-1.0). Only written
+ /// when is non-null.
+ /// Sidestep command or null.
+ /// Sidestep speed or null.
+ /// Turn command or null.
+ /// Turn speed or null.
+ /// Hold-key state (1=None, 2=Run). Null = omit.
+ /// Landblock cell ID (u32).
+ /// World-space position relative to the landblock.
+ /// Rotation quaternion. AC wire order is W, X, Y, Z.
+ /// Instance sequence number from the server.
+ /// Server-control sequence number.
+ /// Teleport sequence number.
+ /// Force-position sequence number.
+ /// 1 if the character is on the ground, 0 if airborne.
+ public static byte[] Build(
+ uint gameActionSequence,
+ uint? forwardCommand,
+ float? forwardSpeed,
+ uint? sidestepCommand,
+ float? sidestepSpeed,
+ uint? turnCommand,
+ float? turnSpeed,
+ uint? holdKey,
+ uint cellId,
+ Vector3 position,
+ Quaternion rotation,
+ ushort instanceSequence,
+ ushort serverControlSequence,
+ ushort teleportSequence,
+ ushort forcePositionSequence,
+ byte contactLongJump = 1)
+ {
+ var w = new PacketWriter(128);
+
+ // --- GameAction envelope ---
+ w.WriteUInt32(GameActionOpcode);
+ w.WriteUInt32(gameActionSequence);
+ w.WriteUInt32(MoveToStateAction);
+
+ // --- RawMotionState ---
+ // Build the flags word. Command list length (bits 11-31) is always 0.
+ uint flags = 0u;
+ if (holdKey.HasValue) flags |= FlagCurrentHoldKey;
+ if (forwardCommand.HasValue) flags |= FlagForwardCommand;
+ if (forwardSpeed.HasValue) flags |= FlagForwardSpeed;
+ if (sidestepCommand.HasValue) flags |= FlagSidestepCommand;
+ if (sidestepSpeed.HasValue) flags |= FlagSidestepSpeed;
+ if (turnCommand.HasValue) flags |= FlagTurnCommand;
+ if (turnSpeed.HasValue) flags |= FlagTurnSpeed;
+
+ w.WriteUInt32(flags); // bits 0-10 = flags, bits 11-31 = 0 (no command list)
+
+ // Conditional fields in RawMotionFlags bit order:
+ if (holdKey.HasValue) w.WriteUInt32(holdKey.Value);
+ // FlagCurrentStyle (0x2): not sent — we don't track stance changes here
+ if (forwardCommand.HasValue) w.WriteUInt32(forwardCommand.Value);
+ // FlagForwardHoldKey (0x8): not sent
+ if (forwardSpeed.HasValue) w.WriteFloat(forwardSpeed.Value);
+ if (sidestepCommand.HasValue) w.WriteUInt32(sidestepCommand.Value);
+ // FlagSidestepHoldKey (0x40): not sent
+ if (sidestepSpeed.HasValue) w.WriteFloat(sidestepSpeed.Value);
+ if (turnCommand.HasValue) w.WriteUInt32(turnCommand.Value);
+ // FlagTurnHoldKey (0x200): not sent
+ if (turnSpeed.HasValue) w.WriteFloat(turnSpeed.Value);
+
+ // --- WorldPosition (32 bytes) ---
+ w.WriteUInt32(cellId);
+ w.WriteFloat(position.X);
+ w.WriteFloat(position.Y);
+ w.WriteFloat(position.Z);
+ // Quaternion wire order: W, X, Y, Z
+ w.WriteFloat(rotation.W);
+ w.WriteFloat(rotation.X);
+ w.WriteFloat(rotation.Y);
+ w.WriteFloat(rotation.Z);
+
+ // --- Sequence numbers ---
+ w.WriteUInt16(instanceSequence);
+ w.WriteUInt16(serverControlSequence);
+ w.WriteUInt16(teleportSequence);
+ w.WriteUInt16(forcePositionSequence);
+
+ // --- Contact byte + 4-byte align ---
+ w.WriteByte(contactLongJump);
+ w.AlignTo4();
+
+ return w.ToArray();
+ }
+}
diff --git a/src/AcDream.Core.Net/Packets/PacketWriter.cs b/src/AcDream.Core.Net/Packets/PacketWriter.cs
index 7c7c7d9..f7edd92 100644
--- a/src/AcDream.Core.Net/Packets/PacketWriter.cs
+++ b/src/AcDream.Core.Net/Packets/PacketWriter.cs
@@ -88,6 +88,13 @@ public sealed class PacketWriter
_position += bytes.Length;
}
+ public void WriteFloat(float value)
+ {
+ EnsureCapacity(4);
+ BinaryPrimitives.WriteSingleLittleEndian(_buffer.AsSpan(_position), value);
+ _position += 4;
+ }
+
/// Pad with zeros so the buffer length is a multiple of 4.
public void AlignTo4()
{
diff --git a/src/AcDream.Core.Net/WorldSession.cs b/src/AcDream.Core.Net/WorldSession.cs
index 089ac14..9172477 100644
--- a/src/AcDream.Core.Net/WorldSession.cs
+++ b/src/AcDream.Core.Net/WorldSession.cs
@@ -126,6 +126,16 @@ public sealed class WorldSession : IDisposable
///
private bool _loginCompleteSent;
+ ///
+ /// Phase B.2: per-session game-action sequence counter. Monotonically
+ /// incremented by and embedded in
+ /// every outbound MoveToState / AutonomousPosition GameAction message.
+ /// ACE's GameActionPacket.HandleGameAction reads the sequence field but
+ /// currently only uses it for logging — however retail clients do
+ /// increment it, so we match that behaviour.
+ ///
+ private uint _gameActionSequence;
+
public WorldSession(IPEndPoint serverLogin)
{
_loginEndpoint = serverLogin;
@@ -419,6 +429,24 @@ public sealed class WorldSession : IDisposable
}
}
+ ///
+ /// Phase B.2: send a pre-built GameAction body (which already contains
+ /// the 0xF7B1 envelope + sequence + action-type header). Used by the
+ /// PlayerMovementController for MoveToState and AutonomousPosition.
+ ///
+ public void SendGameAction(byte[] gameActionBody)
+ {
+ SendGameMessage(gameActionBody);
+ }
+
+ ///
+ /// Phase B.2: get and increment the game-action sequence counter.
+ /// Call once per outbound movement message; pass the returned value
+ /// to or
+ /// .
+ ///
+ public uint NextGameActionSequence() => ++_gameActionSequence;
+
private void SendGameMessage(byte[] gameMessageBody)
{
var fragment = GameMessageFragment.BuildSingleFragment(
diff --git a/tests/AcDream.Core.Net.Tests/Messages/AutonomousPositionTests.cs b/tests/AcDream.Core.Net.Tests/Messages/AutonomousPositionTests.cs
new file mode 100644
index 0000000..7552d80
--- /dev/null
+++ b/tests/AcDream.Core.Net.Tests/Messages/AutonomousPositionTests.cs
@@ -0,0 +1,131 @@
+using System;
+using System.Buffers.Binary;
+using System.Numerics;
+using AcDream.Core.Net.Messages;
+using Xunit;
+
+namespace AcDream.Core.Net.Tests.Messages;
+
+public class AutonomousPositionTests
+{
+ [Fact]
+ public void Build_ProducesValidGameAction()
+ {
+ var body = AutonomousPosition.Build(
+ gameActionSequence: 5,
+ cellId: 0xA9B40001u,
+ position: new Vector3(100f, 100f, 50f),
+ rotation: Quaternion.Identity,
+ instanceSequence: 0,
+ serverControlSequence: 0,
+ teleportSequence: 0,
+ forcePositionSequence: 0);
+
+ uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(0));
+ Assert.Equal(0xF7B1u, opcode);
+
+ uint seq = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(4));
+ Assert.Equal(5u, seq);
+
+ uint actionType = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8));
+ Assert.Equal(0xF753u, actionType);
+ }
+
+ [Fact]
+ public void Build_ContainsCellIdAfterHeader()
+ {
+ var body = AutonomousPosition.Build(
+ gameActionSequence: 1,
+ cellId: 0xDEADBEEFu,
+ position: Vector3.Zero,
+ rotation: Quaternion.Identity,
+ instanceSequence: 0,
+ serverControlSequence: 0,
+ teleportSequence: 0,
+ forcePositionSequence: 0);
+
+ // After the 12-byte GameAction header, the WorldPosition starts
+ // with u32 cell_id.
+ uint cellId = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12));
+ Assert.Equal(0xDEADBEEFu, cellId);
+ }
+
+ [Fact]
+ public void Build_ContainsPosition_AfterCellId()
+ {
+ var body = AutonomousPosition.Build(
+ gameActionSequence: 2,
+ cellId: 0xA9B40001u,
+ position: new Vector3(12.5f, 34.0f, 56.75f),
+ rotation: Quaternion.Identity,
+ instanceSequence: 0,
+ serverControlSequence: 0,
+ teleportSequence: 0,
+ forcePositionSequence: 0);
+
+ // WorldPosition: cellId (4) + x (4) + y (4) + z (4) at offsets 12-27
+ float x = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(16));
+ float y = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(20));
+ float z = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(24));
+ Assert.Equal(12.5f, x);
+ Assert.Equal(34.0f, y);
+ Assert.Equal(56.75f, z);
+ }
+
+ [Fact]
+ public void Build_IsAlignedTo4Bytes()
+ {
+ var body = AutonomousPosition.Build(
+ gameActionSequence: 3,
+ cellId: 0xA9B40001u,
+ position: Vector3.Zero,
+ rotation: Quaternion.Identity,
+ instanceSequence: 0,
+ serverControlSequence: 0,
+ teleportSequence: 0,
+ forcePositionSequence: 0);
+
+ Assert.Equal(0, body.Length % 4);
+ }
+
+ [Fact]
+ public void Build_TotalLengthIsCorrect_NoCommandsNoExtraFields()
+ {
+ // 12 (envelope) + 32 (WorldPosition) + 8 (4x u16 sequences) + 1 (contact) + 3 (align) = 56
+ var body = AutonomousPosition.Build(
+ gameActionSequence: 4,
+ cellId: 0xA9B40001u,
+ position: Vector3.Zero,
+ rotation: Quaternion.Identity,
+ instanceSequence: 0,
+ serverControlSequence: 0,
+ teleportSequence: 0,
+ forcePositionSequence: 0);
+
+ Assert.Equal(56, body.Length);
+ }
+
+ [Fact]
+ public void Build_ContainsIdentityRotation_AfterPosition()
+ {
+ var body = AutonomousPosition.Build(
+ gameActionSequence: 6,
+ cellId: 0xA9B40001u,
+ position: Vector3.Zero,
+ rotation: Quaternion.Identity, // W=1, X=Y=Z=0
+ instanceSequence: 0,
+ serverControlSequence: 0,
+ teleportSequence: 0,
+ forcePositionSequence: 0);
+
+ // Rotation starts at offset 28: rotW(4), rotX(4), rotY(4), rotZ(4)
+ float rotW = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(28));
+ float rotX = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(32));
+ float rotY = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(36));
+ float rotZ = BinaryPrimitives.ReadSingleLittleEndian(body.AsSpan(40));
+ Assert.Equal(1.0f, rotW);
+ Assert.Equal(0.0f, rotX);
+ Assert.Equal(0.0f, rotY);
+ Assert.Equal(0.0f, rotZ);
+ }
+}
diff --git a/tests/AcDream.Core.Net.Tests/Messages/MoveToStateTests.cs b/tests/AcDream.Core.Net.Tests/Messages/MoveToStateTests.cs
new file mode 100644
index 0000000..8070cca
--- /dev/null
+++ b/tests/AcDream.Core.Net.Tests/Messages/MoveToStateTests.cs
@@ -0,0 +1,172 @@
+using System;
+using System.Buffers.Binary;
+using System.Numerics;
+using AcDream.Core.Net.Messages;
+using Xunit;
+
+namespace AcDream.Core.Net.Tests.Messages;
+
+public class MoveToStateTests
+{
+ [Fact]
+ public void Build_IdleState_ProducesValidGameAction()
+ {
+ var body = MoveToState.Build(
+ gameActionSequence: 1,
+ forwardCommand: null,
+ forwardSpeed: null,
+ sidestepCommand: null,
+ sidestepSpeed: null,
+ turnCommand: null,
+ turnSpeed: null,
+ holdKey: null,
+ cellId: 0xA9B40001u,
+ position: new Vector3(96f, 96f, 50f),
+ rotation: Quaternion.Identity,
+ instanceSequence: 0,
+ serverControlSequence: 0,
+ teleportSequence: 0,
+ forcePositionSequence: 0);
+
+ // First 4 bytes: GameAction opcode 0xF7B1
+ uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(0));
+ Assert.Equal(0xF7B1u, opcode);
+
+ // Bytes 4-7: game action sequence
+ uint seq = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(4));
+ Assert.Equal(1u, seq);
+
+ // Bytes 8-11: MoveToState action type 0xF61C
+ uint actionType = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8));
+ Assert.Equal(0xF61Cu, actionType);
+ }
+
+ [Fact]
+ public void Build_WalkForward_IncludesForwardCommandInFlags()
+ {
+ var body = MoveToState.Build(
+ gameActionSequence: 2,
+ forwardCommand: 0x45000005u, // WalkForward
+ forwardSpeed: 1.0f,
+ sidestepCommand: null,
+ sidestepSpeed: null,
+ turnCommand: null,
+ turnSpeed: null,
+ holdKey: null,
+ cellId: 0xA9B40001u,
+ position: new Vector3(96f, 96f, 50f),
+ rotation: Quaternion.Identity,
+ instanceSequence: 0,
+ serverControlSequence: 0,
+ teleportSequence: 0,
+ forcePositionSequence: 0);
+
+ // After the 12-byte GameAction header comes RawMotionState.
+ // First u32 is the packed flags word. ForwardCommand flag = 0x4.
+ uint flags = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12));
+ Assert.True((flags & 0x4u) != 0, "ForwardCommand flag (0x4) should be set");
+ // ForwardSpeed flag = 0x10
+ Assert.True((flags & 0x10u) != 0, "ForwardSpeed flag (0x10) should be set");
+ }
+
+ [Fact]
+ public void Build_IdleState_RawMotionFlagsAreZero()
+ {
+ var body = MoveToState.Build(
+ gameActionSequence: 3,
+ forwardCommand: null,
+ forwardSpeed: null,
+ sidestepCommand: null,
+ sidestepSpeed: null,
+ turnCommand: null,
+ turnSpeed: null,
+ holdKey: null,
+ cellId: 0xA9B40001u,
+ position: Vector3.Zero,
+ rotation: Quaternion.Identity,
+ instanceSequence: 0,
+ serverControlSequence: 0,
+ teleportSequence: 0,
+ forcePositionSequence: 0);
+
+ uint flags = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12));
+ Assert.Equal(0u, flags);
+ }
+
+ [Fact]
+ public void Build_IdleState_WorldPositionFollowsMotionState()
+ {
+ // With no motion state, flags = 0 and no conditional fields are written.
+ // So WorldPosition starts at offset 12 (envelope) + 4 (flags) = 16.
+ var body = MoveToState.Build(
+ gameActionSequence: 4,
+ forwardCommand: null,
+ forwardSpeed: null,
+ sidestepCommand: null,
+ sidestepSpeed: null,
+ turnCommand: null,
+ turnSpeed: null,
+ holdKey: null,
+ cellId: 0xDEADBEEFu,
+ position: Vector3.Zero,
+ rotation: Quaternion.Identity,
+ instanceSequence: 0,
+ serverControlSequence: 0,
+ teleportSequence: 0,
+ forcePositionSequence: 0);
+
+ uint cellId = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16));
+ Assert.Equal(0xDEADBEEFu, cellId);
+ }
+
+ [Fact]
+ public void Build_IsAlignedTo4Bytes()
+ {
+ var body = MoveToState.Build(
+ gameActionSequence: 5,
+ forwardCommand: null,
+ forwardSpeed: null,
+ sidestepCommand: null,
+ sidestepSpeed: null,
+ turnCommand: null,
+ turnSpeed: null,
+ holdKey: null,
+ cellId: 0xA9B40001u,
+ position: Vector3.Zero,
+ rotation: Quaternion.Identity,
+ instanceSequence: 0,
+ serverControlSequence: 0,
+ teleportSequence: 0,
+ forcePositionSequence: 0);
+
+ Assert.Equal(0, body.Length % 4);
+ }
+
+ [Fact]
+ public void Build_WithHoldKey_IncludesHoldKeyFlag()
+ {
+ var body = MoveToState.Build(
+ gameActionSequence: 6,
+ forwardCommand: null,
+ forwardSpeed: null,
+ sidestepCommand: null,
+ sidestepSpeed: null,
+ turnCommand: null,
+ turnSpeed: null,
+ holdKey: 2u, // Run
+ cellId: 0xA9B40001u,
+ position: Vector3.Zero,
+ rotation: Quaternion.Identity,
+ instanceSequence: 0,
+ serverControlSequence: 0,
+ teleportSequence: 0,
+ forcePositionSequence: 0);
+
+ uint flags = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12));
+ Assert.True((flags & 0x1u) != 0, "CurrentHoldKey flag (0x1) should be set");
+
+ // The hold key value (u32 = 2) should immediately follow the flags
+ uint holdKeyValue = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16));
+ Assert.Equal(2u, holdKeyValue);
+ }
+}