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