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();
}