Campaign N Slice N1 (docs/plans/2026-07-29-network-transport-campaign.md
S2.1) - the direct #260 fix: every sent reliable packet is now cached and
re-emitted, header-rebuilt, when ACE NAKs a client-sequence gap. One lost
C2S datagram no longer voids every subsequent action for the session's
lifetime.
New src/AcDream.Core.Net/Transport/:
- TransportClock: injectable monotonic source + retail's 0.5 s interval
counter (ClientFlowQueue::IncrementLocalInterval @ 0x00547F10, tail
`intervalID_ += elapsed`; the same function's ~3 s TimeSync/Echo cadence
stays deferred per TS-58).
- SequenceMath: wrap-safe IsNewer/Max (TimeStampUtils::lhs_newer
@ 0x00543890, reduced to the signed-difference form).
- SentPacketStore: FIFO of ArrayPool-rented wire buffers; Add asserts
optionalLength == 0 (NetPacket::RemoveDisposableOptionalHeaders
@ 0x00549510 pinned as a no-op under standalone-control); FlushOlderThan
pops strictly-older wrap-safe (SentPacketStore::AddSentPacket
@ 0x0054AB00, Flush @ 0x0054ACD0).
- OutboundFlowQueue: owns the outbound ISAAC, highestIDSent (starts 1,
pre-increment, wrap 0xFFFFFFFF->1, never 0), the fragment sequence, the
store, the wrap-safe sorted dedup pending-resend list
(FlowQueue::EnqueueAcks @ 0x005488E0), and the flushNum_ ack watermark.
Cache commit happens AFTER a successful send
(FlowQueue::TransmitNewPackets @ 0x00547A60, commit site 0x00547C85).
NAK ids[0] folds into the watermark as retail's implicit cumulative ack
(RecipientData::ProcessNaks @ 0x00547010). A resend rebuilds ONLY the
20-byte header: flags Retransmission|EncryptedChecksum (|BlobFragments
with fragments), Time = current interval id, Sequence/Id/Iteration/
DataSize verbatim, checksum = fresh header hash + stored sealed checksum
(FlowQueue::TransmitAcks @ 0x005485B0, DequeueAck @ 0x005472F0). The
original ISAAC key rides inside the sealed value - no new keystream word
is ever drawn (CryptoSystem::EncryptData @ 0x0065FF40 non-null-key
path; landmines #1/#2). Resend only on explicit NAK (landmine #3).
- ReliableTransport: composition + Sweep() (interval clock, resends,
prune). The AckNakScheduler joins in N3/N4; ack behavior is untouched
this slice.
- TransportStats: unconditional counters (ResendsSent,
NakRequestsReceived, UncachedNakIds, AcksConsumed) + CacheDepth.
PacketCodec.FinalizeInPlace gains an overload returning (isaacKeyUsed,
sealedChecksum) where sealedChecksum is the pre-header-hash value -
payloadHash cleartext, isaacKey ^ payloadHash encrypted (retail
NetPacket::checksum_). The old signature forwards; encode bytes are
unchanged. Decode is untouched.
WorldSession integration is minimal: the transport is constructed at
ISAAC-seeding time (first reliable packet keeps sequence 2 / fragment 1,
byte-identical to pre-N1); SendGameMessage delegates (probe fseq/pseq now
read the transport); SendAck's borrowed sequence reads HighestIdSent
(identical value, behavior EXACTLY as-is this slice); ProcessDatagram
consumes RequestRetransmit + AckSequence BEFORE the unchanged reflex ack;
the sweep runs at the end of Tick() after the budget break AND inside
both blocking handshake pump loops (Connect step 4, EnterWorld
ServerReady - landmine #8), gated on _transportNegotiated; Dispose
returns the rented cache buffers.
Bookkeeping: TS-57 filed in the divergence register (uncached NAK ids
dropped silently + counted instead of retail's RejectRetransmit - ACE
no-ops the reject and the standalone unsequenced form would trip ACE's
watermark hole); TS-27 narrowed to the inbound direction in the same
commit; the stale WorldSession class-doc gap list corrected.
N0 fold-ins from the re-review: AceSessionModel.ProcessFragment split
into ACE's two literal branches (existing-buffer checks Complete,
NetworkSession.cs:495-507; new-buffer constructs + adds + TryAdds WITHOUT
checking Complete, :509-518), and the zero-count-fragment test now pins
the parked dead buffer (PartialFragmentBufferCount 0 -> 1). e3958610
recorded in the campaign ledger's N0 row.
Tests: 15 new in Transport/OutboundReliableTransportTests.cs - store
FIFO/strict/wrap-safe flush with rent/return balance via a counting
pool, interval-clock start/advance/wrap, resend header shape (flags
exactly 3 or 7, Time = interval, verbatim fields, checksum identity,
bit-identical body), resend-consumes-no-ISAAC-word, uncached-NAK
counting, ids[0] watermark fold + strict prune, wrap-safe ack max,
conformance resend verifying under AceCryptoModel with the ORIGINAL
parked key (Headroom 256, zero orphans, ordering restored), an
end-to-end FakeAceTransport lossy run (10 game actions, C2S #5 dropped,
all 10 dispatched in order, exactly one resend, session alive), and
zero-alloc steady-state SendGameMessage.
Gates: dotnet build green; AcDream.Core.Net.Tests 702/702; full-solution
Release 9,723 passed / 5 skipped / 0 failed; connected world-lifecycle
gate vs local ACE RESULT=PASS (0 failures, both sessions exit 0; one
pre-existing expected world-edge landblock-miss warning).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Addresses the N0 review findings against commit 7e9134b4. Test-only: no
production code changes.
F1 (blocking) - model Session.CheckState (Session.cs:93-110). A three-value
AceSessionState (AuthLoginRequest -> AuthConnectResponse -> AuthConnected)
advances on SendConnectRequest (AuthenticationHandler.cs:127, :232) and on the
accepted ConnectResponse (NetworkManager.cs:77). CheckState runs as the first
statement of Receive after TryParse - ahead of the ConnectResponse route and
ahead of VerifyCRC - so a LoginRequest out of state, a replayed
ConnectResponse, or any of AckSequence|TimeSync|EchoRequest|Flow during
AuthLoginRequest is dropped at zero keystream cost (ACE's PacketHeader.HasFlag
is ANY-of, PacketHeader.cs:70). New StateDropCount counter.
F2 - implement SendBundle faithfully (NetworkSession.cs:808-919). One
NetworkBundle per GameMessageGroup (NetworkBundle.cs:6-63), swapped out and
sent in ascending group order; the InvalidQueue bundle carries the ack /
TimeSync / EchoResponse optional headers. As many same-bundle fragments as fit
the 464-byte body budget now travel in ONE packet - one sequence, one keystream
word - and a message whose remaining data fills a packet splits across packets
with Count>1 fragments (:846-854, :874-888) via a port of ACE's server-side
MessageFragment (MessageFragment.cs:10-103). The old "one packet per message"
shortcut and its incorrect rationale are gone.
F3 - model the two-phase termination. Terminate arms PendingTermination with
the 2 s window (Session.cs:281-298, SessionTerminationDetails.cs:12); inbound
and outbound keep running through it (Session.cs:124-133), then the pump
completes the session work and releases the network resources
(NetworkManager.cs:366-369 -> Session.cs:300-334 -> NetworkSession.cs:958-974).
IsTerminated now means "termination armed"; IsReleased is the point of no
return.
F4 - port ACE's MessageBuffer exactly (MessageBuffer.cs:7-54): a List, not an
index-addressed array. An assembled stream under 4 bytes returns null and is
dropped WITHOUT advancing the fragment gate (:49-50 + NetworkSession.cs:504-506
removing the buffer either way), and a later fragment claiming a larger
Count/Index for the same sequence completes the message instead of throwing.
F5 - the C2S parse path now characterizes ACE: fragment parsing uses ACE's
complete validation (16 <= Size <= 464, ClientPacketFragment.cs:12-24) with no
Count==0 / Index>=Count rejection and with ReadBytes' short-read tolerance,
instead of inheriting acdream's stricter production layout check. The one
remaining strictness we inherit - the 1024-id cap on retransmit lists - is
documented as unreachable (ACE reads into a 1024-byte buffer, so a C2S datagram
can carry at most 250 ids).
F6 - class doc now states that C2S CRC verification reuses acdream's own
PacketHeaderOptional hashing, so the double is NOT an independent oracle on
optional-header wire layout, and names the two known asymmetries (ACE has no
inbound ConnectRequest parse; ACE hashes-but-does-not-advance on
LoginRequest / WorldLoginRequest / ConnectResponse).
F7 - hardened three weak tests: the NAK rate limit is probed at 0.9 s and at
exactly 1.0 s (both closed) before 1.1 s opens it; the session timeout is
probed at exactly 60 s after fixing the model's `>` to ACE's `>=`
(Session.cs:140); the cache prune pins that an entry exactly 120 s old survives
(:258 is strictly greater).
F9 - campaign doc section 9 ledger: N0 row marked complete.
Nine new tests; 687 Core.Net tests green in Release.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Campaign N slice N0 (docs/plans/2026-07-29-network-transport-campaign.md):
the referee that slices N1-N5 are graded against, test-project only, zero
production changes.
- VirtualClock: Stopwatch-shaped deterministic time source (fixed 100 ns
ticks) that N1 will inject behind the production TransportClock.
- AceCryptoModel: verbatim port of ACE CryptoSystem Search/ConsumeKey over
our IsaacRandom - 256-key window, parked-key set, Headroom/OrphanCount
diagnostics (CryptoSystem.cs:8-49 cited per method).
- AceSessionModel: transport-free ACE NetworkSession over raw datagrams,
every rule cited to NetworkSession.cs - CRC-before-everything silent
drop, cleartext-NAK early return (no timeout refresh, :283-308),
60 s timeout refresh (:329-331), exact-equality ack dedup exemption
(:342-347), desired+2 NAK trigger with 1 s limit (:351-363), >window
AbnormalSequenceReceived (:393-397), the :474-476 watermark hole,
ack-value cache prune (:663-673), fragment gate (:532-543), seq>=2
caching (:730), Retransmission-flag resends with the ORIGINAL IssacXor
(:675-686), RejectRetransmit, 2 s cleartext cumulative ack, 20 s
TimeSync, EchoResponse, 120 s cache prune (:251-262). ACE's raw
wrap-unsafe comparisons are modeled bug-for-bug, not fixed.
- LossyLink: deterministic drop/reorder/seeded-loss fault injector, pure
data structure.
- FakeAceTransport: IWorldSessionTransport binding a REAL WorldSession to
the model through the link, with the handshake scripted (ConnectRequest
reusing the negotiation fixture layout, CharacterList, ServerReady,
logoff confirmation) - genuine Connect/EnterWorld/Tick/Dispose with no
sockets.
- 19 new tests pin the double, including
CleartextNonAckAdvancesWatermark_TheAceHole (the self-induced wedge
behind scope rows TS-57/TS-58/AP-125), re-key = permanent orphan,
unrequested-resend window burn, the 115-id NAK cap boundary, and a
full no-socket session lifecycle with both ISAAC streams verified
aligned end-to-end.
Core.Net suite: 678 passed / 0 failed (659 existing + 19 new).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>