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>
This commit is contained in:
parent
19bfb8477d
commit
0265cc4236
9 changed files with 797 additions and 127 deletions
|
|
@ -180,7 +180,9 @@ public sealed class OutboundReliableTransportTests
|
|||
Assert.Equal(
|
||||
PacketHeaderFlags.BlobFragments | PacketHeaderFlags.EncryptedChecksum,
|
||||
originalHeader.Flags);
|
||||
Assert.Equal((ushort)0, originalHeader.Time);
|
||||
// N3 fold-in: fresh sends stamp the current interval id (the clock
|
||||
// starts at 1) — FlowQueue::TransmitNewPackets @ 0x00547A60.
|
||||
Assert.Equal((ushort)1, originalHeader.Time);
|
||||
|
||||
// 1.2 s later (interval id 1 → 3) the server NAKs sequence 2.
|
||||
virtualClock.Advance(TimeSpan.FromSeconds(1.2));
|
||||
|
|
@ -323,6 +325,39 @@ public sealed class OutboundReliableTransportTests
|
|||
Assert.True(stats.AcksConsumed >= 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// N3 fold-in of the N1 review advisory: retail stamps
|
||||
/// <c>CurLocalInterval_.intervalID_</c> into <c>Header.Time</c> on every
|
||||
/// FRESH packet (<c>FlowQueue::TransmitNewPackets @ 0x00547A60</c>, the
|
||||
/// header build at 0x00547A84), and a resend re-stamps the CURRENT
|
||||
/// interval id (possibly newer than the fresh-send stamp). ACE never
|
||||
/// reads inbound <c>Header.Time</c>, so this is wire-cosmetic against
|
||||
/// ACE — but it is retail's behavior.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FreshSend_StampsCurrentIntervalId_ResendRestampsNewer()
|
||||
{
|
||||
(OutboundFlowQueue queue, VirtualClock virtualClock,
|
||||
TransportClock clock, _, List<byte[]> sent) = CreateQueue();
|
||||
|
||||
// K interval ticks before the send: 2.5 s = 5 intervals, id 1 → 6.
|
||||
virtualClock.Advance(TimeSpan.FromSeconds(2.5));
|
||||
clock.Update();
|
||||
Assert.Equal((ushort)6, clock.IntervalId);
|
||||
queue.SendGameMessage(MakeMessage(0xA1), GameMessageGroup.UIQueue);
|
||||
Assert.Equal((ushort)6, PacketHeader.Unpack(Assert.Single(sent)).Time);
|
||||
|
||||
// The interval advances again; the resend carries the CURRENT id,
|
||||
// newer than the fresh-send stamp.
|
||||
virtualClock.Advance(TimeSpan.FromSeconds(1.0));
|
||||
clock.Update();
|
||||
Assert.Equal((ushort)8, clock.IntervalId);
|
||||
Nak(queue, 2u);
|
||||
sent.Clear();
|
||||
queue.TransmitPendingResends();
|
||||
Assert.Equal((ushort)8, PacketHeader.Unpack(Assert.Single(sent)).Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnAckSequence_IsWrapSafeMax_AndNeverRegresses()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue