using System.Buffers.Binary; using System.Numerics; namespace AcDream.Core.Net.Messages; /// /// Inbound VectorUpdate GameMessage (opcode 0xF74E). The /// server broadcasts this when a remote entity's velocity / omega changes /// without an accompanying full UpdatePosition — most importantly when a /// remote player JUMPS. Without handling this, remote jumps look like /// the player teleported through a tiny vertical hop and back: we never /// see the +Z velocity that would integrate into a proper arc. /// /// /// Wire layout (see /// references/ACE/Source/ACE.Server/Network/GameMessages/Messages/GameMessageVectorUpdate.cs): /// /// /// u32 opcode — 0xF74E /// u32 objectGuid /// 3xf32 velocity — world-space (already rotated by ACE's /// GameMessageVectorUpdate.cs:20 from PhysicsObj.Velocity). /// 3xf32 omega — world-space angular velocity. /// u16 instanceSequence — for stale-packet rejection. /// u16 vectorSequence — for stale-packet rejection. /// /// /// Total body size after opcode: 32 bytes. /// public static class VectorUpdate { public const uint Opcode = 0xF74Eu; public readonly record struct Parsed( uint Guid, Vector3 Velocity, Vector3 Omega, ushort InstanceSequence, ushort VectorSequence); /// /// Parse a 0xF74E body. must start with the /// 4-byte opcode (matches the convention used by UpdateMotion / /// UpdatePosition / etc.). Returns null on truncation or opcode /// mismatch. /// public static Parsed? TryParse(ReadOnlySpan body) { // K-fix16 (2026-04-26): body convention includes the opcode at // offset 0 — every other parser in this folder verifies the // opcode word before reading payload fields. The previous // version of this method skipped that, reading guid from the // opcode bytes (and shifting every subsequent field by 4), // which is why VU.recv lines showed guid=0xF74E and gigantic // garbage velocity values. if (body.Length < 4 + 32) return null; try { uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(0, 4)); if (opcode != Opcode) return null; uint guid = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(4, 4)); float vx = BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(body.Slice(8, 4))); float vy = BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(body.Slice(12, 4))); float vz = BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(body.Slice(16, 4))); float ox = BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(body.Slice(20, 4))); float oy = BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(body.Slice(24, 4))); float oz = BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(body.Slice(28, 4))); ushort instSeq = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(32, 2)); ushort vecSeq = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(34, 2)); return new Parsed(guid, new Vector3(vx, vy, vz), new Vector3(ox, oy, oz), instSeq, vecSeq); } catch { return null; } } }