using System; using System.Buffers.Binary; namespace AcDream.Core.Net.Messages; /// /// Parsed header of a retail AC 0xF7B0 GameEvent envelope (S→C). /// /// /// Wire layout /// (references/ACE/Source/ACE.Server/Network/GameEvent/GameEventMessage.cs): /// /// u32 0xF7B0 // GameMessage opcode (GameEvent envelope) /// u32 guid // session's player guid (0 if none yet) /// u32 gameEventSequence // per-session, incremented by server /// u32 eventType // GameEventType sub-opcode (94 values) /// <payload> // variable; type-dispatched /// /// /// /// /// Payload is returned as a view into the /// original message body — no copy. Per-event parsers slice from this. /// /// public readonly record struct GameEventEnvelope( uint PlayerGuid, uint Sequence, GameEventType EventType, ReadOnlyMemory Payload) { /// GameMessage opcode of the outer envelope. public const uint Opcode = 0xF7B0u; /// Header size (4×u32 = 16 bytes before payload). public const int HeaderSize = 16; /// /// Parse a raw 0xF7B0 GameMessage body into the header + payload view. /// Returns null if the body is shorter than the header or the outer /// opcode doesn't match. /// public static GameEventEnvelope? TryParse(byte[] body) { ArgumentNullException.ThrowIfNull(body); return TryParseBorrowed(body); } /// /// Internal receive-path parser. The returned payload borrows /// and must not escape synchronous dispatch. /// internal static GameEventEnvelope? TryParseBorrowed( ReadOnlyMemory body) { if (body.Length < HeaderSize) return null; ReadOnlySpan span = body.Span; uint outer = BinaryPrimitives.ReadUInt32LittleEndian(span); if (outer != Opcode) return null; uint guid = BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(4)); uint sequence = BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(8)); uint eventType = BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(12)); ReadOnlyMemory payload = body.Slice(HeaderSize); return new GameEventEnvelope(guid, sequence, (GameEventType)eventType, payload); } }