Expand the typed client-command boundary across travel, character queries, local UI and layout controls, AFK and consent, emotes, friends, squelch and filters, and fill-components. Preserve retail packet layouts and queue ownership, import the confirmation dialog, and keep authoritative social state in Core. Co-Authored-By: Codex <codex@openai.com>
235 lines
10 KiB
C#
235 lines
10 KiB
C#
using System.Buffers.Binary;
|
|
using System.Text;
|
|
|
|
namespace AcDream.Core.Net.Messages;
|
|
|
|
/// <summary>
|
|
/// Wire builders for game actions initiated by retail client-owned chat
|
|
/// commands. Keeping this family separate from generic object interaction
|
|
/// makes the command boundary explicit while reusing the normal
|
|
/// <c>0xF7B1</c> game-action envelope.
|
|
/// </summary>
|
|
public static class ClientCommandRequests
|
|
{
|
|
public const uint MarketplaceOpcode = 0x028Du;
|
|
public const uint PkArenaOpcode = 0x0027u;
|
|
public const uint PkLiteArenaOpcode = 0x0026u;
|
|
public const uint HouseRecallOpcode = 0x0262u;
|
|
public const uint MansionRecallOpcode = 0x0278u;
|
|
public const uint QueryAgeOpcode = 0x01C2u;
|
|
public const uint QueryBirthOpcode = 0x01C4u;
|
|
public const uint ConfirmationResponseOpcode = 0x0275u;
|
|
public const uint SuicideOpcode = 0x0279u;
|
|
public const uint SetAfkModeOpcode = 0x000Fu;
|
|
public const uint SetAfkMessageOpcode = 0x0010u;
|
|
public const uint EmoteOpcode = 0x01DFu;
|
|
public const uint AddFriendOpcode = 0x0018u;
|
|
public const uint RemoveFriendOpcode = 0x0017u;
|
|
public const uint ClearFriendsOpcode = 0x0025u;
|
|
public const uint ModifyCharacterSquelchOpcode = 0x0058u;
|
|
public const uint ModifyAccountSquelchOpcode = 0x0059u;
|
|
public const uint ModifyGlobalSquelchOpcode = 0x005Bu;
|
|
public const uint ClearConsentOpcode = 0x0216u;
|
|
public const uint DisplayConsentOpcode = 0x0217u;
|
|
public const uint RemoveConsentOpcode = 0x0218u;
|
|
public const uint SetDesiredComponentLevelOpcode = 0x0224u;
|
|
public const uint LegacyFriendsOpcode = 0xF7CDu;
|
|
|
|
// Named-retail anchors:
|
|
// CM_Character::Event_TeleToMarketplace @ 0x006A1C20
|
|
// CM_Character::Event_TeleToPKArena @ 0x006A1CB0
|
|
// CM_Character::Event_TeleToPKLArena @ 0x006A1D40
|
|
// CM_House::Event_TeleToHouse_Event @ 0x006AAFA0
|
|
// CM_House::Event_TeleToMansion_Event @ 0x006AB030
|
|
public static byte[] BuildMarketplace(uint sequence) =>
|
|
BuildParameterless(sequence, MarketplaceOpcode);
|
|
|
|
public static byte[] BuildPkArena(uint sequence) =>
|
|
BuildParameterless(sequence, PkArenaOpcode);
|
|
|
|
public static byte[] BuildPkLiteArena(uint sequence) =>
|
|
BuildParameterless(sequence, PkLiteArenaOpcode);
|
|
|
|
public static byte[] BuildHouseRecall(uint sequence) =>
|
|
BuildParameterless(sequence, HouseRecallOpcode);
|
|
|
|
public static byte[] BuildMansionRecall(uint sequence) =>
|
|
BuildParameterless(sequence, MansionRecallOpcode);
|
|
|
|
// Named-retail anchors:
|
|
// CM_Character::Event_QueryAge @ 0x006A1620
|
|
// CM_Character::Event_QueryBirth @ 0x006A16F0
|
|
public static byte[] BuildQueryAge(uint sequence, uint objectId = 0u) =>
|
|
BuildObjectQuery(sequence, QueryAgeOpcode, objectId);
|
|
|
|
public static byte[] BuildQueryBirth(uint sequence, uint objectId = 0u) =>
|
|
BuildObjectQuery(sequence, QueryBirthOpcode, objectId);
|
|
|
|
// CM_Character::Event_ConfirmationResponse @ 0x006A1210.
|
|
public static byte[] BuildConfirmationResponse(
|
|
uint sequence,
|
|
uint confirmationType,
|
|
uint contextId,
|
|
bool accepted)
|
|
{
|
|
byte[] body = new byte[24];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body, InteractRequests.GameActionEnvelope);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), sequence);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), ConfirmationResponseOpcode);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), confirmationType);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), contextId);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(20), accepted ? 1u : 0u);
|
|
return body;
|
|
}
|
|
|
|
// CM_Character::Event_Suicide @ 0x006A1B00.
|
|
public static byte[] BuildSuicide(uint sequence) =>
|
|
BuildParameterless(sequence, SuicideOpcode);
|
|
|
|
// CM_Communication::Event_SetAFKMode @ 0x006A3EE0.
|
|
public static byte[] BuildSetAfkMode(uint sequence, bool away) =>
|
|
BuildUInt32(sequence, SetAfkModeOpcode, away ? 1u : 0u);
|
|
|
|
// CM_Communication::Event_SetAFKMessage @ 0x006A4440.
|
|
public static byte[] BuildSetAfkMessage(uint sequence, string message) =>
|
|
BuildString(sequence, SetAfkMessageOpcode, message);
|
|
|
|
// CM_Communication::Event_Emote @ 0x006A4120.
|
|
public static byte[] BuildEmote(uint sequence, string message) =>
|
|
BuildString(sequence, EmoteOpcode, message);
|
|
|
|
// CM_Social::Event_AddFriend/RemoveFriend/ClearFriends
|
|
// @ 0x006A5C10 / 0x006A5650 / 0x006A55C0.
|
|
public static byte[] BuildAddFriend(uint sequence, string name) =>
|
|
BuildString(sequence, AddFriendOpcode, name);
|
|
|
|
public static byte[] BuildRemoveFriend(uint sequence, uint friendId) =>
|
|
BuildUInt32(sequence, RemoveFriendOpcode, friendId);
|
|
|
|
public static byte[] BuildClearFriends(uint sequence) =>
|
|
BuildParameterless(sequence, ClearFriendsOpcode);
|
|
|
|
// Proto_UI::SendFriendsCommand @ 0x00546C50 is a control message,
|
|
// not a 0xF7B1 GameAction. Command zero requests the legacy listing.
|
|
public static byte[] BuildLegacyFriendsCommand(uint command, string name)
|
|
{
|
|
byte[] packedName = PackString16L(name);
|
|
byte[] body = new byte[8 + packedName.Length];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body, LegacyFriendsOpcode);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), command);
|
|
packedName.CopyTo(body, 8);
|
|
return body;
|
|
}
|
|
|
|
// CM_Communication::Event_ModifyCharacterSquelch @ 0x006A42D0.
|
|
public static byte[] BuildModifyCharacterSquelch(
|
|
uint sequence,
|
|
bool add,
|
|
uint characterId,
|
|
string name,
|
|
uint messageType)
|
|
{
|
|
byte[] packedName = PackString16L(name);
|
|
byte[] body = CreateBody(sequence, ModifyCharacterSquelchOpcode, 12 + packedName.Length);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), add ? 1u : 0u);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), characterId);
|
|
packedName.CopyTo(body, 20);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(20 + packedName.Length), messageType);
|
|
return body;
|
|
}
|
|
|
|
// CM_Communication::Event_ModifyAccountSquelch @ 0x006A41E0.
|
|
public static byte[] BuildModifyAccountSquelch(uint sequence, bool add, string name)
|
|
{
|
|
byte[] packedName = PackString16L(name);
|
|
byte[] body = CreateBody(sequence, ModifyAccountSquelchOpcode, 4 + packedName.Length);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), add ? 1u : 0u);
|
|
packedName.CopyTo(body, 16);
|
|
return body;
|
|
}
|
|
|
|
// CM_Communication::Event_ModifyGlobalSquelch @ 0x006A3D00.
|
|
public static byte[] BuildModifyGlobalSquelch(uint sequence, bool add, uint messageType)
|
|
{
|
|
byte[] body = CreateBody(sequence, ModifyGlobalSquelchOpcode, 8);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), add ? 1u : 0u);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), messageType);
|
|
return body;
|
|
}
|
|
|
|
// CM_Character consent events @ 0x006A1180 / 0x006A1360 / 0x006A2C10.
|
|
public static byte[] BuildClearConsent(uint sequence) =>
|
|
BuildParameterless(sequence, ClearConsentOpcode);
|
|
|
|
public static byte[] BuildDisplayConsent(uint sequence) =>
|
|
BuildParameterless(sequence, DisplayConsentOpcode);
|
|
|
|
public static byte[] BuildRemoveConsent(uint sequence, string name) =>
|
|
BuildString(sequence, RemoveConsentOpcode, name);
|
|
|
|
// CM_Character::Event_SetDesiredComponentLevel @ 0x006A2350.
|
|
public static byte[] BuildSetDesiredComponentLevel(
|
|
uint sequence, uint componentId, uint amount)
|
|
{
|
|
byte[] body = CreateBody(sequence, SetDesiredComponentLevelOpcode, 8);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), componentId);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), amount);
|
|
return body;
|
|
}
|
|
|
|
private static byte[] BuildParameterless(uint sequence, uint opcode)
|
|
{
|
|
byte[] body = new byte[12];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body, InteractRequests.GameActionEnvelope);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), sequence);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), opcode);
|
|
return body;
|
|
}
|
|
|
|
private static byte[] BuildUInt32(uint sequence, uint opcode, uint value)
|
|
{
|
|
byte[] body = CreateBody(sequence, opcode, 4);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), value);
|
|
return body;
|
|
}
|
|
|
|
private static byte[] BuildString(uint sequence, uint opcode, string value)
|
|
{
|
|
byte[] packed = PackString16L(value);
|
|
byte[] body = CreateBody(sequence, opcode, packed.Length);
|
|
packed.CopyTo(body, 12);
|
|
return body;
|
|
}
|
|
|
|
private static byte[] CreateBody(uint sequence, uint opcode, int payloadLength)
|
|
{
|
|
byte[] body = new byte[12 + payloadLength];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body, InteractRequests.GameActionEnvelope);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), sequence);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), opcode);
|
|
return body;
|
|
}
|
|
|
|
private static byte[] PackString16L(string value)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(value);
|
|
byte[] data = Encoding.GetEncoding(1252).GetBytes(value);
|
|
if (data.Length > ushort.MaxValue)
|
|
throw new ArgumentException("String too long for String16L.", nameof(value));
|
|
int unpadded = 2 + data.Length;
|
|
byte[] packed = new byte[(unpadded + 3) & ~3];
|
|
BinaryPrimitives.WriteUInt16LittleEndian(packed, (ushort)data.Length);
|
|
data.CopyTo(packed, 2);
|
|
return packed;
|
|
}
|
|
|
|
private static byte[] BuildObjectQuery(uint sequence, uint opcode, uint objectId)
|
|
{
|
|
byte[] body = new byte[16];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body, InteractRequests.GameActionEnvelope);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), sequence);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), opcode);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), objectId);
|
|
return body;
|
|
}
|
|
}
|