fix: complete retail parity stability pass
All checks were successful
CI / linux-portable (push) Successful in 3m41s
CI / windows-gate (push) Successful in 6m49s
CI / release (push) Successful in 3m22s

This commit is contained in:
Erik 2026-08-28 20:01:39 +02:00
parent d3df4cb20a
commit f7aa8e0eb7
131 changed files with 7765 additions and 1190 deletions

View file

@ -273,6 +273,9 @@ internal sealed class AceSessionModel
public IReadOnlyCollection<uint> CachedPacketSequences => _cachedPackets.Keys;
/// <summary>Packets silently dropped by CRC/Search failure (NetworkSession.cs:277-280).</summary>
public int CrcDropCount { get; private set; }
public uint LastCrcDropSequence { get; private set; }
public PacketHeaderFlags LastCrcDropFlags { get; private set; }
public uint LastCrcDropWatermark { get; private set; }
/// <summary>Packets dropped by the duplicate-rejection rule (NetworkSession.cs:342-347).</summary>
public int DuplicateDropCount { get; private set; }
/// <summary>Packets dropped by the Session.CheckState gate (Session.cs:93-110) — before CRC.</summary>
@ -337,6 +340,9 @@ internal sealed class AceSessionModel
if (!VerifyCrc(packet))
{
CrcDropCount++;
LastCrcDropSequence = packet.Header.Sequence;
LastCrcDropFlags = packet.Header.Flags;
LastCrcDropWatermark = _lastReceivedPacketSequence;
return;
}

View file

@ -239,7 +239,12 @@ public sealed class LossyTransportDecoratorTests
// 256-key crypto window is intact at convergence.
// =====================================================================
// The real WorldSession receive owner is deliberately part of this
// end-to-end test. Under full-suite CPU contention its scheduling affects
// when virtual-time server traffic reaches the frame thread, so run it in
// the repository's serialized timing lane (#439).
[Fact]
[Trait("Lane", "Timing")]
public void LossySession_FivePercentSeeded_ZeroMessageLoss_Headroom256()
{
var fake = new FakeAceTransport
@ -338,7 +343,15 @@ public sealed class LossyTransportDecoratorTests
+ $"dropped-in={lossy.InboundDropped} "
+ $"resends={session.Transport!.Stats.ResendsSent} "
+ $"naks-sent={session.Transport.Stats.NaksSent} "
+ $"headroom={fake.Model.Crypto.Headroom}";
+ $"headroom={fake.Model.Crypto.Headroom} "
+ $"crc-drops={fake.Model.CrcDropCount} "
+ $"last-crc-seq={fake.Model.LastCrcDropSequence} "
+ $"last-crc-flags={fake.Model.LastCrcDropFlags} "
+ $"last-crc-watermark={fake.Model.LastCrcDropWatermark} "
+ $"dup-drops={fake.Model.DuplicateDropCount} "
+ $"ooo={fake.Model.OutOfOrderPacketCount} "
+ $"last-seq={fake.Model.LastReceivedPacketSequence} "
+ $"cache={session.Transport.Outbound.CacheDepth}";
// Zero message loss, both directions.
Assert.True(s2cReceived == MessagesEachWay, $"S2C loss: {ledger}");
@ -354,8 +367,8 @@ public sealed class LossyTransportDecoratorTests
// ACE's crypto search window never eroded (no re-key, no
// unrequested resend) — Headroom back to the full 256.
Assert.Equal(256, fake.Model.Crypto.Headroom);
Assert.Equal(0, fake.Model.Crypto.OrphanCount);
Assert.True(fake.Model.Crypto.Headroom == 256, ledger);
Assert.True(fake.Model.Crypto.OrphanCount == 0, ledger);
Assert.False(fake.Model.IsTerminated);
Assert.Equal(WorldSession.State.InWorld, session.CurrentState);
}

View file

@ -392,6 +392,29 @@ public sealed class OutboundReliableTransportTests
Assert.Equal(5u, wrapQueue.AckWatermark);
}
[Fact]
public void NewerAckOvertakingQueuedNak_SuppressesStaleEncryptedResend()
{
(OutboundFlowQueue queue, _, _, TransportStats stats,
List<byte[]> sent) = CreateQueue();
queue.SendGameMessage(MakeMessage(2), GameMessageGroup.UIQueue);
queue.SendGameMessage(MakeMessage(3), GameMessageGroup.UIQueue);
queue.SendGameMessage(MakeMessage(4), GameMessageGroup.UIQueue);
// The NAK for seq 2 is queued first. Before the frame sweep, a later
// cumulative ACK proves the server has already advanced through 3.
Nak(queue, 2u);
queue.OnAckSequence(3u);
sent.Clear();
queue.TransmitPendingResends();
Assert.Empty(sent);
Assert.Equal(0, stats.ResendsSent);
Assert.Equal(2, queue.CacheDepth); // watermark itself + newer seq 4
Assert.Equal(0, queue.PendingResendCount);
}
// =====================================================================
// Conformance against the N0 ACE-behaviour double
// =====================================================================

View file

@ -0,0 +1,77 @@
using AcDream.Core.Net.Transport;
namespace AcDream.Core.Net.Tests.Transport;
public sealed class RetailPacketLossAveragerTests
{
[Fact]
public void Sweep_BeforeRetailHeartbeat_DoesNotPublishPartialSample()
{
long now = 0;
var clock = new TransportClock(() => now, frequency: 10);
var stats = new TransportStats { PacketsSent = 5, NakIdsSent = 1 };
var sut = new RetailPacketLossAverager(clock, stats);
stats.PacketsSent += 10;
stats.NakIdsSent += 2;
now = 19;
sut.Sweep(now, stats);
Assert.Equal(0d, sut.Percentage);
}
[Fact]
public void Sweep_UsesRetailNakPlusRetransmitOverReceivedPlusSentFormula()
{
long now = 0;
var clock = new TransportClock(() => now, frequency: 10);
var stats = new TransportStats();
var sut = new RetailPacketLossAverager(clock, stats);
stats.PacketsSent = 60;
stats.PacketsReceived = 40;
stats.NakIdsSent = 2;
stats.ResendsSent = 3;
now = 20;
sut.Sweep(now, stats);
Assert.Equal(5d, sut.Percentage);
}
[Fact]
public void Sweep_EvictsTheOldestSampleAfterFortyHeartbeats()
{
long now = 0;
var clock = new TransportClock(() => now, frequency: 10);
var stats = new TransportStats();
var sut = new RetailPacketLossAverager(clock, stats);
stats.PacketsSent = 10;
stats.NakIdsSent = 10;
now = 20;
sut.Sweep(now, stats);
for (int i = 0; i < RetailPacketLossAverager.WindowSize; i++)
{
stats.PacketsSent += 10;
now += 20;
sut.Sweep(now, stats);
}
Assert.Equal(0d, sut.Percentage);
}
[Fact]
public void Sweep_ZeroTrafficReportsZero()
{
long now = 0;
var clock = new TransportClock(() => now, frequency: 10);
var stats = new TransportStats();
var sut = new RetailPacketLossAverager(clock, stats);
now = 20;
sut.Sweep(now, stats);
Assert.Equal(0d, sut.Percentage);
}
}