using System.Buffers.Binary; namespace AcDream.Core.Net.Messages; /// /// Inbound UpdatePosition GameMessage (opcode 0xF748). The /// server sends this whenever an entity moves in the world — NPCs walking /// their patrol routes, creatures hunting, other players running past, /// projectiles tracking. Without handling this, NPCs only ever render at /// their CreateObject spawn point and never follow their server-side /// position. Pairs with : motion tells us /// what cycle to play, position tells us where in world space. /// /// /// Wire layout (see /// references/ACE/Source/ACE.Server/Network/GameMessages/Messages/GameMessageUpdatePosition.cs /// and references/ACE/Source/ACE.Server/Network/Structure/PositionPack.cs::Write): /// /// /// u32 opcode — 0xF748 /// u32 objectGuid /// u32 positionFlags — see /// Origin — u32 landblockCellId + 3xf32 local XYZ /// Rotation components — f32 W / X / Y / Z, but only the /// ones whose OrientationHasNo* flag is clear. Absent /// components default to 0. /// Velocity — 3xf32 if HasVelocity set /// PlacementID — u32 if HasPlacementID set /// Four u16 sequence numbers — instance, position, teleport, /// forcePosition. We don't currently check these for freshness but /// we must consume them to walk the buffer correctly. /// /// public static class UpdatePosition { public const uint Opcode = 0xF748u; /// /// Bitflag layout mirroring ACE.Entity.Enum.PositionFlags. /// Exposed so callers can inspect whether velocity / placement were /// present in the wire payload, though for the basic rendering use /// case only the position/rotation matter. /// [Flags] public enum PositionFlags : uint { None = 0x00, HasVelocity = 0x01, HasPlacementID = 0x02, IsGrounded = 0x04, OrientationHasNoW = 0x08, OrientationHasNoX = 0x10, OrientationHasNoY = 0x20, OrientationHasNoZ = 0x40, } /// /// Extracted payload: the target guid plus its new world position and /// rotation. Velocity and placement are captured too but are optional /// information for clients that want to smooth motion between updates. /// public readonly record struct Parsed( uint Guid, CreateObject.ServerPosition Position, System.Numerics.Vector3? Velocity, uint? PlacementId, bool IsGrounded, ushort InstanceSequence = 0, ushort TeleportSequence = 0, ushort ForcePositionSequence = 0); /// /// Parse a reassembled UpdatePosition body. /// must start with the 4-byte opcode. Returns null on truncation or /// wrong opcode. /// public static Parsed? TryParse(ReadOnlySpan body) { try { int pos = 0; if (body.Length - pos < 4) return null; uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos)); pos += 4; if (opcode != Opcode) return null; if (body.Length - pos < 4) return null; uint guid = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos)); pos += 4; if (body.Length - pos < 4) return null; var flags = (PositionFlags)BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos)); pos += 4; // Origin: u32 cellId + Vector3 position if (body.Length - pos < 16) return null; uint cellId = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos)); float px = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos + 4)); float py = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos + 8)); float pz = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos + 12)); pos += 16; // Rotation: each component is f32 *only if the corresponding // OrientationHasNo* flag is CLEAR*. An unset flag means "this // component is present in the payload". Default 0 for absent. float rw = 0f, rx = 0f, ry = 0f, rz = 0f; if ((flags & PositionFlags.OrientationHasNoW) == 0) { if (body.Length - pos < 4) return null; rw = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos)); pos += 4; } if ((flags & PositionFlags.OrientationHasNoX) == 0) { if (body.Length - pos < 4) return null; rx = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos)); pos += 4; } if ((flags & PositionFlags.OrientationHasNoY) == 0) { if (body.Length - pos < 4) return null; ry = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos)); pos += 4; } if ((flags & PositionFlags.OrientationHasNoZ) == 0) { if (body.Length - pos < 4) return null; rz = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos)); pos += 4; } System.Numerics.Vector3? velocity = null; if ((flags & PositionFlags.HasVelocity) != 0) { if (body.Length - pos < 12) return null; velocity = new System.Numerics.Vector3( BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos + 0)), BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos + 4)), BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos + 8))); pos += 12; } uint? placementId = null; if ((flags & PositionFlags.HasPlacementID) != 0) { if (body.Length - pos < 4) return null; placementId = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos)); pos += 4; } // Four u16 sequence numbers: instance, position, teleport, forcePosition. ushort instSeq = 0, teleSeq = 0, forceSeq = 0; if (body.Length - pos >= 8) { instSeq = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos)); // pos+2 = positionSequence (not tracked by movement) teleSeq = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos + 4)); forceSeq = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos + 6)); pos += 8; } var serverPos = new CreateObject.ServerPosition( LandblockId: cellId, PositionX: px, PositionY: py, PositionZ: pz, RotationW: rw, RotationX: rx, RotationY: ry, RotationZ: rz); return new Parsed(guid, serverPos, velocity, placementId, IsGrounded: (flags & PositionFlags.IsGrounded) != 0, instSeq, teleSeq, forceSeq); } catch { return null; } } }