using System.Buffers.Binary; namespace AcDream.Core.Net.Packets; /// /// The variable-sized "optional header" section that lives between the /// 20-byte and the body fragments (or login /// payload) of an AC UDP packet. Each sub-section is gated by a flag in /// header.Flags — no flags, no optional section. /// /// /// Fields that actually exist once parsed (all default-zeroed when the /// corresponding flag isn't set): /// /// /// — present when is set. /// — list of sequence numbers to /// retransmit, present when is set. /// — server time sync value, present when /// is set. /// — present on echo requests. /// / — flow control, present on Flow packets. /// /// /// /// The parser also records the raw bytes it consumed in /// — AC's CRC verification requires hashing this /// optional section separately from the main header and the fragments, so /// the byte slice is needed downstream. /// /// /// Reimplemented from ACE's AGPL reference; see NOTICE.md. /// public sealed class PacketHeaderOptional { /// Raw bytes consumed from the wire, used by . public byte[] RawBytes { get; private set; } = Array.Empty(); public uint AckSequence { get; private set; } public IReadOnlyList RetransmitRequests { get; private set; } = Array.Empty(); /// N2: sequence ids the server refuses to retransmit /// (RejectRetransmit 0x2000) — the client abandons the matching /// NAK-set entries (SharedNet::HandleEmptyAck @ 0x005448F0). public IReadOnlyList RejectRetransmits { get; private set; } = Array.Empty(); public double TimeSync { get; private set; } public float EchoRequestClientTime { get; private set; } public uint FlowBytes { get; private set; } public ushort FlowInterval { get; private set; } // ConnectRequest fields (server → client handshake packet, 32 bytes). // ACE's AGPL parser doesn't decode these because servers only send them. // acdream is a client so we DO need the decoded values. public double ConnectRequestServerTime { get; private set; } public ulong ConnectRequestCookie { get; private set; } public uint ConnectRequestClientId { get; private set; } /// 4-byte seed to feed the ISAAC instance used for INBOUND /// packets (server's outgoing stream = our incoming). public uint ConnectRequestServerSeed { get; private set; } /// 4-byte seed for the ISAAC used for OUTBOUND packets /// (our outgoing stream = server's incoming). public uint ConnectRequestClientSeed { get; private set; } /// /// Parse the optional section from (which starts /// right after the 20-byte header). Returns the number of bytes consumed /// from , or -1 if the section is malformed /// (short reads, impossible lengths). On success, /// holds a copy of the consumed slice. /// public int Parse(ReadOnlySpan body, PacketHeaderFlags flags) { int pos = 0; // ServerSwitch: 8 bytes, no semantic parse yet (we just consume it // for checksum coverage — ACE doesn't decode these either). if (HasFlag(flags, PacketHeaderFlags.ServerSwitch)) { if (!Take(body, ref pos, 8)) return -1; } if (HasFlag(flags, PacketHeaderFlags.RequestRetransmit)) { if (!Take(body, ref pos, 4)) return -1; uint count = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos - 4)); // Bounds: count * 4 bytes must fit. if (count > 1024 || body.Length - pos < (int)count * 4) return -1; var list = new uint[count]; for (int i = 0; i < count; i++) { list[i] = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos)); pos += 4; } RetransmitRequests = list; } if (HasFlag(flags, PacketHeaderFlags.RejectRetransmit)) { if (!Take(body, ref pos, 4)) return -1; uint count = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos - 4)); if (count > 1024 || body.Length - pos < (int)count * 4) return -1; var rejected = new uint[count]; for (int i = 0; i < count; i++) { rejected[i] = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos)); pos += 4; } RejectRetransmits = rejected; } if (HasFlag(flags, PacketHeaderFlags.AckSequence)) { if (!Take(body, ref pos, 4)) return -1; AckSequence = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos - 4)); } // LoginRequest: the remaining body (after all prior optional sections) // is the login payload. ACE reads it without advancing position so // the fragment loop can re-read it from the start — our parser mirrors // that behavior by "peeking" the remaining bytes into RawBytes without // advancing pos beyond this point in the main pipeline. Callers that // want the login blob should read from body[pos..] directly. // // Note: LoginRequest packets have no fragments after this point. if (HasFlag(flags, PacketHeaderFlags.LoginRequest)) { // Count the entire remaining body as part of the optional section // for checksum purposes, and stop parsing here. int loginBytes = body.Length - pos; if (loginBytes < 0) return -1; RawBytes = body.Slice(0, pos + loginBytes).ToArray(); return pos + loginBytes; } // WorldLoginRequest: 8 bytes peeked (not advanced in ACE either — our // consumers treat it as covered by checksum, not advanced). // Strict port: we DO advance pos so the fragment loop reads after it, // matching the on-wire byte layout. ACE's peek-and-reset is an // implementation detail of its MemoryStream seeking that doesn't // affect the bytes-consumed count. if (HasFlag(flags, PacketHeaderFlags.WorldLoginRequest)) { if (!Take(body, ref pos, 8)) return -1; } // ConnectRequest (server → client): 32-byte fixed section. // Layout from ACE's PacketOutboundConnectRequest writer: // double ServerTime, ulong Cookie, uint ClientId, // byte[4] IsaacServerSeed, byte[4] IsaacClientSeed, uint Padding. if (HasFlag(flags, PacketHeaderFlags.ConnectRequest)) { if (body.Length - pos < 32) return -1; ConnectRequestServerTime = BitConverter.Int64BitsToDouble( BinaryPrimitives.ReadInt64LittleEndian(body.Slice(pos))); ConnectRequestCookie = BinaryPrimitives.ReadUInt64LittleEndian(body.Slice(pos + 8)); ConnectRequestClientId = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos + 16)); ConnectRequestServerSeed = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos + 20)); ConnectRequestClientSeed = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos + 24)); // bytes 28-31 are the trailing padding uint — skip via Take. pos += 32; } if (HasFlag(flags, PacketHeaderFlags.ConnectResponse)) { if (!Take(body, ref pos, 8)) return -1; } if (HasFlag(flags, PacketHeaderFlags.CICMDCommand)) { if (!Take(body, ref pos, 8)) return -1; } if (HasFlag(flags, PacketHeaderFlags.TimeSync)) { if (!Take(body, ref pos, 8)) return -1; TimeSync = BitConverter.Int64BitsToDouble( BinaryPrimitives.ReadInt64LittleEndian(body.Slice(pos - 8))); } if (HasFlag(flags, PacketHeaderFlags.EchoRequest)) { if (!Take(body, ref pos, 4)) return -1; EchoRequestClientTime = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos - 4)); } if (HasFlag(flags, PacketHeaderFlags.Flow)) { if (!Take(body, ref pos, 6)) return -1; FlowBytes = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos - 6)); FlowInterval = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos - 2)); } RawBytes = body.Slice(0, pos).ToArray(); return pos; } /// /// Hash32 over the raw bytes consumed during . Forms /// one of the three summands in the packet's CRC: header + optional + /// fragments. /// public uint CalculateHash32() => Cryptography.Hash32.Calculate(RawBytes); private static bool HasFlag(PacketHeaderFlags all, PacketHeaderFlags bit) => (all & bit) != 0; private static bool Take(ReadOnlySpan body, ref int pos, int n) { if (body.Length - pos < n) return false; pos += n; return true; } }