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;
}
}
}