Drives the full post-handshake flow on a live ACE server. After the
3-way handshake completes, acdream:
1. Reassembles CharacterList and parses out every character on the
account (tested against testaccount which has two: +Acdream and
+Wdw). Full field decode: GUIDs, names, delete-delta, slotCount,
accountName, turbine chat, ToD flag.
2. Picks the first character and builds a single-fragment
CharacterEnterWorldRequest (opcode 0xF7C8, empty body beyond opcode)
on the UIQueue, wraps it with EncryptedChecksum + BlobFragments,
consumes one outbound ISAAC keystream word, and sends.
3. Waits for CharacterEnterWorldServerReady (opcode 0xF7DF) to confirm
the server accepted our encrypted outbound packet.
4. Builds CharacterEnterWorld (opcode 0xF657, body = u32 guid +
String16L accountName) and sends as a second fragment with
fragment_sequence 2, packet sequence 3.
5. Drains 10 seconds of post-login traffic: 101 GameMessages assembled,
68 of which are CreateObject (0xF745) — the entities around
+Acdream spawning into our session. Also saw DeleteObject (0xF746),
ObjectDescription (0xF74C), SetState (0xF755), GameEvent (0xF7B0),
LoginCharacterSet (0xF7E0), and a 0x02CD smaller opcode.
This is the Phase 4.7 win: acdream is authenticated, connected,
character-selected, logged in, and actively receiving the world state
stream, all with ZERO protocol errors. Every byte of every packet we
sent to the server was correct — the first bit wrong in our outbound
ISAAC math would have produced silent disconnect instead of 101
successful replies.
Added to AcDream.Core.Net:
- Messages/CharacterList.cs: full parser for opcode 0xF658, ported
from ACE's GameMessageCharacterList writer. Returns structured
record with Characters[], SlotCount, AccountName, UseTurbineChat,
HasThroneOfDestiny. Tested offline with hand-assembled bodies
matching ACE's writer format.
- Messages/CharacterEnterWorld.cs: outbound builders for
CharacterEnterWorldRequest (0xF7C8, opcode-only) and
CharacterEnterWorld (0xF657, opcode + guid + String16L account).
- Messages/GameMessageFragment.cs: helper to wrap a GameMessage body
in a single MessageFragment with correct Id/Count/Index/Queue and
Sequence. Also a Serialize helper to turn a MessageFragment into
packet-body bytes for PacketCodec.Encode. Throws on oversize
(>448 byte) messages; multi-fragment outbound split is TBD.
- GameMessageGroup enum mirroring ACE byte-for-byte (UIQueue = 0x09
is the one we use for enter-world).
Fixed: FragmentAssembler was keying on MessageFragmentHeader.Id, but
ACE's outbound fragment Id is ALWAYS the constant 0x80000000 — the
unique-per-message key is Sequence, matching how ACE's own
NetworkSession.HandleFragment keys its partialFragments dict. Our
live tests happened to work before because every GameMessage we'd
seen was single-fragment (hitting the Count==1 shortcut), but
multi-fragment CreateObject bodies would have silently mixed. Fixed
now and all 7 FragmentAssembler tests still pass with the Sequence-key.
Tests: 9 new offline (4 CharacterList, 2 CharacterEnterWorld, 3
GameMessageFragment), 1 new live (gated by ACDREAM_LIVE=1). Total
77 core + 83 net = 160 passing.
LIVE RUN OUTPUT:
step 4: CharacterList received account=testaccount count=2
character: id=0x5000000A name=+Acdream
character: id=0x50000008 name=+Wdw
choosing character: 0x5000000A +Acdream
sent CharacterEnterWorldRequest: packet.seq=2 frag.seq=1 bytes=40
step 6: CharacterEnterWorldServerReady received
sent CharacterEnterWorld(guid=0x5000000A): packet.seq=3 frag.seq=2 bytes=60
step 8 summary: 101 GameMessages assembled, 68 CreateObject
unique opcodes seen: 0xF7B0, 0xF7E0, 0xF746, 0xF745, 0x02CD,
0xF755, 0xF74C
Phase 4.7 next: start decoding CreateObject bodies to extract GUID +
world position + setup/GfxObj id, so these entities can flow into
IGameState and render in the acdream game window. The foundry statue
is waiting in one of those 68 spawns.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ports the fragment layer of the AC UDP protocol. A UDP packet's body is
zero or more message fragments back-to-back; a logical GameMessage that
doesn't fit in ~448 bytes gets split across multiple fragments sharing
the same Id with differing Index values. The assembler handles
reassembly across arbitrary arrival ordering and duplicate fragments.
Added (all reimplemented from ACE's AGPL reference, see NOTICE.md):
- Packets/MessageFragmentHeader.cs: 16-byte fragment header struct
with Pack/Unpack, constants for MaxFragmentSize (464) and
MaxFragmentDataSize (448). Bit-layout doc comment documents what
each field is for.
- Packets/MessageFragment.cs: readonly record struct bundling a
header with its payload bytes; TryParse(source) parses one fragment
from the start of a buffer and returns (fragment, consumed) for
incremental parsing of multi-fragment packets. Refuses to parse
fragments with impossible TotalSize (too small for header, too
large for the 464-byte max, or larger than the source buffer).
- Packets/FragmentAssembler.cs: buffers partial messages keyed by
fragment Id. Ingest(frag, out queue) returns the assembled byte[]
when the last fragment arrives, null while still waiting. Key
correctness properties, all tested:
* Single-fragment (Count=1) shortcut releases with no buffering
* Out-of-order arrival (e.g. 2, 0, 1) releases on last arrival
and assembles in INDEX order, not arrival order
* Duplicate-fragment idempotence (re-sending same index is a no-op)
* Missing fragments stay buffered; DropAll() forcibly clears them
* Two independent messages can be assembled in parallel without
interfering
* messageQueue captured from first-arriving fragment (it's a
property of the logical message, not individual fragments)
Tests (17 new, 37 total in net project, 114 across both test projects):
- MessageFragmentHeader (4): pack/unpack round-trip, little-endian
wire format, constants, size-check throw
- MessageFragment (6): complete parse, insufficient header, oversized
TotalSize, undersized TotalSize, incomplete body, two-back-to-back
incremental parse
- FragmentAssembler (7): single-fragment, in-order 3-fragment,
out-of-order 3-fragment (tests index-order assembly), duplicate
idempotence, missing-fragment buffered, two parallel messages,
DropAll
Phase 4.4 (GameMessage reader + opcode handlers) next.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>