diff --git a/src/AcDream.Core.Net/Cryptography/Hash32.cs b/src/AcDream.Core.Net/Cryptography/Hash32.cs
new file mode 100644
index 0000000..5a66ac9
--- /dev/null
+++ b/src/AcDream.Core.Net/Cryptography/Hash32.cs
@@ -0,0 +1,41 @@
+using System.Buffers.Binary;
+
+namespace AcDream.Core.Net.Cryptography;
+
+///
+/// Simple 32-bit checksum used by the AC packet header. The algorithm is:
+///
+/// - Seed the accumulator with length << 16.
+/// - Sum the input as little-endian uint32s, four bytes at a time.
+/// - Fold any tail bytes (length not divisible by 4) into the top of
+/// the accumulator with a descending left-shift per byte (24, 16, 8, 0).
+///
+/// Reimplemented from reading ACE's AGPL reference
+/// (references/ACE/Source/ACE.Common/Cryptography/Hash32.cs). See
+/// NOTICE.md for the attribution policy.
+///
+public static class Hash32
+{
+ public static uint Calculate(ReadOnlySpan data)
+ {
+ int length = data.Length;
+ uint checksum = (uint)length << 16;
+
+ int wordAligned = length & ~3;
+ for (int i = 0; i < wordAligned; i += 4)
+ {
+ checksum += BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(i));
+ }
+
+ // Tail bytes (0, 1, 2, or 3 bytes) with descending shift: first tail
+ // byte goes into bits 24..31, second into 16..23, third into 8..15.
+ int shift = 24;
+ for (int j = wordAligned; j < length; j++)
+ {
+ checksum += (uint)data[j] << shift;
+ shift -= 8;
+ }
+
+ return checksum;
+ }
+}
diff --git a/src/AcDream.Core.Net/Packets/PacketHeader.cs b/src/AcDream.Core.Net/Packets/PacketHeader.cs
new file mode 100644
index 0000000..f2d9fe8
--- /dev/null
+++ b/src/AcDream.Core.Net/Packets/PacketHeader.cs
@@ -0,0 +1,99 @@
+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);
+ }
+}
diff --git a/src/AcDream.Core.Net/Packets/PacketHeaderFlags.cs b/src/AcDream.Core.Net/Packets/PacketHeaderFlags.cs
new file mode 100644
index 0000000..5aef33f
--- /dev/null
+++ b/src/AcDream.Core.Net/Packets/PacketHeaderFlags.cs
@@ -0,0 +1,35 @@
+namespace AcDream.Core.Net.Packets;
+
+///
+/// Flags field in a 20-byte . Each flag gates the
+/// presence of an optional body section or signals a handshake/control
+/// operation. Reimplemented from ACE's AGPL reference; values are
+/// wire-format facts about AC's retail protocol.
+///
+[Flags]
+public enum PacketHeaderFlags : uint
+{
+ None = 0x00000000,
+ Retransmission = 0x00000001,
+ EncryptedChecksum = 0x00000002,
+ BlobFragments = 0x00000004,
+ ServerSwitch = 0x00000100,
+ LogonServerAddr = 0x00000200,
+ EmptyHeader1 = 0x00000400,
+ Referral = 0x00000800,
+ RequestRetransmit = 0x00001000,
+ RejectRetransmit = 0x00002000,
+ AckSequence = 0x00004000,
+ Disconnect = 0x00008000,
+ LoginRequest = 0x00010000,
+ WorldLoginRequest = 0x00020000,
+ ConnectRequest = 0x00040000,
+ ConnectResponse = 0x00080000,
+ NetError = 0x00100000,
+ NetErrorDisconnect = 0x00200000,
+ CICMDCommand = 0x00400000,
+ TimeSync = 0x01000000,
+ EchoRequest = 0x02000000,
+ EchoResponse = 0x04000000,
+ Flow = 0x08000000,
+}
diff --git a/tests/AcDream.Core.Net.Tests/Cryptography/Hash32Tests.cs b/tests/AcDream.Core.Net.Tests/Cryptography/Hash32Tests.cs
new file mode 100644
index 0000000..9021377
--- /dev/null
+++ b/tests/AcDream.Core.Net.Tests/Cryptography/Hash32Tests.cs
@@ -0,0 +1,75 @@
+using AcDream.Core.Net.Cryptography;
+
+namespace AcDream.Core.Net.Tests.Cryptography;
+
+public class Hash32Tests
+{
+ [Fact]
+ public void Calculate_EmptyInput_ReturnsZero()
+ {
+ Assert.Equal(0u, Hash32.Calculate(ReadOnlySpan.Empty));
+ }
+
+ [Fact]
+ public void Calculate_FourBytes_HandComputedValue()
+ {
+ // length=4 → seed = 4 << 16 = 0x00040000
+ // word = LE(0x01,0x02,0x03,0x04) = 0x04030201
+ // checksum = 0x00040000 + 0x04030201 = 0x04070201
+ var data = new byte[] { 0x01, 0x02, 0x03, 0x04 };
+ Assert.Equal(0x04070201u, Hash32.Calculate(data));
+ }
+
+ [Fact]
+ public void Calculate_FiveBytes_TailByteGoesToHighNibble()
+ {
+ // length=5 → seed = 0x00050000
+ // word0 = LE(0xAA,0xBB,0xCC,0xDD) = 0xDDCCBBAA
+ // checksum after word = 0x00050000 + 0xDDCCBBAA = 0xDDD1BBAA
+ // tail byte: data[4]=0xEE << 24 = 0xEE000000
+ // checksum += 0xEE000000 → 0xDDD1BBAA + 0xEE000000 = 0xCBD1BBAA (wrap)
+ var data = new byte[] { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE };
+ Assert.Equal(0xCBD1BBAAu, Hash32.Calculate(data));
+ }
+
+ [Fact]
+ public void Calculate_SingleTailByte_ShiftsInto24()
+ {
+ // length=1 → seed = 0x00010000
+ // tail: 0x42 << 24 = 0x42000000
+ // checksum = 0x42010000
+ var data = new byte[] { 0x42 };
+ Assert.Equal(0x42010000u, Hash32.Calculate(data));
+ }
+
+ [Fact]
+ public void Calculate_TwoTailBytes_ShiftInto24And16()
+ {
+ // length=2 → seed = 0x00020000
+ // tail0 (0x42) << 24 = 0x42000000
+ // tail1 (0x7F) << 16 = 0x007F0000
+ // total = 0x42020000 + 0x007F0000 = 0x42810000
+ // Wait — order matters. First tail byte shifts by 24 (most significant),
+ // second by 16. So: seed + 0x42000000 + 0x007F0000 = 0x42810000
+ var data = new byte[] { 0x42, 0x7F };
+ Assert.Equal(0x42810000u, Hash32.Calculate(data));
+ }
+
+ [Fact]
+ public void Calculate_ThreeTailBytes_ShiftInto24And16And8()
+ {
+ // length=3 → seed = 0x00030000
+ // tail bytes (0x01, 0x02, 0x03) shifted by 24, 16, 8 respectively:
+ // 0x01000000 + 0x00020000 + 0x00000300 = 0x01020300
+ // total = 0x00030000 + 0x01020300 = 0x01050300
+ var data = new byte[] { 0x01, 0x02, 0x03 };
+ Assert.Equal(0x01050300u, Hash32.Calculate(data));
+ }
+
+ [Fact]
+ public void Calculate_IsDeterministic()
+ {
+ var data = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
+ Assert.Equal(Hash32.Calculate(data), Hash32.Calculate(data));
+ }
+}
diff --git a/tests/AcDream.Core.Net.Tests/Packets/PacketHeaderTests.cs b/tests/AcDream.Core.Net.Tests/Packets/PacketHeaderTests.cs
new file mode 100644
index 0000000..3b1cb0d
--- /dev/null
+++ b/tests/AcDream.Core.Net.Tests/Packets/PacketHeaderTests.cs
@@ -0,0 +1,121 @@
+using AcDream.Core.Net.Packets;
+
+namespace AcDream.Core.Net.Tests.Packets;
+
+public class PacketHeaderTests
+{
+ [Fact]
+ public void PackUnpack_RoundTrip_PreservesAllFields()
+ {
+ var original = new PacketHeader
+ {
+ Sequence = 0x11223344u,
+ Flags = PacketHeaderFlags.EncryptedChecksum | PacketHeaderFlags.LoginRequest,
+ Checksum = 0xDEADBEEFu,
+ Id = 0x1234,
+ Time = 0x5678,
+ DataSize = 256,
+ Iteration = 3,
+ };
+
+ Span buffer = stackalloc byte[PacketHeader.Size];
+ original.Pack(buffer);
+ var decoded = PacketHeader.Unpack(buffer);
+
+ Assert.Equal(original.Sequence, decoded.Sequence);
+ Assert.Equal(original.Flags, decoded.Flags);
+ Assert.Equal(original.Checksum, decoded.Checksum);
+ Assert.Equal(original.Id, decoded.Id);
+ Assert.Equal(original.Time, decoded.Time);
+ Assert.Equal(original.DataSize, decoded.DataSize);
+ Assert.Equal(original.Iteration, decoded.Iteration);
+ }
+
+ [Fact]
+ public void Pack_WritesLittleEndianWireFormat()
+ {
+ var header = new PacketHeader
+ {
+ Sequence = 0x04030201u, // LE wire: 01 02 03 04
+ Flags = (PacketHeaderFlags)0x08070605u, // LE: 05 06 07 08
+ Checksum = 0x0C0B0A09u, // LE: 09 0A 0B 0C
+ Id = 0x0E0D, // LE: 0D 0E
+ Time = 0x100F, // LE: 0F 10
+ DataSize = 0x1211, // LE: 11 12
+ Iteration = 0x1413, // LE: 13 14
+ };
+
+ Span buf = stackalloc byte[20];
+ header.Pack(buf);
+
+ byte[] expected = new byte[]
+ {
+ 0x01, 0x02, 0x03, 0x04,
+ 0x05, 0x06, 0x07, 0x08,
+ 0x09, 0x0A, 0x0B, 0x0C,
+ 0x0D, 0x0E,
+ 0x0F, 0x10,
+ 0x11, 0x12,
+ 0x13, 0x14,
+ };
+ Assert.Equal(expected, buf.ToArray());
+ }
+
+ [Fact]
+ public void HasFlag_DetectsSingleBit()
+ {
+ var h = new PacketHeader { Flags = PacketHeaderFlags.EncryptedChecksum | PacketHeaderFlags.TimeSync };
+ Assert.True(h.HasFlag(PacketHeaderFlags.EncryptedChecksum));
+ Assert.True(h.HasFlag(PacketHeaderFlags.TimeSync));
+ Assert.False(h.HasFlag(PacketHeaderFlags.LoginRequest));
+ }
+
+ [Fact]
+ public void CalculateHeaderHash32_UsesBaddSentinelNotActualChecksum()
+ {
+ // The key property: two headers that differ ONLY in the Checksum field
+ // MUST produce the same CalculateHeaderHash32 output — the function
+ // substitutes the sentinel BADD70DD value before hashing.
+ var a = new PacketHeader { Sequence = 42, Flags = PacketHeaderFlags.None, Checksum = 0x11111111u };
+ var b = new PacketHeader { Sequence = 42, Flags = PacketHeaderFlags.None, Checksum = 0xDEADBEEFu };
+ Assert.Equal(a.CalculateHeaderHash32(), b.CalculateHeaderHash32());
+ }
+
+ [Fact]
+ public void CalculateHeaderHash32_DoesNotMutateHeaderChecksum()
+ {
+ var h = new PacketHeader { Checksum = 0x11111111u };
+ _ = h.CalculateHeaderHash32();
+ Assert.Equal(0x11111111u, h.Checksum);
+ }
+
+ [Fact]
+ public void CalculateHeaderHash32_DeterministicForSameInput()
+ {
+ var h = new PacketHeader
+ {
+ Sequence = 1,
+ Flags = PacketHeaderFlags.EncryptedChecksum,
+ Checksum = 0xDEADBEEFu,
+ Id = 0x1234,
+ Time = 0x5678,
+ DataSize = 32,
+ Iteration = 0,
+ };
+ Assert.Equal(h.CalculateHeaderHash32(), h.CalculateHeaderHash32());
+ }
+
+ [Fact]
+ public void Unpack_InsufficientData_Throws()
+ {
+ Assert.Throws(() => PacketHeader.Unpack(new byte[19]));
+ }
+
+ [Fact]
+ public void Pack_InsufficientDestination_Throws()
+ {
+ var h = new PacketHeader();
+ var buf = new byte[19];
+ Assert.Throws(() => h.Pack(buf));
+ }
+}