feat(net): N1 - outbound sent-packet cache + resend on NAK
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>
This commit is contained in:
parent
e395861053
commit
43e60a6971
13 changed files with 1491 additions and 65 deletions
71
src/AcDream.Core.Net/Transport/ReliableTransport.cs
Normal file
71
src/AcDream.Core.Net/Transport/ReliableTransport.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
using System.Buffers;
|
||||
using AcDream.Core.Net.Cryptography;
|
||||
|
||||
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), and the
|
||||
/// unconditional counters. The inbound sequence tracker joins in N2 and the
|
||||
/// <c>AckNakScheduler</c> in N3/N4 — N1 deliberately leaves ack behavior in
|
||||
/// <c>WorldSession</c> untouched.
|
||||
///
|
||||
/// <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).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal sealed class ReliableTransport : IDisposable
|
||||
{
|
||||
public TransportClock Clock { get; }
|
||||
|
||||
public OutboundFlowQueue Outbound { get; }
|
||||
|
||||
public TransportStats Stats { get; }
|
||||
|
||||
public ReliableTransport(
|
||||
IsaacRandom outboundIsaac,
|
||||
ushort sessionClientId,
|
||||
DatagramSendDelegate send,
|
||||
TransportClock? clock = null,
|
||||
ArrayPool<byte>? pool = null)
|
||||
{
|
||||
Clock = clock ?? new TransportClock();
|
||||
Stats = new TransportStats();
|
||||
Outbound = new OutboundFlowQueue(
|
||||
outboundIsaac,
|
||||
sessionClientId,
|
||||
Clock,
|
||||
Stats,
|
||||
send,
|
||||
pool);
|
||||
Stats.CacheDepthSource = () => Outbound.CacheDepth;
|
||||
}
|
||||
|
||||
/// <summary>Last reliable sequence on the wire — the value unsequenced
|
||||
/// control packets (the reflex 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).
|
||||
/// </summary>
|
||||
public void Sweep()
|
||||
{
|
||||
Clock.Update();
|
||||
Outbound.TransmitPendingResends();
|
||||
}
|
||||
|
||||
/// <summary>Returns every rented cache buffer to the pool.</summary>
|
||||
public void Dispose() => Outbound.Dispose();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue