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

@ -17,6 +17,20 @@ public sealed class ClientCommandRequestsTests
{ ClientCommandRequests.BuildClearFriends, ClientCommandRequests.ClearFriendsOpcode },
{ ClientCommandRequests.BuildClearConsent, ClientCommandRequests.ClearConsentOpcode },
{ ClientCommandRequests.BuildDisplayConsent, ClientCommandRequests.DisplayConsentOpcode },
{ ClientCommandRequests.BuildQueryAllegianceName, 0x0030u },
{ ClientCommandRequests.BuildClearAllegianceName, 0x0031u },
{ ClientCommandRequests.BuildListAllegianceOfficerTitles, 0x003Du },
{ ClientCommandRequests.BuildClearAllegianceOfficerTitles, 0x003Eu },
{ ClientCommandRequests.BuildListAllegianceBans, 0x02A3u },
{ ClientCommandRequests.BuildListAllegianceOfficers, 0x02A6u },
{ ClientCommandRequests.BuildClearAllegianceOfficers, 0x02A7u },
{ ClientCommandRequests.BuildQueryMotd, 0x0255u },
{ ClientCommandRequests.BuildClearMotd, 0x0256u },
{ ClientCommandRequests.BuildRemoveAllPermanentGuests, 0x025Eu },
{ ClientCommandRequests.BuildAddAllStoragePermission, 0x025Cu },
{ ClientCommandRequests.BuildRemoveAllStoragePermission, 0x024Cu },
{ ClientCommandRequests.BuildRequestFullGuestList, 0x024Du },
{ ClientCommandRequests.BuildBootEveryone, 0x025Fu },
};
[Theory]
@ -70,6 +84,15 @@ public sealed class ClientCommandRequestsTests
{ ClientCommandRequests.BuildSoulEmote, ClientCommandRequests.SoulEmoteOpcode },
{ ClientCommandRequests.BuildAddFriend, ClientCommandRequests.AddFriendOpcode },
{ ClientCommandRequests.BuildRemoveConsent, ClientCommandRequests.RemoveConsentOpcode },
{ ClientCommandRequests.BuildAddAllegianceBan, 0x02A1u },
{ ClientCommandRequests.BuildRemoveAllegianceBan, 0x02A2u },
{ ClientCommandRequests.BuildRemoveAllegianceOfficer, 0x02A5u },
{ ClientCommandRequests.BuildSetAllegianceName, 0x0033u },
{ ClientCommandRequests.BuildSetAllegianceApprovedVassal, 0x0040u },
{ ClientCommandRequests.BuildSetMotd, 0x0254u },
{ ClientCommandRequests.BuildAddPermanentGuest, 0x0245u },
{ ClientCommandRequests.BuildRemovePermanentGuest, 0x0246u },
{ ClientCommandRequests.BuildBootSpecificHouseGuest, 0x024Au },
};
[Theory]
@ -98,6 +121,94 @@ public sealed class ClientCommandRequestsTests
Assert.Equal(1u, Read(body, 12));
}
[Theory]
[InlineData(0x003Fu, 6u)]
[InlineData(0x0042u, 5u)]
public void AllegianceAction_PreservesRetailUInt32Payload(
uint expectedOpcode,
uint action)
{
byte[] body = expectedOpcode == 0x003Fu
? ClientCommandRequests.BuildAllegianceLockAction(12u, action)
: ClientCommandRequests.BuildAllegianceHouseAction(12u, action);
Assert.Equal(16, body.Length);
Assert.Equal(expectedOpcode, Read(body, 8));
Assert.Equal(action, Read(body, 12));
}
[Theory]
[InlineData(0x0247u)]
[InlineData(0x0266u)]
[InlineData(0x0267u)]
[InlineData(0x0268u)]
public void HouseBooleanAction_UsesRetailOneOrZeroPayload(uint expectedOpcode)
{
byte[] enabled = expectedOpcode switch
{
0x0247u => ClientCommandRequests.BuildSetOpenHouseStatus(13u, true),
0x0266u => ClientCommandRequests.BuildSetHooksVisibility(13u, true),
0x0267u => ClientCommandRequests.BuildModifyAllegianceGuestPermission(13u, true),
_ => ClientCommandRequests.BuildModifyAllegianceStoragePermission(13u, true),
};
Assert.Equal(16, enabled.Length);
Assert.Equal(expectedOpcode, Read(enabled, 8));
Assert.Equal(1u, Read(enabled, 12));
}
[Fact]
public void AllegianceStringBooleanActions_PreserveStringThenUInt32Order()
{
byte[] boot = ClientCommandRequests.BuildBreakAllegianceBoot(
14u, "Bjørn", accountBoot: true);
byte[] gag = ClientCommandRequests.BuildAllegianceChatGag(
15u, "Bjørn", enabled: false);
byte[] storage = ClientCommandRequests.BuildChangeStoragePermission(
16u, "Bjørn", enabled: true);
AssertStringThenUInt32(boot, 0x0277u, 1u);
AssertStringThenUInt32(gag, 0x0041u, 0u);
AssertStringThenUInt32(storage, 0x0249u, 1u);
}
[Fact]
public void SetAllegianceOfficer_PreservesNameThenLevelOrder()
{
byte[] body = ClientCommandRequests.BuildSetAllegianceOfficer(
17u, "Bjørn", 3u);
AssertStringThenUInt32(body, 0x003Bu, 3u);
}
[Fact]
public void SetAllegianceOfficerTitle_PreservesLevelThenTitleOrder()
{
byte[] body = ClientCommandRequests.BuildSetAllegianceOfficerTitle(
18u, 2u, "Marshal");
Assert.Equal(28, body.Length);
Assert.Equal(0x003Cu, Read(body, 8));
Assert.Equal(2u, Read(body, 12));
Assert.Equal(7, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(16)));
Assert.Equal("Marshal"u8.ToArray(), body[18..25]);
Assert.Equal([0, 0, 0], body[25..28]);
}
[Fact]
public void AllegianceChatBoot_PreservesTwoAlignedString16LFields()
{
byte[] body = ClientCommandRequests.BuildAllegianceChatBoot(
19u, "Bob", "No.");
Assert.Equal(28, body.Length);
Assert.Equal(0x02A0u, Read(body, 8));
Assert.Equal(3, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(12)));
Assert.Equal("Bob"u8.ToArray(), body[14..17]);
Assert.Equal(3, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(20)));
Assert.Equal("No."u8.ToArray(), body[22..25]);
}
[Fact]
public void FriendAndGlobalSquelchIntegerActions_MatchRetailPayloads()
{
@ -191,4 +302,17 @@ public sealed class ClientCommandRequestsTests
private static uint Read(byte[] body, int offset) =>
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(offset));
private static void AssertStringThenUInt32(
byte[] body,
uint expectedOpcode,
uint expectedValue)
{
Assert.Equal(24, body.Length);
Assert.Equal(expectedOpcode, Read(body, 8));
Assert.Equal(5, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(12)));
Assert.Equal([0x42, 0x6A, 0xF8, 0x72, 0x6E], body[14..19]);
Assert.Equal(0, body[19]);
Assert.Equal(expectedValue, Read(body, 20));
}
}

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

