Commit graph

4 commits

Author SHA1 Message Date
Erik
f9c5e47e7f feat(net): N6 - ConnectResponse retransmit + fragment assembler eviction
Campaign N Slice N6, the final implementation slice.

ConnectResponse handshake retransmit:
- While the connection is unconfirmed, the Connect character-list pump
  resends the IDENTICAL cleartext ConnectResponse (same sequence 1, same
  cookie, the one encoded datagram - no new outbound state) on retail's
  strict 0.333333333 s gate. Retail: ClientNet::ProcessConnection
  @ 0x00545450, case cs_ConnectionRequestAcked @ 0x0054547B (the constant
  load at 0x00545481; the mask-0x41 strictly-greater x87 test at
  0x0054548C); ClientNet::SendConnectAck @ 0x005440F0 re-stamps
  lastSentHandshake_ (0x00544102) and rebuilds the same cookie packet.
- Confirmation = the first checksum-valid post-negotiation packet whose
  header lacks the ConnectRequest flag: retail's cs_ConnectionRequestAcked
  -> cs_Connected edge (ClientNet::ProcessPacket @ 0x00545100, the 0x40000
  exclusion at 0x0054514E, SetConnectionState(..., 5) at 0x00545160).
- The cadence rides the TransportClock (virtual-clock testable through
  TransportClockSource); the Connect deadline stays wall-clock.
- ACE safety pinned against the N0 model: a duplicate while still
  AuthConnectResponse re-routes idempotently through NetworkManager's
  pre-route; after acceptance CheckState clause 2 drops it pre-CRC at
  zero keystream cost.
- Pre-N6, one lost ConnectResponse was a hang to the Connect deadline;
  the N5 decorator deliberately arms after this window, so nothing
  covered it.

FragmentAssembler eviction (divergence register row AD-52):
- Partials evict 60 s after their last ACCEPTED fragment; the stamp
  refreshes on every new fragment (retail's re-stamp rule,
  ArrivedEphInfo::UpdateNetBlobID @ 0x0054AE00), so a merely-slow partial
  can never age out - 60 s is a floor, not a tunable. Swept from
  ReliableTransport.Sweep on retail's 5 s flush cadence
  (Indicator::FlushTimedOutEphInfo @ 0x0054A3D0, the gate at 0x0054A3DC;
  per-entry ArrivedEphInfo::fTimedOut @ 0x0054AE30). N4's RejectRetransmit
  abandonment made an unrecoverable partial a REACHABLE permanent state;
  the TTL reclaims it.
- A 64-entry completed-sequence ring drops late duplicate fragments of
  already-completed messages instead of allocating a fresh partial that
  can never complete (the completed-then-duplicate leak).

Fold-ins:
- N5 review LOW-5: NetProbeTests + LossyTransportDecoratorTests (the
  static NetDiagnostics / Console.SetOut mutators) share one
  DisableParallelization xunit collection so they never run alongside
  classes constructing WorldSession.
- Campaign section 9: N6 ledger row recorded; N5 row verified carrying
  4e290f00.

Gates: 757 Core.Net Release tests green (10 new); full solution Release
green (0 failures / 5 skips); connected lifecycle gate PASS; the
N5-strengthened connected loss gate PASS on its first live run (2%/seed 1:
dropped out=3 in=10, resends=1 nak-in=1 nak-out=5, cksum-fail=0
sanity-drop=0 uncached-nak=0).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 17:20:12 +02:00
Erik
e928c5dd02 perf(net): borrow inbound packet storage
Decode headers, optional fields, fragments, and single-fragment messages directly over pooled datagrams. Copy only fragment state that crosses a datagram lifetime, preserve synchronous dispatch and ACK ordering, and lock the path to the owned decoder with differential and zero-allocation tests.
2026-07-25 05:58:55 +02:00
Erik
94da385ff4 feat(net): acdream enters the world — CharacterList parsed + CharacterEnterWorld sent + 68 CreateObject received (Phase 4.7)
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>
2026-04-11 15:14:31 +02:00
Erik
3226c4bcab feat(net): message fragment header + fragment + assembler (Phase 4.3)
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>
2026-04-11 14:20:53 +02:00