Commit graph

2 commits

Author SHA1 Message Date
Erik
852a59e388 feat(net): N4 - client NAK emission + RejectRetransmit reclaim
Campaign N slice N4 completes the AckNakScheduler NAK branch and closes
the ACE cleartext-reject keystream hazard - the slice that makes S2C
loss actually RECOVER.

NAK emission (SharedNet::EnqueueNaks @ 0x00543BD0):
- One cleartext exact-flags RequestRetransmit per sweep behind the
  STRICT 0.6 s gate on the ONE shared timestamp (the x87 0x41-mask test
  at 0x00543C03 proceeds only on strictly-greater; the ack's gate stays
  >=). Never an ack in a NAK sweep; a NAK delays the next ack by 2.0 s
  and vice versa (landmine #7).
- Body = u32 count + ids ascending, capped at 114 (ReceiverData::GetNaks
  @ 0x005490C0, cap 0x72; the m_cbData = 4*count+4 store at 0x00543C3E);
  header Sequence borrowed from highestIDSent_ without incrementing;
  cleartext or ACE ignores it (landmine #6, NetworkSession.cs:283-284) -
  and a NAK never refreshes ACE's 60 s timeout.
- Control-header rule decided once for BOTH ack and NAK: Time = the
  interval id, Iteration = the session iteration, matching retail's
  shared header build (FlowQueue::TransmitNewPackets @ 0x00547A60, the
  stack build at 0x00547A84). ACE reads neither field inbound.
- Gate ticks now round instead of truncate: 0.6 has no exact double
  form, and truncation opened the strict gate exactly AT the boundary.

RejectRetransmit reclaim (divergence register AD-51, ACE adaptation):
- ACE's RejectRetransmit consumes a FRESH sequence, cleartext, with NO
  keystream word, and is cached (ACE NetworkSession.cs:299-304,
  :722-725, :743-748) - the one place ACE breaks retail's gap-walk
  invariant that every missing id was word-bearing (retail cleartext
  always borrows live sequences). Unhandled, the gap walk parks a word
  for the reject's id and the inbound stream runs permanently one word
  ahead - the N2 desync class reintroduced through the reject path.
- Fix: on a VALIDATED cleartext reject, InboundSequenceTracker removes
  the mis-park, shifts every later-drawn parked word down one position
  (per-word draw ordinals; ascending wrap-safe id <=> ascending draw
  order), and pools the excess word, consumed lowest-draw-order-first
  ahead of fresh ISAAC draws. Exact for any number of interleaved
  rejects in ANY arrival order - a plain reclaim FIFO is not: a reject
  arriving after a higher encrypted arrival crosses the parked chain,
  and two out-of-order rejects pool their excess words out of draw
  order (both orderings pinned by tests).
- Reject BODY ids keep N2's discard: word-bearing server-side,
  consumed-in-place. The pool is provably empty against retail servers.

N3 advisories folded (all five): honest transitional-state wording (the
empty N3 NAK branch could silently disconnect a loopback session at
ACE's 60 s timeout, witness [net-tick] acks/s=0), the
ReceiverData::SharedInit @ 0x00548EF0 (from Init @ 0x00548FA0)
citation, the FlowQueue::Empty pump-order wording (TransmitNaks ->
TransmitAcks -> TransmitNewPackets with the interval increment LAST @
0x00548A9D; our clock-first Sweep is cosmetic vs ACE), the
Time/Iteration rule above, and the stale WorldSession budget-break
comment rewritten to the sweep reality.

Tests: 737 Core.Net green (14 new in NakEmissionTests + updated N3
pins): strict-gate boundary, shared timestamp both directions,
NAK-xor-ack exclusivity, full wire-shape + 114-cap pins, model-served
retransmission round trip, five tracker reclaim proofs, the 130 s
virtual prune -> fresh-sequence reject system test (victim abandoned,
later traffic decodes, pool drains to zero), 10 s long-loss survival
(NAKs on the gate cadence, zero acks, heal inside the window), and the
capstone soak: 2% seeded bidirectional loss x 10,000 messages -> zero
message loss both ways, ACE crypto headroom 256 at convergence, every
ledger drained (cache at the single watermark entry - retail's Flush
prunes STRICTLY below the ack). Full solution Release: 9,758 passed /
5 skipped. Connected world-lifecycle gate PASS
(logs/connected-world-gate-20260729-150238); canonical nine-stop soak
PASS (logs/connected-r6-soak-20260729-150856).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 15:20:35 +02:00
Erik
0265cc4236 feat(net): N3 - AckNakScheduler, retail 2.0s cumulative ack replaces per-packet acks
Campaign N slice N3. Retail never acks per packet: SharedNet::EnqueuePak
@ 0x00543B10 is the binary's only AckSequence (0x4000) construction site,
gated at >= 2.0 s on ReceiverData::timeStamp_ (@ +0x10), armed at
connection birth by ReceiverData::Init @ 0x00548EF0, and arbitrated
NAK-xor-ack per sweep by ClientNet::ProcessConnection @ 0x00545450
(m_SeqIDsWeNAKed non-empty -> EnqueueNaks, else EnqueuePak;
SharedNet::EnqueueNaks @ 0x00543BD0 shares the SAME timestamp -
campaign landmine #7).

- New Transport/AckNakScheduler: owns the one shared timestamp; a
  non-empty NAK set suppresses the ack (N4 emits RequestRetransmit in
  that branch; in N3 it emits nothing - a documented transitional state,
  safe for exactly one slice on loopback), else ONE cleartext exact-flags
  AckSequence carrying the tracker's HighestIdReceived, header sequence
  borrowed from HighestIdSent without incrementing, 4-byte LE body.
  Flags are an EQUALITY, never an OR (landmine #5 - ACE's dedup
  exemption NetworkSession.cs:342-343 and watermark-skip :474-476 both
  require the exact value).
- ReliableTransport.Sweep pump order per FlowQueue::Empty @ 0x00548A20:
  interval clock, NAK/ack arbitration, pending resends, prune. The sweep
  already runs in Tick and both handshake pump loops (landmine #8), so
  cumulative acks flow during the character-list/enter-world floods at
  ACE's own ~2 s cadence.
- WorldSession: the Phase 4.9 per-packet reflex ack in ProcessDatagram
  and SendAck are DELETED; the [net-tick] acks/s probe now reads
  Stats.AcksSent; new internal TransportClockSource seam drives the
  2.0 s gate on virtual time in the conformance suite.
- N1 Fable-review advisory retired (Time-stamp fold-in): fresh reliable
  sends now stamp Header.Time = the current interval id, matching retail
  FlowQueue::TransmitNewPackets @ 0x00547A60 (header build at
  0x00547A84); resends already re-stamped. ACE never reads inbound
  Header.Time, so the wire stays compatible.

Tests: 723 Core.Net (7 new) - gate cadence + watermark-at-emission,
flags-equality pin + model acceptance at the reused sequence without a
watermark advance, NAK suppression and resume after the gap clears, a
50-packet CreateObject flood collapsing to ONE ack, the quiet-session
keepalive property across a 120 s virtual horizon (the reflex ack's
keepalive role, replaced and proven against ACE's 60 s TimeoutDeadline),
the Time fold-in, and a full FakeAceTransport lifecycle with zero
CRC/state/duplicate drops. Full solution Release: 9,744 passed /
5 skipped / 0 failed. Connected world-lifecycle gate PASS (capped +
uncapped-reconnect, graceful exits, 0 failures); canonical nine-stop
route PASS (0 failures).

Campaign section 9 N3 row updated (complete; SHA recorded at N4
kickoff).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 13:51:57 +02:00