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>
Campaign N Slice N5 (docs/plans/2026-07-29-network-transport-campaign.md
section 8 rung 3): the permanent removal of the loopback blindness that let
#260 ship. Local ACE never drops a datagram, so every historical connected
gate was structurally incapable of exercising the N1-N4 recovery machinery;
from this slice on, tools/run-connected-loss-gate.ps1 runs the standard
lifecycle route through deterministic seeded loss and passes only on proven
non-zero recovery.
Observability:
- [net-tick] gains resend/s nak-out/s nak-in/s rej-in/s dup-drop/s parked/s
reclaim/s cache= nakset= - TransportStats window deltas mirroring the
acks/s cumulative-delta pattern, plus the two instantaneous depths (the
unbounded-like-retail sent-packet cache watchdog and the inbound NAK set).
TransportStats gains RejectsReceived (inbound RejectRetransmit packets).
Counters increment unconditionally; every string is behind
NetDiagnostics.ProbeNet (Code Structure Rule 5).
- WorldSession.Dispose emits one cumulative [net-final] totals line so the
loss gate asserts exact counters instead of reconstructing them from
rounded per-second rates.
- LinkStatusSnapshot.PacketLossPercentage is deliberately NOT wired: filed
#261 - retail's CLinkStatusAverages formula
(LinkStatusHolder::GetPacketLossPercentage @ 0x00411370) must be located
first; inventing a ratio is forbidden.
N4-review F3 fold-in:
- Fresh reliable sends stamp Header.Iteration = the session iteration
through the same shared retail header build already cited for Time (N3)
and the N4 control packets: FlowQueue::TransmitNewPackets @ 0x00547A60,
the stack build at 0x00547A84/0x00547AA8. The control-header rule now
holds across all three send shapes (fresh reliable, ack, NAK). ACE reads
neither Time nor Iteration inbound (campaign section 3) - wire-safe, and
resends keep the stamp verbatim per the N1 rebuild rule.
Loss injection (Transport/LossyTransportDecorator):
- IWorldSessionTransport wrapper with deterministic seeded per-direction
loss. Config via NetDiagnostics typed env properties read once:
ACDREAM_NET_DROP_PCT (0 = off = default), ACDREAM_NET_DROP_SEED (default
1), ACDREAM_NET_DROP_DIR (out|in|both, default both).
- Arming gate: NOTHING drops in either direction until the decorator has
FORWARDED the first ENCRYPTED outbound datagram - parse-free check on
length > 20 with EncryptedChecksum set in the LE flags word at bytes
4..8. The cleartext handshake always survives and the arming datagram is
never a casualty; handshake-loss testing belongs to N6's ConnectResponse
0.333 s retransmit.
- Structurally absent at 0%: WrapIfConfigured returns the raw transport -
WorldSession's default factory is the only production seam and a normal
run never constructs the decorator.
Root-cause fix the gate immediately exposed:
- The logoff-confirmation wait in Dispose processed inbound datagrams but
never pumped the transport, so a lost S2C logoff confirmation was
gap-detected but its healing NAK never went out. Retail's pump
(Client::UseTime @ 0x00411C40 -> PacketController::UseTime @ 0x005410D0)
runs until LogOffServer; the wait now sweeps per processed datagram,
making the logoff wait the third covered blocking pump (after Tick and
the handshake loops). A lost C2S logoff REQUEST remains unrecoverable by
ACE design (arrival-driven NAK; a quiet client is never NAKed - campaign
section 3 row 1), recorded in the gate header.
Gates:
- tools/run-connected-loss-gate.ps1 (-DropPct 2 -Seed 1): PASS vs local
ACE - the first automated observation of packet loss in project history.
Decorator ledger: dropped out=3 in=10 of forwarded out=183 in=496.
[net-final] resends=2 nak-in=2 nak-out=6 rej-in=0 acks-out=114
acks-in=119 dup-drop=0 sanity-drop=0 cksum-fail=0 parked=9 reclaimed=0
uncached-nak=0 cache=1 nakset=0. Every injected loss healed: both
ACE-driven C2S resend recovery (nak-in=2 -> resends=2) and client-driven
S2C NAK recovery (parked=9 -> nak-out=6) fired on a real connected
route, all six checkpoints validated, graceful logout confirmed, ACE
recorded the transport Disconnect.
- tools/run-connected-world-lifecycle-gate.ps1 (decorator absent): PASS -
zero behavior change on the no-loss baseline; the gate now defensively
clears the drop env vars.
- Core.Net Release: 747/747 (737 + 10 N5: decorator determinism/direction/
arming/structural-absence/env parsing, the 5% seeded WorldSession lossy
lifecycle with zero message loss both ways + ACE Headroom 256, the
[net-tick] field pins, the Iteration stamps).
- Full solution Release: 9,763 passed / 5 skipped / 0 failed.
Test-fixture note: FakeAceTransport gains AutoAdvanceOnBlockingReceive so
virtual time can move during the blocking Connect()/EnterWorld() pumps -
with the clock frozen there, a dropped handshake-window datagram could
never be NAK-healed (a fixture artifact, not a transport property).
Campaign section 9 ledger row added (SHA recorded at N6 kickoff).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>