using System.Buffers.Binary;
using AcDream.Core.Net.Cryptography;
namespace AcDream.Core.Net.Packets;
///
/// The fixed 20-byte header that prefixes every AC UDP packet. Fields are
/// little-endian on the wire.
///
///
/// Layout (byte offsets):
///
/// 0 Sequence uint32 Packet sequence number (client- or server-side monotonic)
/// 4 Flags uint32 PacketHeaderFlags bitmask
/// 8 Checksum uint32 Header+body checksum (XOR'd with ISAAC keystream when the
/// EncryptedChecksum flag is set)
/// 12 Id uint16 Session/connection id
/// 14 Time uint16 Server time echo (used for latency tracking)
/// 16 Size uint16 Body length in bytes (excludes this 20-byte header)
/// 18 Iteration uint16 Retransmit iteration counter
///
///
///
/// Reimplemented from ACE's AGPL reference; see NOTICE.md.
///
public struct PacketHeader
{
public const int Size = 20;
///
/// Placeholder checksum value that every header uses while its own Hash32
/// checksum is being calculated. The real checksum replaces this sentinel
/// before transmit. Matches AC's retail + ACE's convention byte-for-byte.
///
public const uint ChecksumPlaceholder = 0xBADD70DDu;
public uint Sequence;
public PacketHeaderFlags Flags;
public uint Checksum;
public ushort Id;
public ushort Time;
public ushort DataSize;
public ushort Iteration;
/// True if the header's Flags field has any of the bits in .
public readonly bool HasFlag(PacketHeaderFlags flags) => (Flags & flags) != 0;
/// Write this header into the first 20 bytes of .
public readonly void Pack(Span destination)
{
if (destination.Length < Size)
throw new ArgumentException($"destination must be at least {Size} bytes", nameof(destination));
BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(0), Sequence);
BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(4), (uint)Flags);
BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(8), Checksum);
BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(12), Id);
BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(14), Time);
BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(16), DataSize);
BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(18), Iteration);
}
/// Read a header from the first 20 bytes of .
public static PacketHeader Unpack(ReadOnlySpan source)
{
if (source.Length < Size)
throw new ArgumentException($"source must be at least {Size} bytes", nameof(source));
return new PacketHeader
{
Sequence = BinaryPrimitives.ReadUInt32LittleEndian(source.Slice(0)),
Flags = (PacketHeaderFlags)BinaryPrimitives.ReadUInt32LittleEndian(source.Slice(4)),
Checksum = BinaryPrimitives.ReadUInt32LittleEndian(source.Slice(8)),
Id = BinaryPrimitives.ReadUInt16LittleEndian(source.Slice(12)),
Time = BinaryPrimitives.ReadUInt16LittleEndian(source.Slice(14)),
DataSize = BinaryPrimitives.ReadUInt16LittleEndian(source.Slice(16)),
Iteration = BinaryPrimitives.ReadUInt16LittleEndian(source.Slice(18)),
};
}
///
/// Compute the Hash32 of this header with the Checksum field replaced by
/// . Used by both the transmit path (to
/// fill in the real checksum before sending) and the receive path (to
/// verify a packet's checksum matches the one we'd compute).
///
/// Returned value is the header's contribution to the overall packet
/// checksum; the full packet checksum also folds in body fragments.
///
public readonly uint CalculateHeaderHash32()
{
Span buffer = stackalloc byte[Size];
// Save and restore Checksum via a local copy rather than mutating this.
var saved = this;
saved.Checksum = ChecksumPlaceholder;
saved.Pack(buffer);
return Hash32.Calculate(buffer);
}
}