diff --git a/AcDream.slnx b/AcDream.slnx
index fa37fc2..cd884f7 100644
--- a/AcDream.slnx
+++ b/AcDream.slnx
@@ -3,11 +3,13 @@
+
+
diff --git a/src/AcDream.Core.Net/AcDream.Core.Net.csproj b/src/AcDream.Core.Net/AcDream.Core.Net.csproj
new file mode 100644
index 0000000..68dd27c
--- /dev/null
+++ b/src/AcDream.Core.Net/AcDream.Core.Net.csproj
@@ -0,0 +1,10 @@
+
+
+ net10.0
+ enable
+ enable
+ latest
+ true
+ AcDream.Core.Net
+
+
diff --git a/src/AcDream.Core.Net/Cryptography/IsaacRandom.cs b/src/AcDream.Core.Net/Cryptography/IsaacRandom.cs
new file mode 100644
index 0000000..2b99bce
--- /dev/null
+++ b/src/AcDream.Core.Net/Cryptography/IsaacRandom.cs
@@ -0,0 +1,172 @@
+namespace AcDream.Core.Net.Cryptography;
+
+///
+/// Asheron's Call variant of Bob Jenkins' ISAAC keystream generator. AC's
+/// client and server each seed an ISAAC instance with a 4-byte value shared
+/// during the connect handshake, then XOR the keystream into the CRC field of
+/// each outbound packet so the peer can both verify the packet is authentic
+/// and recover the CRC for the real checksum comparison.
+///
+///
+/// Algorithm overview (public ISAAC from Bob Jenkins, 1996):
+///
+/// - Internal state is 256 uint32 mm[] words plus a,
+/// b, c registers and a 256-word output buffer
+/// randRsl[].
+/// - Initialization mixes mm[] via 4 rounds of Jenkins' golden-
+/// ratio shuffle on 8-word state, then folds in the seed-derived
+/// randRsl[] and mm[] words via two more passes.
+/// - Scramble() produces 256 output words per call, consumed in
+/// reverse order (offset 255 → 0) by Next(). When the buffer
+/// is drained the scramble re-runs.
+/// - AC variant: seed is exactly 4 bytes (the UDP-handshake ISAAC seed
+/// from the server). Those 4 bytes are interpreted as a
+/// little-endian uint32 and assigned to a = b = c before the
+/// first scramble. The rest of mm[] and randRsl[]
+/// start at zero.
+///
+///
+///
+///
+/// This is a clean-room reimplementation based on reading the public
+/// algorithm description and ACE's AGPL reference implementation at
+/// references/ACE/Source/ACE.Common/Cryptography/ISAAC.cs. See
+/// NOTICE.md for attribution. No code is copied.
+///
+///
+public sealed class IsaacRandom
+{
+ private const int StateSize = 256;
+ private const uint GoldenRatio = 0x9E3779B9u;
+
+ private readonly uint[] _mm = new uint[StateSize];
+ private readonly uint[] _rsl = new uint[StateSize];
+ private uint _a;
+ private uint _b;
+ private uint _c;
+
+ /// Current position in .
+ /// consumes values from index _offset down to 0 then re-scrambles.
+ private int _offset;
+
+ ///
+ /// Construct a new keystream seeded with the 4 bytes starting at
+ /// [0]. The bytes are interpreted as a
+ /// little-endian uint32 and fed into the ISAAC initialization such that
+ /// two instances with the same seed produce identical output sequences.
+ ///
+ public IsaacRandom(ReadOnlySpan seedBytes)
+ {
+ if (seedBytes.Length < 4)
+ throw new ArgumentException("seed must be at least 4 bytes", nameof(seedBytes));
+
+ Initialize();
+
+ uint seed = (uint)seedBytes[0]
+ | ((uint)seedBytes[1] << 8)
+ | ((uint)seedBytes[2] << 16)
+ | ((uint)seedBytes[3] << 24);
+ _a = _b = _c = seed;
+
+ Scramble();
+ _offset = StateSize - 1;
+ }
+
+ ///
+ /// Return the next uint32 of the keystream. Cheap — amortized one array
+ /// read per call, with a scramble round every 256 calls.
+ ///
+ public uint Next()
+ {
+ uint value = _rsl[_offset];
+ if (_offset > 0)
+ {
+ _offset--;
+ }
+ else
+ {
+ Scramble();
+ _offset = StateSize - 1;
+ }
+ return value;
+ }
+
+ ///
+ /// Run the Jenkins golden-ratio shuffle on the supplied 8-word state.
+ /// Used during initialization to stir mm[] and randRsl[].
+ /// Each line is a mix step from the reference; the constants are the
+ /// Jenkins-published avalanche offsets.
+ ///
+ private static void Mix(Span s)
+ {
+ s[0] ^= s[1] << 11; s[3] += s[0]; s[1] += s[2];
+ s[1] ^= s[2] >> 2; s[4] += s[1]; s[2] += s[3];
+ s[2] ^= s[3] << 8; s[5] += s[2]; s[3] += s[4];
+ s[3] ^= s[4] >> 16; s[6] += s[3]; s[4] += s[5];
+ s[4] ^= s[5] << 10; s[7] += s[4]; s[5] += s[6];
+ s[5] ^= s[6] >> 4; s[0] += s[5]; s[6] += s[7];
+ s[6] ^= s[7] << 8; s[1] += s[6]; s[7] += s[0];
+ s[7] ^= s[0] >> 9; s[2] += s[7]; s[0] += s[1];
+ }
+
+ private void Initialize()
+ {
+ // mm[] and rsl[] start as all zeroes.
+ Span s = stackalloc uint[8];
+ for (int i = 0; i < 8; i++) s[i] = GoldenRatio;
+
+ // 4 warmup rounds so the initial state diverges from the golden-ratio
+ // pattern before we start folding in real values.
+ for (int i = 0; i < 4; i++) Mix(s);
+
+ // First pass folds _rsl (zeroes on a fresh instance) into mm[].
+ for (int j = 0; j < StateSize; j += 8)
+ {
+ for (int k = 0; k < 8; k++) s[k] += _rsl[j + k];
+ Mix(s);
+ for (int k = 0; k < 8; k++) _mm[j + k] = s[k];
+ }
+
+ // Second pass folds mm[] (now populated) back into itself.
+ for (int j = 0; j < StateSize; j += 8)
+ {
+ for (int k = 0; k < 8; k++) s[k] += _mm[j + k];
+ Mix(s);
+ for (int k = 0; k < 8; k++) _mm[j + k] = s[k];
+ }
+ }
+
+ ///
+ /// Produce the next 256 output words into . Consumed
+ /// in reverse by . Each iteration rotates a
+ /// through one of four avalanche shifts depending on i & 3,
+ /// pulls an indirection from the far half of the state, and produces the
+ /// next output + next state word.
+ ///
+ private void Scramble()
+ {
+ _c++;
+ _b += _c;
+
+ for (int i = 0; i < StateSize; i++)
+ {
+ uint x = _mm[i];
+ switch (i & 3)
+ {
+ case 0: _a ^= _a << 13; break;
+ case 1: _a ^= _a >> 6; break;
+ case 2: _a ^= _a << 2; break;
+ case 3: _a ^= _a >> 16; break;
+ }
+
+ _a += _mm[(i + 128) & 0xFF];
+
+ uint y = _mm[(int)((x >> 2) & 0xFF)] + _a + _b;
+ _mm[i] = y;
+
+ uint nextB = _mm[(int)((y >> 10) & 0xFF)] + x;
+ _rsl[i] = nextB;
+ _b = nextB;
+ }
+ }
+}
diff --git a/src/AcDream.Core.Net/NOTICE.md b/src/AcDream.Core.Net/NOTICE.md
new file mode 100644
index 0000000..b0e6738
--- /dev/null
+++ b/src/AcDream.Core.Net/NOTICE.md
@@ -0,0 +1,25 @@
+# AcDream.Core.Net — Protocol Reference Attribution
+
+This project implements the Asheron's Call network protocol for acdream.
+The wire format is a fact about the retail AC client/server and cannot be
+copyrighted, but the specific implementation details and structure conventions
+are informed by reading the following open-source projects:
+
+- **ACE** (ACEmulator / `references/ACE/Source/ACE.Server/Network/` and
+ `references/ACE/Source/ACE.Common/Cryptography/ISAAC.cs`) — AGPL-3.0.
+ The authoritative server-side reference for packet framing, ISAAC keystream
+ generation, fragment reassembly, and GameMessage opcodes.
+
+- **ac-protocol** (holtburger's Rust crate) — AGPL-3.0. Confirms the handshake
+ works in practice.
+
+No code is copied from these sources. Algorithms are reimplemented from scratch
+in acdream's own style after reading and understanding the reference. Where
+golden test vectors are derived from running the reference implementations,
+those vectors are factual outputs of the algorithm, not copyrighted expression,
+and appear only in test code.
+
+If you plan to redistribute acdream outside personal use, consult a lawyer
+about the interaction between our chosen license and the AGPL heritage of the
+references we read. (We didn't at the time of writing — see the project-level
+licensing notes.)
diff --git a/tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj b/tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj
new file mode 100644
index 0000000..88c158a
--- /dev/null
+++ b/tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/AcDream.Core.Net.Tests/Cryptography/IsaacRandomTests.cs b/tests/AcDream.Core.Net.Tests/Cryptography/IsaacRandomTests.cs
new file mode 100644
index 0000000..9a9719c
--- /dev/null
+++ b/tests/AcDream.Core.Net.Tests/Cryptography/IsaacRandomTests.cs
@@ -0,0 +1,79 @@
+using AcDream.Core.Net.Cryptography;
+
+namespace AcDream.Core.Net.Tests.Cryptography;
+
+public class IsaacRandomTests
+{
+ ///
+ /// Golden vectors derived from ACE's reference ISAAC implementation
+ /// (references/ACE/Source/ACE.Common/Cryptography/ISAAC.cs) with seed
+ /// bytes [0x78, 0x56, 0x34, 0x12] (= 0x12345678 as little-endian uint32).
+ /// Ran a throwaway oracle harness that compiles ACE's ISAAC.cs + a Main
+ /// that calls Next() 16 times and prints the values; the numbers below
+ /// are factual outputs of that algorithm, not copied code.
+ ///
+ private static readonly uint[] GoldenVectorsSeed12345678 =
+ {
+ 0xFDD4AFEBu, 0x398311D2u, 0xC71829A4u, 0xB19DF96Au,
+ 0x7ED4A1E7u, 0xB718A446u, 0x5FA77DF1u, 0x5CB793D2u,
+ 0x52C95BACu, 0xA8F8FD7Eu, 0x18582C04u, 0xD257F506u,
+ 0x4D11A3F0u, 0x919DE7A7u, 0x1D809C6Bu, 0xE97F65F8u,
+ };
+
+ [Fact]
+ public void Next_Seed12345678_MatchesAceGoldenVectors()
+ {
+ var seed = new byte[] { 0x78, 0x56, 0x34, 0x12 };
+ var isaac = new IsaacRandom(seed);
+
+ for (int i = 0; i < GoldenVectorsSeed12345678.Length; i++)
+ {
+ Assert.Equal(GoldenVectorsSeed12345678[i], isaac.Next());
+ }
+ }
+
+ [Fact]
+ public void Next_TwoInstancesSameSeed_ProduceIdenticalSequence()
+ {
+ var seed = new byte[] { 0xAA, 0xBB, 0xCC, 0xDD };
+ var a = new IsaacRandom(seed);
+ var b = new IsaacRandom(seed);
+
+ for (int i = 0; i < 1000; i++)
+ {
+ Assert.Equal(a.Next(), b.Next());
+ }
+ }
+
+ [Fact]
+ public void Next_DifferentSeeds_ProduceDifferentFirstOutput()
+ {
+ var a = new IsaacRandom(new byte[] { 1, 0, 0, 0 });
+ var b = new IsaacRandom(new byte[] { 2, 0, 0, 0 });
+ Assert.NotEqual(a.Next(), b.Next());
+ }
+
+ [Fact]
+ public void Next_512Calls_SpansTwoScrambleBatches()
+ {
+ // Two full scramble rounds (256 outputs each). Tests that the
+ // scramble boundary at offset 0 → offset 255 doesn't emit garbage or
+ // repeat values trivially.
+ var isaac = new IsaacRandom(new byte[] { 0x01, 0x02, 0x03, 0x04 });
+ var outputs = new uint[512];
+ for (int i = 0; i < 512; i++) outputs[i] = isaac.Next();
+
+ // Not a statistical test — just catch obvious bugs like "all zero"
+ // or "stuck at one value". A healthy PRNG produces far more than
+ // 100 distinct values in 512 calls.
+ var distinct = new HashSet(outputs);
+ Assert.True(distinct.Count > 400,
+ $"Expected >400 distinct values in 512 outputs, got {distinct.Count}");
+ }
+
+ [Fact]
+ public void Ctor_ShortSeed_Throws()
+ {
+ Assert.Throws(() => new IsaacRandom(new byte[] { 1, 2, 3 }));
+ }
+}