using System.Buffers; using AcDream.Core.Net.Cryptography; using AcDream.Core.Net.Packets; namespace AcDream.Core.Net.Transport; /// /// Composition root for the session's reliable transport (campaign doc §4): /// one , the outbound flow queue (N1), the /// inbound sequence tracker (N2), the ack/NAK scheduler (N3), and the /// unconditional counters. /// /// /// is the once-per-frame pump slice retail runs from /// Client::UseTime @ 0x00411C40 → /// PacketController::UseTime @ 0x005410D0: advance the interval /// clock, arbitrate NAK-xor-ack, serve pending retransmits, prune the acked /// cache. The session calls it at the end of Tick() 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 Session.CheckState /// discards early control traffic). /// /// internal sealed class ReliableTransport : IDisposable { public TransportClock Clock { get; } public OutboundFlowQueue Outbound { get; } /// N2: the inbound sequence tracker — inbound ISAAC, /// highestIDReceived_, and the NAK set. Born beside the outbound /// queue at ISAAC-seeding time so both keystreams share one owner. public InboundSequenceTracker Inbound { get; } /// N3: retail's NAK-xor-ack sweep arbitration on the one /// shared timestamp (ClientNet::ProcessConnection @ 0x00545450); /// owns the 2.0 s cumulative AckSequence. public AckNakScheduler Scheduler { get; } public TransportStats Stats { get; } /// /// N6: retail's ephemeral-info flush cadence /// (Indicator::FlushTimedOutEphInfo @ 0x0054A3D0, the x87 compare /// against 5.0 at 0x0054A3DC) — how often the sweep asks the fragment /// assembler to evict aged partials. The per-entry TTL itself lives in /// (AD-52). /// public const double AssemblerSweepSeconds = 5.0; private readonly FragmentAssembler? _assembler; private readonly long _assemblerSweepTicks; private long _assemblerSweepTimestamp; public ReliableTransport( IsaacRandom outboundIsaac, IsaacRandom inboundIsaac, ushort sessionClientId, ushort sessionIteration, DatagramSendDelegate send, TransportClock? clock = null, ArrayPool? pool = null, FragmentAssembler? assembler = null) { Clock = clock ?? new TransportClock(); Stats = new TransportStats(); _assembler = assembler; // Same defensive rounding as the scheduler gates (N4 review F1). _assemblerSweepTicks = (long)Math.Round(AssemblerSweepSeconds * Clock.Frequency); _assemblerSweepTimestamp = Clock.GetTimestamp(); Outbound = new OutboundFlowQueue( outboundIsaac, sessionClientId, sessionIteration, Clock, Stats, send, pool); Inbound = new InboundSequenceTracker(inboundIsaac, Stats); Scheduler = new AckNakScheduler( Clock, Inbound, Outbound, sessionClientId, sessionIteration, Stats, send); Stats.CacheDepthSource = () => Outbound.CacheDepth; } /// Last reliable sequence on the wire — the value unsequenced /// control packets (the cumulative ack) borrow without incrementing. public uint HighestIdSent => Outbound.HighestIdSent; /// /// One transport pump: interval clock forward, NAK-xor-ack arbitration, /// pending NAKed resends out, acked cache entries pruned. Retail's /// FlowQueue::Empty @ 0x00548A20 drains /// TransmitNaks → TransmitAcks → TransmitNewPackets and advances /// the interval clock LAST (the 0.5 s walk + /// IncrementLocalInterval at 0x00548A9D); our Sweep advances the /// clock FIRST. The divergence is cosmetic against ACE — it only shifts /// which interval id lands in Header.Time at an interval /// boundary, and ACE never reads that field inbound (campaign §3). 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. /// public void Sweep() { Clock.Update(); long now = Clock.GetTimestamp(); Scheduler.Sweep(now); Outbound.TransmitPendingResends(); // N6: age out abandoned fragment partials on retail's 5 s flush // cadence (Indicator::FlushTimedOutEphInfo @ 0x0054A3D0 — re-stamp // the flush clock, then walk the table dropping timed-out entries). if (_assembler is not null && now - _assemblerSweepTimestamp >= _assemblerSweepTicks) { _assemblerSweepTimestamp = now; _assembler.SweepExpired(); } } /// Returns every rented cache buffer to the pool. public void Dispose() => Outbound.Dispose(); }