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
|
|
@ -6,19 +6,20 @@ namespace AcDream.Core.Net.Transport;
|
|||
/// <summary>
|
||||
/// Composition root for the session's reliable transport (campaign doc §4):
|
||||
/// one <see cref="TransportClock"/>, the outbound flow queue (N1), the
|
||||
/// inbound sequence tracker (N2), and the unconditional counters. The
|
||||
/// <c>AckNakScheduler</c> joins in N3/N4 — until then ack behavior stays in
|
||||
/// <c>WorldSession</c> untouched.
|
||||
/// inbound sequence tracker (N2), the ack/NAK scheduler (N3), and the
|
||||
/// unconditional counters.
|
||||
///
|
||||
/// <para>
|
||||
/// <see cref="Sweep"/> is the once-per-frame pump slice retail runs from
|
||||
/// <c>Client::UseTime @ 0x00411C40</c> →
|
||||
/// <c>PacketController::UseTime @ 0x005410D0</c>: advance the interval
|
||||
/// clock, serve pending retransmits, prune the acked cache. The session
|
||||
/// calls it at the end of <c>Tick()</c> AND inside the blocking handshake
|
||||
/// pump loops (landmine #8 — the EnterWorld flood precedes the first Tick),
|
||||
/// gated on transport negotiation (ACE's <c>Session.CheckState</c> discards
|
||||
/// early control traffic).
|
||||
/// clock, arbitrate NAK-xor-ack, serve pending retransmits, prune the acked
|
||||
/// cache. The session calls it at the end of <c>Tick()</c> AND inside the
|
||||
/// blocking handshake pump loops (landmine #8 — the EnterWorld flood
|
||||
/// precedes the first Tick; ACE needs acks during the character-list /
|
||||
/// enter-world floods, and the scheduler's ~2 s cadence there matches ACE's
|
||||
/// own), gated on transport negotiation (ACE's <c>Session.CheckState</c>
|
||||
/// discards early control traffic).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal sealed class ReliableTransport : IDisposable
|
||||
|
|
@ -32,6 +33,11 @@ internal sealed class ReliableTransport : IDisposable
|
|||
/// queue at ISAAC-seeding time so both keystreams share one owner.</summary>
|
||||
public InboundSequenceTracker Inbound { get; }
|
||||
|
||||
/// <summary>N3: retail's NAK-xor-ack sweep arbitration on the one
|
||||
/// shared timestamp (<c>ClientNet::ProcessConnection @ 0x00545450</c>);
|
||||
/// owns the 2.0 s cumulative <c>AckSequence</c>.</summary>
|
||||
public AckNakScheduler Scheduler { get; }
|
||||
|
||||
public TransportStats Stats { get; }
|
||||
|
||||
public ReliableTransport(
|
||||
|
|
@ -52,24 +58,34 @@ internal sealed class ReliableTransport : IDisposable
|
|||
send,
|
||||
pool);
|
||||
Inbound = new InboundSequenceTracker(inboundIsaac, Stats);
|
||||
Scheduler = new AckNakScheduler(
|
||||
Clock,
|
||||
Inbound,
|
||||
Outbound,
|
||||
sessionClientId,
|
||||
Stats,
|
||||
send);
|
||||
Stats.CacheDepthSource = () => Outbound.CacheDepth;
|
||||
}
|
||||
|
||||
/// <summary>Last reliable sequence on the wire — the value unsequenced
|
||||
/// control packets (the reflex ack) borrow without incrementing.</summary>
|
||||
/// control packets (the cumulative ack) borrow without incrementing.</summary>
|
||||
public uint HighestIdSent => Outbound.HighestIdSent;
|
||||
|
||||
/// <summary>
|
||||
/// One transport pump: interval clock forward, pending NAKed resends
|
||||
/// out, acked cache entries pruned. Pump order per retail
|
||||
/// <c>FlowQueue::Empty @ 0x00548A20</c> (NAK consumption already
|
||||
/// happened at receive time; retransmits precede new packets — new
|
||||
/// packets are sent synchronously by the session, so the sweep runs
|
||||
/// before the frame's sends the same way retail's per-frame pump does).
|
||||
/// One transport pump: interval clock forward, NAK-xor-ack arbitration,
|
||||
/// pending NAKed resends out, acked cache entries pruned. Pump order per
|
||||
/// retail <c>FlowQueue::Empty @ 0x00548A20</c>: the interval clock, then
|
||||
/// the control-packet arbitration (<c>ClientNet::ProcessConnection
|
||||
/// @ 0x00545450</c> enqueues NAKs-or-ack before the flow queue drains),
|
||||
/// then retransmits, then new packets — new packets are sent
|
||||
/// synchronously by the session, so the sweep runs before the frame's
|
||||
/// sends the same way retail's per-frame pump does.
|
||||
/// </summary>
|
||||
public void Sweep()
|
||||
{
|
||||
Clock.Update();
|
||||
Scheduler.Sweep(Clock.GetTimestamp());
|
||||
Outbound.TransmitPendingResends();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue