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