fix: complete retail parity stability pass
This commit is contained in:
parent
d3df4cb20a
commit
f7aa8e0eb7
131 changed files with 7765 additions and 1190 deletions
|
|
@ -275,5 +275,6 @@ internal sealed class AckNakScheduler
|
|||
outboundIsaac: null);
|
||||
_send(datagram.Slice(0, datagramLength));
|
||||
_stats.NaksSent++;
|
||||
_stats.NakIdsSent += count;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -263,6 +263,23 @@ internal sealed class OutboundFlowQueue : IDisposable
|
|||
{
|
||||
for (int i = 0; i < _pendingResends.Count; i++)
|
||||
{
|
||||
// A later cumulative ACK can overtake an earlier NAK in the
|
||||
// receive-owner queue before this frame reaches its sweep.
|
||||
// Such a request is stale: the server has already advanced
|
||||
// its ordered watermark past the requested packet. Replaying
|
||||
// the old encrypted key is actively harmful against ACE,
|
||||
// whose CRC/key search runs before duplicate-sequence
|
||||
// rejection and can burn the remaining 256-word window.
|
||||
// The equal case is NOT stale: ids[0] of an ordinary NAK is
|
||||
// also its implicit ACK and still needs retransmission.
|
||||
if (AckWatermark != 0
|
||||
&& SequenceMath.IsNewer(
|
||||
AckWatermark,
|
||||
_pendingResends[i]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// A pending id can leave the cache between NAK arrival and
|
||||
// this sweep only if a newer ack already covered it — the
|
||||
// server has it; serving nothing is correct.
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ internal sealed class ReliableTransport : IDisposable
|
|||
|
||||
public TransportStats Stats { get; }
|
||||
|
||||
/// <summary>Retail's 40-heartbeat packet-loss percentage.</summary>
|
||||
public double PacketLossPercentage => _packetLoss.Percentage;
|
||||
|
||||
/// <summary>
|
||||
/// N6: retail's ephemeral-info flush cadence
|
||||
/// (<c>Indicator::FlushTimedOutEphInfo @ 0x0054A3D0</c>, the x87 compare
|
||||
|
|
@ -51,6 +54,7 @@ internal sealed class ReliableTransport : IDisposable
|
|||
public const double AssemblerSweepSeconds = 5.0;
|
||||
|
||||
private readonly FragmentAssembler? _assembler;
|
||||
private readonly RetailPacketLossAverager _packetLoss;
|
||||
private readonly long _assemblerSweepTicks;
|
||||
private long _assemblerSweepTimestamp;
|
||||
|
||||
|
|
@ -71,13 +75,19 @@ internal sealed class ReliableTransport : IDisposable
|
|||
_assemblerSweepTicks =
|
||||
(long)Math.Round(AssemblerSweepSeconds * Clock.Frequency);
|
||||
_assemblerSweepTimestamp = Clock.GetTimestamp();
|
||||
void CountedSend(ReadOnlySpan<byte> datagram)
|
||||
{
|
||||
send(datagram);
|
||||
Stats.PacketsSent++;
|
||||
}
|
||||
|
||||
Outbound = new OutboundFlowQueue(
|
||||
outboundIsaac,
|
||||
sessionClientId,
|
||||
sessionIteration,
|
||||
Clock,
|
||||
Stats,
|
||||
send,
|
||||
CountedSend,
|
||||
pool);
|
||||
Inbound = new InboundSequenceTracker(inboundIsaac, Stats);
|
||||
Scheduler = new AckNakScheduler(
|
||||
|
|
@ -87,8 +97,9 @@ internal sealed class ReliableTransport : IDisposable
|
|||
sessionClientId,
|
||||
sessionIteration,
|
||||
Stats,
|
||||
send);
|
||||
CountedSend);
|
||||
Stats.CacheDepthSource = () => Outbound.CacheDepth;
|
||||
_packetLoss = new RetailPacketLossAverager(Clock, Stats);
|
||||
}
|
||||
|
||||
/// <summary>Last reliable sequence on the wire — the value unsequenced
|
||||
|
|
@ -112,6 +123,12 @@ internal sealed class ReliableTransport : IDisposable
|
|||
{
|
||||
Clock.Update();
|
||||
long now = Clock.GetTimestamp();
|
||||
// Retail snapshots the preceding two-second interval inside
|
||||
// ClientNet::ProcessConnection before this heartbeat emits its
|
||||
// ACK/NAK control traffic. Keep boundary control packets in the new
|
||||
// sample rather than attributing them to the interval that just
|
||||
// ended.
|
||||
_packetLoss.Sweep(now, Stats);
|
||||
Scheduler.Sweep(now);
|
||||
Outbound.TransmitPendingResends();
|
||||
|
||||
|
|
|
|||
124
src/AcDream.Core.Net/Transport/RetailPacketLossAverager.cs
Normal file
124
src/AcDream.Core.Net/Transport/RetailPacketLossAverager.cs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
namespace AcDream.Core.Net.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// Retail's packet-loss telemetry window from
|
||||
/// <c>CLinkStatusAverages::GetAveragePacketLoss @ 0x00546610</c> and
|
||||
/// <c>CLinkStatusAverages::AddSnapshot @ 0x00546650</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <c>ClientNet::ProcessConnection @ 0x00545450</c> snapshots the four
|
||||
/// packet counters on its 2.0-second heartbeat. Each counter is a
|
||||
/// <c>CAverager<unsigned short,40></c>. The loss function divides the
|
||||
/// windowed NAK + retransmit totals by received + sent totals and reports a
|
||||
/// percentage. A zero denominator reports zero.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The input values here are cumulative acdream counters; each sample stores
|
||||
/// their delta since the preceding heartbeat. The unchecked ushort conversion
|
||||
/// preserves retail's snapshot-field width.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class RetailPacketLossAverager
|
||||
{
|
||||
public const int WindowSize = 40;
|
||||
public const double SnapshotSeconds = 2.0;
|
||||
|
||||
private readonly Sample[] _samples = new Sample[WindowSize];
|
||||
private readonly long _snapshotTicks;
|
||||
private long _snapshotTimestamp;
|
||||
private long _lastPacketsSent;
|
||||
private long _lastRetransmitsSent;
|
||||
private long _lastPacketsReceived;
|
||||
private long _lastNakIdsSent;
|
||||
private long _sentTotal;
|
||||
private long _retransmitTotal;
|
||||
private long _receivedTotal;
|
||||
private long _nakTotal;
|
||||
private int _next;
|
||||
private int _count;
|
||||
|
||||
public RetailPacketLossAverager(TransportClock clock, TransportStats stats)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(clock);
|
||||
ArgumentNullException.ThrowIfNull(stats);
|
||||
|
||||
_snapshotTicks = (long)Math.Round(SnapshotSeconds * clock.Frequency);
|
||||
_snapshotTimestamp = clock.GetTimestamp();
|
||||
CaptureBaselines(stats);
|
||||
}
|
||||
|
||||
public double Percentage
|
||||
{
|
||||
get
|
||||
{
|
||||
long denominator = _receivedTotal + _sentTotal;
|
||||
return denominator <= 0
|
||||
? 0d
|
||||
: 100d * (_nakTotal + _retransmitTotal) / denominator;
|
||||
}
|
||||
}
|
||||
|
||||
public void Sweep(long now, TransportStats stats)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stats);
|
||||
if (now - _snapshotTimestamp < _snapshotTicks)
|
||||
return;
|
||||
|
||||
// ProcessConnection records one snapshot when the heartbeat branch
|
||||
// runs; it does not synthesize empty samples for skipped frames.
|
||||
_snapshotTimestamp = now;
|
||||
|
||||
var sample = new Sample(
|
||||
DeltaAsRetailUShort(stats.PacketsSent, ref _lastPacketsSent),
|
||||
DeltaAsRetailUShort(stats.ResendsSent, ref _lastRetransmitsSent),
|
||||
DeltaAsRetailUShort(stats.PacketsReceived, ref _lastPacketsReceived),
|
||||
DeltaAsRetailUShort(stats.NakIdsSent, ref _lastNakIdsSent));
|
||||
|
||||
if (_count == WindowSize)
|
||||
Remove(_samples[_next]);
|
||||
else
|
||||
_count++;
|
||||
|
||||
_samples[_next] = sample;
|
||||
_next = (_next + 1) % WindowSize;
|
||||
Add(sample);
|
||||
}
|
||||
|
||||
private void CaptureBaselines(TransportStats stats)
|
||||
{
|
||||
_lastPacketsSent = stats.PacketsSent;
|
||||
_lastRetransmitsSent = stats.ResendsSent;
|
||||
_lastPacketsReceived = stats.PacketsReceived;
|
||||
_lastNakIdsSent = stats.NakIdsSent;
|
||||
}
|
||||
|
||||
private static ushort DeltaAsRetailUShort(long current, ref long previous)
|
||||
{
|
||||
long delta = current - previous;
|
||||
previous = current;
|
||||
return unchecked((ushort)Math.Max(0L, delta));
|
||||
}
|
||||
|
||||
private void Add(Sample sample)
|
||||
{
|
||||
_sentTotal += sample.Sent;
|
||||
_retransmitTotal += sample.Retransmitted;
|
||||
_receivedTotal += sample.Received;
|
||||
_nakTotal += sample.Naked;
|
||||
}
|
||||
|
||||
private void Remove(Sample sample)
|
||||
{
|
||||
_sentTotal -= sample.Sent;
|
||||
_retransmitTotal -= sample.Retransmitted;
|
||||
_receivedTotal -= sample.Received;
|
||||
_nakTotal -= sample.Naked;
|
||||
}
|
||||
|
||||
private readonly record struct Sample(
|
||||
ushort Sent,
|
||||
ushort Retransmitted,
|
||||
ushort Received,
|
||||
ushort Naked);
|
||||
}
|
||||
|
|
@ -9,6 +9,15 @@ namespace AcDream.Core.Net.Transport;
|
|||
/// </summary>
|
||||
internal sealed class TransportStats
|
||||
{
|
||||
/// <summary>Checksum-valid post-negotiation datagrams received. Used by
|
||||
/// retail's 2-second/40-sample link-status loss window.</summary>
|
||||
public long PacketsReceived;
|
||||
|
||||
/// <summary>Datagrams successfully emitted after transport negotiation.
|
||||
/// Includes reliable traffic, retransmits, cumulative ACKs, and NAKs,
|
||||
/// matching retail's packet-level denominator.</summary>
|
||||
public long PacketsSent;
|
||||
|
||||
/// <summary>Datagrams re-emitted in response to a server NAK.</summary>
|
||||
public long ResendsSent;
|
||||
|
||||
|
|
@ -57,6 +66,11 @@ internal sealed class TransportStats
|
|||
/// <c>ReceiverData::GetNaks @ 0x005490C0</c>, ≤114 ids each).</summary>
|
||||
public long NaksSent;
|
||||
|
||||
/// <summary>Total missing sequence ids carried by emitted NAKs. Retail's
|
||||
/// <c>CLinkStatusSnapshot::nPktsNAKed</c> counts missing packets, not the
|
||||
/// number of control datagrams used to request them.</summary>
|
||||
public long NakIdsSent;
|
||||
|
||||
/// <summary>N4: mis-parked keystream words reclaimed from validated
|
||||
/// cleartext <c>RejectRetransmit</c> sequences (the AD-51 ACE
|
||||
/// adaptation; always zero against a retail server).</summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue