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, uint? forwardHoldKey = null, uint? sidestepHoldKey = null, uint? turnHoldKey = null) { 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. // Field order matches holtburger's RawMotionState::pack — for any axis // where we send a COMMAND + SPEED, retail expects the matching // *_HOLD_KEY to accompany them (see holtburger's // build_motion_state_raw_motion_state). Without the per-axis hold // keys the server gets the flags but can't classify the input as a // continuously-held key, so other players see the character sliding // forward without an animation cycle. uint flags = 0u; if (holdKey.HasValue) flags |= FlagCurrentHoldKey; if (forwardCommand.HasValue) flags |= FlagForwardCommand; if (forwardHoldKey.HasValue) flags |= FlagForwardHoldKey; if (forwardSpeed.HasValue) flags |= FlagForwardSpeed; if (sidestepCommand.HasValue) flags |= FlagSidestepCommand; if (sidestepHoldKey.HasValue) flags |= FlagSidestepHoldKey; if (sidestepSpeed.HasValue) flags |= FlagSidestepSpeed; if (turnCommand.HasValue) flags |= FlagTurnCommand; if (turnHoldKey.HasValue) flags |= FlagTurnHoldKey; 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); if (forwardHoldKey.HasValue) w.WriteUInt32(forwardHoldKey.Value); if (forwardSpeed.HasValue) w.WriteFloat(forwardSpeed.Value); if (sidestepCommand.HasValue) w.WriteUInt32(sidestepCommand.Value); if (sidestepHoldKey.HasValue) w.WriteUInt32(sidestepHoldKey.Value); if (sidestepSpeed.HasValue) w.WriteFloat(sidestepSpeed.Value); if (turnCommand.HasValue) w.WriteUInt32(turnCommand.Value); if (turnHoldKey.HasValue) w.WriteUInt32(turnHoldKey.Value); 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(); } }