feat(net): extract GameMessage opcodes from live fragment stream (Phase 4.6f)

Reassembles the fragments arriving from the live handshake into full
game message bodies, reads the opcode from the first 4 bytes, and
identifies them by name. On the live wire we now see exactly the
sequence ACE sends right after HandleConnectResponse:

  GameMessage assembled: opcode=0xF7E5 (DDDInterrogation), body=28 bytes
  GameMessage assembled: opcode=0xF658 (CharacterList),    body=80 bytes
  GameMessage assembled: opcode=0xF7E1 (ServerName),       body=20 bytes

  summary: 5 packets received, 5 decoded OK, 0 checksum failures,
           3 GameMessages assembled

Every layer of the net stack is now proven live:
  * NetClient send/receive on both ports 9000 and 9001
  * PacketCodec.Encode building LoginRequest + ConnectResponse with
    correct unencrypted CRC
  * IsaacRandom byte-compatible with ACE's ISAAC (3 EncryptedChecksum
    packets decoded, zero mismatches)
  * PacketHeaderOptional parsing ConnectRequest, TimeSync, AckSequence
  * MessageFragment.TryParse walking a body tail of back-to-back
    fragments (the 152-byte packet had TWO messages: CharacterList
    and ServerName packed into one datagram)
  * FragmentAssembler reassembling by index

The CharacterList body has our test character +Acdream inside it but
we're not decoding its fields yet — that's Phase 4.7 where we actually
pick a character and send CharacterLogin to enter the game world.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-11 14:53:08 +02:00
parent a961d842d4
commit 0aea24c78e

View file

@ -197,12 +197,15 @@ public class LiveHandshakeTests
$"inbound.next=0x{new IsaacRandom(serverSeedBytes).Next():X8}, " +
$"outbound.next=0x{new IsaacRandom(clientSeedBytes).Next():X8}");
// Step 4: receive post-handshake traffic. We expect EncryptedChecksum
// packets containing CharacterList and friends. If our ISAAC seed is
// wrong or our CRC math is off, TryDecode will return ChecksumMismatch.
// Step 4: receive post-handshake traffic. Run fragments through a
// FragmentAssembler so multi-packet game messages reassemble, then
// read the opcode (first 4 bytes of the assembled body) to prove
// we're looking at real GameMessage opcodes.
var assembler = new FragmentAssembler();
int postHandshakePackets = 0;
int successfullyDecoded = 0;
int checksumFailures = 0;
var seenOpcodes = new List<uint>();
var postDeadline = DateTime.UtcNow + TimeSpan.FromSeconds(5);
while (DateTime.UtcNow < postDeadline)
{
@ -214,13 +217,34 @@ public class LiveHandshakeTests
$"decode={decoded.Error}, flags={decoded.Packet?.Header.Flags}, " +
$"seq={decoded.Packet?.Header.Sequence}");
if (decoded.IsOk)
{
successfullyDecoded++;
foreach (var frag in decoded.Packet!.Fragments)
{
var completeBody = assembler.Ingest(frag, out _);
if (completeBody is not null && completeBody.Length >= 4)
{
uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(completeBody);
seenOpcodes.Add(opcode);
string name = opcode switch
{
0xF658 => "CharacterList",
0xF7E1 => "ServerName",
0xF7E5 => "DDDInterrogation",
_ => "unknown",
};
Console.WriteLine($"[live] GameMessage assembled: opcode=0x{opcode:X8} ({name}), " +
$"body={completeBody.Length} bytes");
}
}
}
else if (decoded.Error == PacketCodec.DecodeError.ChecksumMismatch)
checksumFailures++;
}
Console.WriteLine($"[live] step 4 summary: {postHandshakePackets} packets received, " +
$"{successfullyDecoded} decoded OK, {checksumFailures} checksum failures");
$"{successfullyDecoded} decoded OK, {checksumFailures} checksum failures, " +
$"{seenOpcodes.Count} GameMessages assembled");
// The contract of Phase 4.6e is "server accepted our ConnectResponse
// and started streaming". Any post-handshake traffic at all proves