View file

@ -0,0 +1,95 @@
using System.Net;
using AcDream.Core.Net.Messages;
namespace AcDream.Core.Net.Tests;
public sealed class WorldSessionAdministrationCommandTests
{
[Fact]
public void EveryAdministrationWrapper_UsesSharedSequenceAndExactBuilder()
{
using var session = new WorldSession(
new IPEndPoint(IPAddress.Loopback, 65000));
var actual = new List<byte[]>();
session.GameActionCapture = actual.Add;
session.SendBreakAllegianceBoot("Bob", true);
session.SendAllegianceChatBoot("Bob", "Reason");
session.SendAllegianceChatGag("Bob", false);
session.SendAddAllegianceBan("Bob");
session.SendRemoveAllegianceBan("Bob");
session.SendListAllegianceBans();
session.SendSetAllegianceOfficer("Bob", 2u);
session.SendRemoveAllegianceOfficer("Bob");
session.SendListAllegianceOfficers();
session.SendClearAllegianceOfficers();
session.SendSetAllegianceOfficerTitle(2u, "Regent");
session.SendListAllegianceOfficerTitles();
session.SendClearAllegianceOfficerTitles();
session.SendQueryAllegianceName();
session.SendSetAllegianceName("Guild");
session.SendClearAllegianceName();
session.SendAllegianceLockAction(3u);
session.SendSetAllegianceApprovedVassal("Bob");
session.SendAllegianceHouseAction(4u);
session.SendQueryMotd();
session.SendSetMotd("Welcome");
session.SendClearMotd();
session.SendSetOpenHouseStatus(true);
session.SendAddPermanentGuest("Bob");
session.SendRemovePermanentGuest("Bob");
session.SendRemoveAllPermanentGuests();
session.SendChangeStoragePermission("Bob", true);
session.SendAddAllStoragePermission();
session.SendRemoveAllStoragePermission();
session.SendRequestFullGuestList();
session.SendBootSpecificHouseGuest("Bob");
session.SendBootEveryone();
session.SendSetHooksVisibility(false);
session.SendModifyAllegianceGuestPermission(true);
session.SendModifyAllegianceStoragePermission(false);
byte[][] expected =
[
ClientCommandRequests.BuildBreakAllegianceBoot(1u, "Bob", true),
ClientCommandRequests.BuildAllegianceChatBoot(2u, "Bob", "Reason"),
ClientCommandRequests.BuildAllegianceChatGag(3u, "Bob", false),
ClientCommandRequests.BuildAddAllegianceBan(4u, "Bob"),
ClientCommandRequests.BuildRemoveAllegianceBan(5u, "Bob"),
ClientCommandRequests.BuildListAllegianceBans(6u),
ClientCommandRequests.BuildSetAllegianceOfficer(7u, "Bob", 2u),
ClientCommandRequests.BuildRemoveAllegianceOfficer(8u, "Bob"),
ClientCommandRequests.BuildListAllegianceOfficers(9u),
ClientCommandRequests.BuildClearAllegianceOfficers(10u),
ClientCommandRequests.BuildSetAllegianceOfficerTitle(11u, 2u, "Regent"),
ClientCommandRequests.BuildListAllegianceOfficerTitles(12u),
ClientCommandRequests.BuildClearAllegianceOfficerTitles(13u),
ClientCommandRequests.BuildQueryAllegianceName(14u),
ClientCommandRequests.BuildSetAllegianceName(15u, "Guild"),
ClientCommandRequests.BuildClearAllegianceName(16u),
ClientCommandRequests.BuildAllegianceLockAction(17u, 3u),
ClientCommandRequests.BuildSetAllegianceApprovedVassal(18u, "Bob"),
ClientCommandRequests.BuildAllegianceHouseAction(19u, 4u),
ClientCommandRequests.BuildQueryMotd(20u),
ClientCommandRequests.BuildSetMotd(21u, "Welcome"),
ClientCommandRequests.BuildClearMotd(22u),
ClientCommandRequests.BuildSetOpenHouseStatus(23u, true),
ClientCommandRequests.BuildAddPermanentGuest(24u, "Bob"),
ClientCommandRequests.BuildRemovePermanentGuest(25u, "Bob"),
ClientCommandRequests.BuildRemoveAllPermanentGuests(26u),
ClientCommandRequests.BuildChangeStoragePermission(27u, "Bob", true),
ClientCommandRequests.BuildAddAllStoragePermission(28u),
ClientCommandRequests.BuildRemoveAllStoragePermission(29u),
ClientCommandRequests.BuildRequestFullGuestList(30u),
ClientCommandRequests.BuildBootSpecificHouseGuest(31u, "Bob"),
ClientCommandRequests.BuildBootEveryone(32u),
ClientCommandRequests.BuildSetHooksVisibility(33u, false),
ClientCommandRequests.BuildModifyAllegianceGuestPermission(34u, true),
ClientCommandRequests.BuildModifyAllegianceStoragePermission(35u, false),
];
Assert.Equal(expected.Length, actual.Count);
for (int i = 0; i < expected.Length; i++)
Assert.Equal(expected[i], actual[i]);
}
}