fix: complete retail parity stability pass
This commit is contained in:
parent
d3df4cb20a
commit
f7aa8e0eb7
131 changed files with 7765 additions and 1190 deletions
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// =====================================================================
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue