using System.Buffers.Binary; using System.Text; namespace AcDream.Core.Net.Messages; /// /// 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 /// 0xF7B1 game-action envelope. /// 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; } }