using System.Numerics; using AcDream.Core.Net.Packets; using AcDream.Core.Physics; 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 — ports retail's MoveToStatePack::Pack verbatim /// (0x005168f0, decomp lines ~284694-284722). Confirmed against the /// Ghidra decompile-by-address bridge during this slice (2026-06-30): /// /// /// GameAction envelope: u32 0xF7B1, u32 sequence, u32 0xF61C /// RawMotionState::Pack: see /// — default-difference flags dword + conditional fields + actions. /// Position::Pack: u32 cellId, f32 x, f32 y, f32 z, /// f32 qw, f32 qx, f32 qy, f32 qz (32 bytes) /// Sequences: u16 instance, u16 serverControl, /// u16 teleport, u16 forcePosition /// Trailing byte: (standingLongjump ? 0x02 : 0) | /// (contact ? 0x01 : 0)MoveToStatePack::Pack trailing /// byte expression (longjump_mode == 0) - 1U & 2 | /// contact != 0. /// Align to 4 bytes /// /// public static class MoveToState { public const uint GameActionOpcode = 0xF7B1u; public const uint MoveToStateAction = 0xF61Cu; /// /// Build a MoveToState GameAction body. /// /// Monotonically increasing counter from /// . /// Complete raw-motion snapshot, matching /// retail's CPhysicsObj::InqRawMotionState(). Fields equal to /// are omitted from the wire /// (see ). /// 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. /// True if the character is on the ground. /// True during a standing (charged, /// stationary) longjump wind-up. Not yet implemented in acdream — /// callers pass false honestly until that feature lands. public static byte[] Build( uint gameActionSequence, RawMotionState rawMotionState, uint cellId, Vector3 position, Quaternion rotation, ushort instanceSequence, ushort serverControlSequence, ushort teleportSequence, ushort forcePositionSequence, bool contact = true, bool standingLongjump = false) { var w = new PacketWriter(128); // --- GameAction envelope --- w.WriteUInt32(GameActionOpcode); w.WriteUInt32(gameActionSequence); w.WriteUInt32(MoveToStateAction); // --- RawMotionState::Pack (0x0051ed10) --- RawMotionStatePacker.Pack(w, rawMotionState); // --- Position::Pack (0x005a9640): cellId + Frame::Pack (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); // --- Trailing byte (MoveToStatePack::Pack, 0x005168f0): --- // ((longjump_mode != 0) ? 0x02 : 0) | (contact != 0 ? 0x01 : 0) byte trailing = (byte)((standingLongjump ? 0x02 : 0) | (contact ? 0x01 : 0)); w.WriteByte(trailing); w.AlignTo4(); return w.ToArray(); } }