acdream never implemented @pklite. It is a CLIENT command in retail, not a
server one — ACE has no pklite text-command handler — so typing it forwarded as
inert chat text that the server ignored.
Retail: ClientCommunicationSystem::DoPKLite @0x0057A490 rejects with
WeenieError 0x507 when ACCWeenieObject::IsPlayerKiller @0x0058C910 is true
(that returns true when EITHER the PK bit 0x20 OR the PKLite bit 0x2000000 is
set), prints "Please see @help pklite for more..." and sends nothing if given
any argument text, and otherwise calls CM_Character::Event_EnterPKLite
@0x006A13F0 — a bare 12-byte parameterless game action, opcode 0x28F, the same
shape as Event_LoginCompleteNotification beside it. Verb string at 0x007E16B0,
help text at 0x007DF0C8, failure string at 0x007D31E8; one verb, no alias.
HasPlayerFlag is a tri-state (null = the local PublicWeenieDesc has not
arrived). The existing arena gates compare `== false` because they reject on a
known-FALSE flag; retail's DoPKLite gates the other way, rejecting on
known-TRUE. So this case compares `== true` on either bit: an indeterminate
description sends rather than blocks, which matches retail trusting the server
instead of inventing a client-side suppression rule.
Landed as its own commit because it is retail-faithful on its own merits, but
the motivation is C4 route 2: ACE advances SequenceType.ObjectForcePosition in
exactly two places, and the only reachable one is Player.HandleActionEnterPkLite's
entry-collision bump (allow_pkl_bump, default on). Every admin teleport advances
ObjectTeleport instead, so @teleto-style displacement exercises route 3, not
route 2. Without this command route 2 has no connected acceptance gate at all.
Gates: complete Release solution 10,867 passed / 4 skipped / 0 failed
(9966b531 baseline 10,858/4/0; +9 = the 9 tests added). Coverage includes both
known-true rejections, the known-false success case, the tri-state unknown
case, the 12-byte wire envelope, and @pklite resolving as ClientHandled rather
than falling through to the server-text path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
193 lines
7.9 KiB
C#
193 lines
7.9 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.BuildEnterPkLite, ClientCommandRequests.EnterPkLiteOpcode },
|
|
{ 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));
|
|
}
|