acdream/tests/AcDream.Core.Net.Tests/Messages/ClientCommandRequestsTests.cs
Erik ac2ca8f965 feat(ui): port retail spellbook interactions
Resolve the authored spell shortcut row prototype so the spellbook presents the retail icon, name, separator, selected overlay, and scrollbar geometry. Port exact school and level filters, stable display ordering, selection exposure, and learned-spell drags into the open favorite bar.

Route deletion through the shared retail confirmation dialog and send CM_Magic::Event_RemoveSpell only after an affirmative answer, leaving the inbound server notice authoritative for list state.

Co-Authored-By: Codex <noreply@openai.com>
2026-07-15 16:26:30 +02:00

192 lines
7.8 KiB
C#

using System.Buffers.Binary;
using AcDream.Core.Net.Messages;
namespace AcDream.Core.Net.Tests.Messages;
public sealed class ClientCommandRequestsTests
{
public static TheoryData<Func<uint, byte[]>, uint> ParameterlessActions => new()
{
{ ClientCommandRequests.BuildMarketplace, ClientCommandRequests.MarketplaceOpcode },
{ ClientCommandRequests.BuildPkArena, ClientCommandRequests.PkArenaOpcode },
{ ClientCommandRequests.BuildPkLiteArena, ClientCommandRequests.PkLiteArenaOpcode },
{ ClientCommandRequests.BuildHouseRecall, ClientCommandRequests.HouseRecallOpcode },
{ ClientCommandRequests.BuildMansionRecall, ClientCommandRequests.MansionRecallOpcode },
{ ClientCommandRequests.BuildSuicide, ClientCommandRequests.SuicideOpcode },
{ ClientCommandRequests.BuildClearFriends, ClientCommandRequests.ClearFriendsOpcode },
{ ClientCommandRequests.BuildClearConsent, ClientCommandRequests.ClearConsentOpcode },
{ ClientCommandRequests.BuildDisplayConsent, ClientCommandRequests.DisplayConsentOpcode },
};
[Theory]
[MemberData(nameof(ParameterlessActions))]
public void ParameterlessAction_MatchesRetailEnvelope(
Func<uint, byte[]> build, uint opcode)
{
byte[] body = build(0x1234u);
Assert.Equal(12, body.Length);
Assert.Equal(InteractRequests.GameActionEnvelope, Read(body, 0));
Assert.Equal(0x1234u, Read(body, 4));
Assert.Equal(opcode, Read(body, 8));
}
[Theory]
[InlineData(false, ClientCommandRequests.QueryAgeOpcode)]
[InlineData(true, ClientCommandRequests.QueryBirthOpcode)]
public void CharacterQuery_MatchesRetailPayload(bool birth, uint opcode)
{
byte[] body = birth
? ClientCommandRequests.BuildQueryBirth(7u, 0x50000001u)
: ClientCommandRequests.BuildQueryAge(7u, 0x50000001u);
Assert.Equal(16, body.Length);
Assert.Equal(InteractRequests.GameActionEnvelope, Read(body, 0));
Assert.Equal(7u, Read(body, 4));
Assert.Equal(opcode, Read(body, 8));
Assert.Equal(0x50000001u, Read(body, 12));
}
[Fact]
public void ConfirmationResponse_MatchesRetailPayload()
{
byte[] body = ClientCommandRequests.BuildConfirmationResponse(
9u, confirmationType: 7u, contextId: 42u, accepted: true);
Assert.Equal(24, body.Length);
Assert.Equal(InteractRequests.GameActionEnvelope, Read(body, 0));
Assert.Equal(9u, Read(body, 4));
Assert.Equal(ClientCommandRequests.ConfirmationResponseOpcode, Read(body, 8));
Assert.Equal(7u, Read(body, 12));
Assert.Equal(42u, Read(body, 16));
Assert.Equal(1u, Read(body, 20));
}
public static TheoryData<Func<uint, string, byte[]>, uint> StringActions => new()
{
{ ClientCommandRequests.BuildSetAfkMessage, ClientCommandRequests.SetAfkMessageOpcode },
{ ClientCommandRequests.BuildEmote, ClientCommandRequests.EmoteOpcode },
{ ClientCommandRequests.BuildAddFriend, ClientCommandRequests.AddFriendOpcode },
{ ClientCommandRequests.BuildRemoveConsent, ClientCommandRequests.RemoveConsentOpcode },
};
[Theory]
[MemberData(nameof(StringActions))]
public void StringAction_PacksAlignedCp1252String16L(
Func<uint, string, byte[]> build, uint opcode)
{
byte[] body = build(11u, "Bjørn");
Assert.Equal(20, body.Length);
Assert.Equal(InteractRequests.GameActionEnvelope, Read(body, 0));
Assert.Equal(11u, Read(body, 4));
Assert.Equal(opcode, 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]);
}
[Fact]
public void SetAfkMode_MatchesRetailBooleanPayload()
{
byte[] body = ClientCommandRequests.BuildSetAfkMode(12u, away: true);
Assert.Equal(16, body.Length);
Assert.Equal(ClientCommandRequests.SetAfkModeOpcode, Read(body, 8));
Assert.Equal(1u, Read(body, 12));
}
[Fact]
public void FriendAndGlobalSquelchIntegerActions_MatchRetailPayloads()
{
byte[] remove = ClientCommandRequests.BuildRemoveFriend(2u, 0x50000001u);
byte[] filter = ClientCommandRequests.BuildModifyGlobalSquelch(3u, add: false, 17u);
Assert.Equal(ClientCommandRequests.RemoveFriendOpcode, Read(remove, 8));
Assert.Equal(0x50000001u, Read(remove, 12));
Assert.Equal(ClientCommandRequests.ModifyGlobalSquelchOpcode, Read(filter, 8));
Assert.Equal(0u, Read(filter, 12));
Assert.Equal(17u, Read(filter, 16));
}
[Fact]
public void CharacterSquelch_PreservesRetailFieldOrder()
{
byte[] body = ClientCommandRequests.BuildModifyCharacterSquelch(
4u, add: true, 0x50000001u, "Alice", 12u);
Assert.Equal(32, body.Length);
Assert.Equal(ClientCommandRequests.ModifyCharacterSquelchOpcode, Read(body, 8));
Assert.Equal(1u, Read(body, 12));
Assert.Equal(0x50000001u, Read(body, 16));
Assert.Equal(5, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(20)));
Assert.Equal(12u, Read(body, 28));
}
[Fact]
public void AccountSquelch_PreservesRetailFieldOrder()
{
byte[] body = ClientCommandRequests.BuildModifyAccountSquelch(
5u, add: false, "Alice");
Assert.Equal(24, body.Length);
Assert.Equal(ClientCommandRequests.ModifyAccountSquelchOpcode, Read(body, 8));
Assert.Equal(0u, Read(body, 12));
Assert.Equal(5, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(16)));
}
[Fact]
public void DesiredComponentLevel_MatchesRetailTwoIntegerPayload()
{
byte[] body = ClientCommandRequests.BuildSetDesiredComponentLevel(
6u, 0x12000042u, 25u);
Assert.Equal(20, body.Length);
Assert.Equal(ClientCommandRequests.SetDesiredComponentLevelOpcode, Read(body, 8));
Assert.Equal(0x12000042u, Read(body, 12));
Assert.Equal(25u, Read(body, 16));
}
[Fact]
public void AddSpellFavorite_MatchesRetailThreeIntegerPayload()
{
byte[] body = ClientCommandRequests.BuildAddSpellFavorite(9u, 42u, 3, 7);
Assert.Equal(24, body.Length);
Assert.Equal(ClientCommandRequests.AddSpellFavoriteOpcode, Read(body, 8));
Assert.Equal(42u, Read(body, 12));
Assert.Equal(3u, Read(body, 16));
Assert.Equal(7u, Read(body, 20));
}
[Fact]
public void RemoveFavoriteFilterAndLearnedSpell_MatchRetailPayloads()
{
byte[] remove = ClientCommandRequests.BuildRemoveSpellFavorite(2u, 42u, 6);
byte[] filter = ClientCommandRequests.BuildSpellbookFilter(3u, 0xA5u);
byte[] removeLearned = ClientCommandRequests.BuildRemoveSpell(4u, 77u);
Assert.Equal(ClientCommandRequests.RemoveSpellFavoriteOpcode, Read(remove, 8));
Assert.Equal(42u, Read(remove, 12));
Assert.Equal(6u, Read(remove, 16));
Assert.Equal(ClientCommandRequests.SpellbookFilterOpcode, Read(filter, 8));
Assert.Equal(0xA5u, Read(filter, 12));
Assert.Equal(16, removeLearned.Length);
Assert.Equal(ClientCommandRequests.RemoveSpellOpcode, Read(removeLearned, 8));
Assert.Equal(77u, Read(removeLearned, 12));
}
[Fact]
public void LegacyFriendsCommand_IsAControlMessageWithoutGameActionEnvelope()
{
byte[] body = ClientCommandRequests.BuildLegacyFriendsCommand(0u, string.Empty);
Assert.Equal(12, body.Length);
Assert.Equal(ClientCommandRequests.LegacyFriendsOpcode, Read(body, 0));
Assert.Equal(0u, Read(body, 4));
Assert.Equal(0, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(8)));
}
private static uint Read(byte[] body, int offset) =>
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(offset));
}