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