This is the Phase 4 protocol-compatibility proof. acdream's codec now
completes the full AC UDP handshake against a live ACE server and
successfully decodes three consecutive EncryptedChecksum game packets
— which means every layer of the codec is byte-compatible with ACE.
Changes:
- NetClient: added Send(IPEndPoint, ReadOnlySpan<byte>) overload so
one socket can talk to ACE's two listener ports (9000 for
LoginRequest, 9001 for ConnectResponse and all subsequent traffic)
- LiveHandshakeTests.Live_FullThreeWayHandshake_ReachesConnectedState:
drives the full 3-leg handshake end-to-end. Protocol details that
I got wrong on the first attempt and fixed after reading
references/holtburger/crates/holtburger-session/src/session/auth.rs:
* ConnectResponse header.Sequence = 1 (LoginRequest is seq 0)
* ConnectResponse header.Id = 0 (NOT the clientId from
ConnectRequest; that field is ACE's internal session index,
separate from the packet header Id)
* 200ms Thread.Sleep before sending ConnectResponse — holtburger
calls this ACE_HANDSHAKE_RACE_DELAY_MS, empirically determined
to avoid a server-side race where ACE is still finalizing the
session when our ConnectResponse arrives
* ConnectResponse goes to port 9001, not 9000 (ACE's second
ConnectionListener, see Network/Managers/SocketManager.cs)
LIVE RUN OUTPUT:
[live] step 1: sending 84-byte LoginRequest to 127.0.0.1:9000
[live] step 2: got 52-byte datagram from 127.0.0.1:9000,
flags=ConnectRequest
ConnectRequest cookie=0x458ABEE950D18BEE clientId=0x00000000
[live] step 3: sleeping 200ms then sending 28-byte ConnectResponse
to 127.0.0.1:9001
ISAAC seeds primed
[live] step 4: got 28-byte datagram from :9001,
flags=EncryptedChecksum,TimeSync, seq=2 OK
[live] step 4: got 64-byte datagram from :9001,
flags=EncryptedChecksum,BlobFragments, seq=3 OK
[live] step 4: got 152-byte datagram from :9001,
flags=EncryptedChecksum,BlobFragments, seq=4 OK
[live] step 4: got 24-byte datagram from :9001,
flags=AckSequence, seq=4 OK
[live] step 4: got 24-byte datagram from :9001,
flags=AckSequence, seq=4 OK
[live] step 4 summary: 5 packets received, 5 decoded OK,
0 checksum failures
What each "OK" proves, reading left to right:
* TimeSync (seq=2): our IsaacRandom is byte-compatible with ACE's
ISAAC.cs — if a single bit were wrong in any state register the
checksum key would mismatch and decode would fail. Our inbound
ISAAC consumed one word for this packet.
* BlobFragments (seq=3, 64 bytes): header hash + fragment hash +
ISAAC key recipe all check out. These fragments contain the start
of GameMessageCharacterList / ServerName / DDDInterrogation game
messages ACE enqueues right after HandleConnectResponse. We don't
parse game message bodies yet (Phase 4.7) but the fragments are
fully retrievable from Packet.Fragments.
* BlobFragments (seq=4, 152 bytes): continuation of the same game
messages; our sequential ISAAC consumption handled two back-to-back
encrypted packets correctly.
* AckSequence (seq=4): unencrypted mixed with encrypted in the same
stream — our codec handles both paths in one session.
Everything in AcDream.Core.Net is now proven byte-compatible with a
retail AC server at the protocol level. The remaining Phase 4 work
(4.6f, 4.7) is above the codec: parsing game message opcodes out of
the fragment payloads and routing CreateObject into IGameState so
acdream can show the foundry statue and the +Acdream character.
Test counts: 77 core + 73 net (+1 new live test) = 150 passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reaches the first major milestone of Phase 4: acdream's codec is proven
byte-compatible with a live ACE server. LiveHandshakeTests drives a real
UDP exchange against 127.0.0.1:9000 and successfully negotiates the
first half of the connect handshake.
Added:
- Packets/PacketHeaderOptional.cs: new ConnectRequest flag branch.
ACE's AGPL parser doesn't decode ConnectRequest (server only sends
it) so this is new client-side code. Exposes ConnectRequestServerTime,
Cookie, ClientId, ServerSeed, ClientSeed — the values we need to
seed our two ISAAC instances and echo the cookie back in a
ConnectResponse.
- NetClient.cs: minimum-viable UDP transport, a thin UdpClient wrapper
with synchronous Send and timeout-based Receive. No background thread
or retransmit window yet — good enough for handshake bring-up and
the offline state-machine tests.
- LiveHandshakeTests.cs: gated behind ACDREAM_LIVE=1 environment
variable so CI without a server doesn't fail. Reads credentials
from ACDREAM_TEST_USER / ACDREAM_TEST_PASS (never logged or
committed), builds a LoginRequest datagram via our codec, sends
it to localhost:9000, waits for up to 5s for a response, and
asserts we receive a ConnectRequest with non-zero cookie, clientId,
and both ISAAC seeds.
Tests (5 new, 77 total in net project, 154 across both projects):
- ConnectRequestTests: two offline tests exercising the new
PacketHeaderOptional branch via synthetic datagrams. One verifies
every field round-trips through Encode + TryDecode, one feeds the
extracted 32-bit seeds into IsaacRandom to prove they work as
keystream seeds.
- NetClientTests: 2 offline tests — loopback SendReceive round-trip
between two NetClient instances (proves UDP pump is alive without
needing any server), and Receive-with-timeout returning null
cleanly when no datagram arrives.
- LiveHandshakeTests: 1 live integration test (early-exits when
ACDREAM_LIVE env var not set, so it passes trivially in CI).
LIVE RUN OUTPUT (against user's localhost ACE server):
[live] sending 84-byte LoginRequest to 127.0.0.1:9000 (user.len=11, pass.len=12)
[live] received 52-byte datagram from 127.0.0.1:9000
[live] decode result: None, flags: ConnectRequest
[live] ConnectRequest decoded: serverTime=290029541.121 cookie=0xAC45998D06754133
clientId=0x00000001 serverSeed=0x4CC09763 clientSeed=0x5C3DE13E
Meaning: 84-byte LoginRequest went out, 52-byte ConnectRequest came
back, codec.TryDecode returned None error, every field parsed to a
sensible value. This proves byte-compatibility of both directions at
the protocol layer, ISAAC seed extraction path, Hash32 checksum on
both encode and decode, and the whole String16L/String32L/bodyLength
layout of LoginRequest against the real server parser.
Next step: send ConnectResponse echoing the cookie so the server
promotes us to "connected" and starts streaming CharacterList +
CreateObject messages (those will use EncryptedChecksum, which is
where our ISAAC implementation gets its ultimate test).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>