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 EnterPkLiteOpcode = 0x028Fu; 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 SoulEmoteOpcode = 0x01E1u; public const uint AddFriendOpcode = 0x0018u; public const uint AbandonContractOpcode = 0x0316u; 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 AddSpellFavoriteOpcode = 0x01E3u; public const uint RemoveSpellFavoriteOpcode = 0x01E4u; public const uint SpellbookFilterOpcode = 0x0286u; public const uint RemoveSpellOpcode = 0x01A8u; public const uint LegacyFriendsOpcode = 0xF7CDu; // Campaign CH slice CH4 (2026-08-09): command-registry completion. // GameActionType opcodes cross-checked against ACE // (Source/ACE.Server/Network/GameAction/GameActionType.cs) — ACE's // numbering is the same wire protocol retail uses, matching this // file's existing cross-check convention (see ClientCommandRequests // class doc). Each handler's payload shape was read directly from // ACE's GameAction Actions/*.cs reader (cited per-method below) rather // than guessed. public const uint IndexChannelsOpcode = 0x0149u; public const uint ListChannelsOpcode = 0x0148u; public const uint AddChannelOpcode = 0x0145u; public const uint RemoveChannelOpcode = 0x0146u; public const uint RecallAllegianceHometownOpcode = 0x02ABu; public const uint AllegianceInfoRequestOpcode = 0x027Bu; public const uint ListAvailableHousesOpcode = 0x0270u; public const uint AddPlayerPermissionOpcode = 0x0219u; public const uint RemovePlayerPermissionOpcode = 0x021Au; public const uint AbandonHouseOpcode = 0x021Fu; public const uint QueryAllegianceNameOpcode = 0x0030u; public const uint ClearAllegianceNameOpcode = 0x0031u; public const uint SetAllegianceNameOpcode = 0x0033u; public const uint SetAllegianceOfficerOpcode = 0x003Bu; public const uint SetAllegianceOfficerTitleOpcode = 0x003Cu; public const uint ListAllegianceOfficerTitlesOpcode = 0x003Du; public const uint ClearAllegianceOfficerTitlesOpcode = 0x003Eu; public const uint DoAllegianceLockActionOpcode = 0x003Fu; public const uint SetAllegianceApprovedVassalOpcode = 0x0040u; public const uint AllegianceChatGagOpcode = 0x0041u; public const uint DoAllegianceHouseActionOpcode = 0x0042u; public const uint AddPermanentGuestOpcode = 0x0245u; public const uint RemovePermanentGuestOpcode = 0x0246u; public const uint SetOpenHouseStatusOpcode = 0x0247u; public const uint ChangeStoragePermissionOpcode = 0x0249u; public const uint BootSpecificHouseGuestOpcode = 0x024Au; public const uint RemoveAllStoragePermissionOpcode = 0x024Cu; public const uint RequestFullGuestListOpcode = 0x024Du; public const uint SetMotdOpcode = 0x0254u; public const uint QueryMotdOpcode = 0x0255u; public const uint ClearMotdOpcode = 0x0256u; public const uint AddAllStoragePermissionOpcode = 0x025Cu; public const uint RemoveAllPermanentGuestsOpcode = 0x025Eu; public const uint BootEveryoneOpcode = 0x025Fu; public const uint SetHooksVisibilityOpcode = 0x0266u; public const uint ModifyAllegianceGuestPermissionOpcode = 0x0267u; public const uint ModifyAllegianceStoragePermissionOpcode = 0x0268u; public const uint BreakAllegianceBootOpcode = 0x0277u; public const uint AllegianceChatBootOpcode = 0x02A0u; public const uint AddAllegianceBanOpcode = 0x02A1u; public const uint RemoveAllegianceBanOpcode = 0x02A2u; public const uint ListAllegianceBansOpcode = 0x02A3u; public const uint RemoveAllegianceOfficerOpcode = 0x02A5u; public const uint ListAllegianceOfficersOpcode = 0x02A6u; public const uint ClearAllegianceOfficersOpcode = 0x02A7u; // Batch C (Map/House toolbar panel, 2026-08-17): the query the House // tab needs to populate. ACE GameActionHouseQuery.cs: [GameAction( // GameActionType.HouseQuery)] (0x021E), Handle reads no payload and // calls session.Player.HandleActionQueryHouse() — which replies with // either GameEventHouseStatus (0x0226, no house owned) or // GameEventHouseData (0x0225, house owned). No known retail client // call site was found in this session's decomp reading (gmHouseUI:: // PostInit never sends it) — HousePageController fires it when the // House tab becomes visible, an acdream convention, not a ported // retail trigger. public const uint HouseQueryOpcode = 0x021Eu; // 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); // CM_Character::Event_EnterPKLite @ 0x006A13F0 — 12-byte parameterless // game action, opcode 0x28F, identical shape to Event_LoginCompleteNotification. public static byte[] BuildEnterPkLite(uint sequence) => BuildParameterless(sequence, EnterPkLiteOpcode); 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_Communication::Event_SoulEmote @ 0x006A4500. public static byte[] BuildSoulEmote(uint sequence, string message) => BuildString(sequence, SoulEmoteOpcode, message); // CM_Social::Event_AddFriend/RemoveFriend/ClearFriends // @ 0x006A5C10 / 0x006A5650 / 0x006A55C0. public static byte[] BuildAddFriend(uint sequence, string name) => BuildString(sequence, AddFriendOpcode, name); /// /// Campaign QJ: abandon a tracked contract — game action 0x0316, /// payload one contract id. The server answers with 0x0315 carrying /// DeleteContract, which is why the client does not remove the row /// itself (ACE: GameActionAbandonContract). /// public static byte[] BuildAbandonContract(uint sequence, uint contractId) => BuildUInt32(sequence, AbandonContractOpcode, contractId); 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; } // CM_Character::Event_AddSpellFavorite @ 0x006A0F70. public static byte[] BuildAddSpellFavorite( uint sequence, uint spellId, int position, int tabIndex) { byte[] body = CreateBody(sequence, AddSpellFavoriteOpcode, 12); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), spellId); BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(16), position); BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(20), tabIndex); return body; } // CM_Character::Event_RemoveSpellFavorite @ 0x006A1890. public static byte[] BuildRemoveSpellFavorite( uint sequence, uint spellId, int tabIndex) { byte[] body = CreateBody(sequence, RemoveSpellFavoriteOpcode, 8); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), spellId); BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(16), tabIndex); return body; } // CM_Character::Event_SpellbookFilterEvent @ 0x006A1A30. public static byte[] BuildSpellbookFilter(uint sequence, uint filters) => BuildUInt32(sequence, SpellbookFilterOpcode, filters); // CM_Magic::Event_RemoveSpell @ 0x006A3220. public static byte[] BuildRemoveSpell(uint sequence, uint spellId) => BuildUInt32(sequence, RemoveSpellOpcode, spellId); // @index — GameActionChannelIndex.Handle: no payload read. public static byte[] BuildIndexChannels(uint sequence) => BuildParameterless(sequence, IndexChannelsOpcode); // @clist — GameActionChannelList.Handle: // ReadUInt32() as the Channel bitflag. public static byte[] BuildListChannel(uint sequence, uint channelId) => BuildUInt32(sequence, ListChannelsOpcode, channelId); // @on — GameActionAddChannel.Handle: ReadUInt32() Channel id. public static byte[] BuildOnChannel(uint sequence, uint channelId) => BuildUInt32(sequence, AddChannelOpcode, channelId); // @off — GameActionRemoveChannel.Handle: ReadUInt32() Channel id. public static byte[] BuildOffChannel(uint sequence, uint channelId) => BuildUInt32(sequence, RemoveChannelOpcode, channelId); // @alh / @ah / "@allegiance hometown" — // GameActionRecallAllegianceHometown.Handle: no payload read. public static byte[] BuildRecallAllegianceHometown(uint sequence) => BuildParameterless(sequence, RecallAllegianceHometownOpcode); // "@allegiance info " — GameActionAllegianceInfoRequest.Handle: // ReadString16L() player name. Retail refuses an empty argument before // constructing this request. public static byte[] BuildAllegianceInfoRequest(uint sequence, string playerName) => BuildString(sequence, AllegianceInfoRequestOpcode, playerName); // @hslist / "@house available" — // GameActionHouseListAvailable.Handle: ReadUInt32() as the ACE // HouseType enum (Undef=0, Cottage=1, Villa=2, Mansion=3, Apartment=4). public static byte[] BuildListAvailableHouses(uint sequence, uint houseType) => BuildUInt32(sequence, ListAvailableHousesOpcode, houseType); // @permit add — GameActionAddPlayerPermission.Handle: // ReadString16L() player name. public static byte[] BuildAddPlayerPermission(uint sequence, string playerName) => BuildString(sequence, AddPlayerPermissionOpcode, playerName); // @permit remove — GameActionRemovePlayerPermission.Handle: // ReadString16L() player name. public static byte[] BuildRemovePlayerPermission(uint sequence, string playerName) => BuildString(sequence, RemovePlayerPermissionOpcode, playerName); // "@house abandon" — GameActionHouseAbandon.Handle: no payload read. public static byte[] BuildAbandonHouse(uint sequence) => BuildParameterless(sequence, AbandonHouseOpcode); // Complete named-retail @allegiance/@house administration family. // Opcodes and field order are cross-checked against ACE's individual // GameAction readers; the client-side grammar is ported separately by // ClientCommandController from DoAllegiance/DoHouse and their helpers. public static byte[] BuildBreakAllegianceBoot( uint sequence, string playerName, bool accountBoot) => BuildStringUInt32( sequence, BreakAllegianceBootOpcode, playerName, accountBoot ? 1u : 0u); public static byte[] BuildAllegianceChatBoot( uint sequence, string playerName, string reason) => BuildTwoStrings(sequence, AllegianceChatBootOpcode, playerName, reason); public static byte[] BuildAllegianceChatGag( uint sequence, string playerName, bool enabled) => BuildStringUInt32( sequence, AllegianceChatGagOpcode, playerName, enabled ? 1u : 0u); public static byte[] BuildAddAllegianceBan(uint sequence, string playerName) => BuildString(sequence, AddAllegianceBanOpcode, playerName); public static byte[] BuildRemoveAllegianceBan(uint sequence, string playerName) => BuildString(sequence, RemoveAllegianceBanOpcode, playerName); public static byte[] BuildListAllegianceBans(uint sequence) => BuildParameterless(sequence, ListAllegianceBansOpcode); public static byte[] BuildSetAllegianceOfficer( uint sequence, string playerName, uint officerLevel) => BuildStringUInt32( sequence, SetAllegianceOfficerOpcode, playerName, officerLevel); public static byte[] BuildRemoveAllegianceOfficer( uint sequence, string playerName) => BuildString(sequence, RemoveAllegianceOfficerOpcode, playerName); public static byte[] BuildListAllegianceOfficers(uint sequence) => BuildParameterless(sequence, ListAllegianceOfficersOpcode); public static byte[] BuildClearAllegianceOfficers(uint sequence) => BuildParameterless(sequence, ClearAllegianceOfficersOpcode); public static byte[] BuildSetAllegianceOfficerTitle( uint sequence, uint officerLevel, string title) => BuildUInt32String( sequence, SetAllegianceOfficerTitleOpcode, officerLevel, title); public static byte[] BuildListAllegianceOfficerTitles(uint sequence) => BuildParameterless(sequence, ListAllegianceOfficerTitlesOpcode); public static byte[] BuildClearAllegianceOfficerTitles(uint sequence) => BuildParameterless(sequence, ClearAllegianceOfficerTitlesOpcode); public static byte[] BuildQueryAllegianceName(uint sequence) => BuildParameterless(sequence, QueryAllegianceNameOpcode); public static byte[] BuildSetAllegianceName(uint sequence, string name) => BuildString(sequence, SetAllegianceNameOpcode, name); public static byte[] BuildClearAllegianceName(uint sequence) => BuildParameterless(sequence, ClearAllegianceNameOpcode); public static byte[] BuildAllegianceLockAction(uint sequence, uint action) => BuildUInt32(sequence, DoAllegianceLockActionOpcode, action); public static byte[] BuildSetAllegianceApprovedVassal( uint sequence, string playerName) => BuildString(sequence, SetAllegianceApprovedVassalOpcode, playerName); public static byte[] BuildAllegianceHouseAction(uint sequence, uint action) => BuildUInt32(sequence, DoAllegianceHouseActionOpcode, action); public static byte[] BuildQueryMotd(uint sequence) => BuildParameterless(sequence, QueryMotdOpcode); public static byte[] BuildSetMotd(uint sequence, string motd) => BuildString(sequence, SetMotdOpcode, motd); public static byte[] BuildClearMotd(uint sequence) => BuildParameterless(sequence, ClearMotdOpcode); public static byte[] BuildSetOpenHouseStatus(uint sequence, bool isOpen) => BuildUInt32(sequence, SetOpenHouseStatusOpcode, isOpen ? 1u : 0u); public static byte[] BuildAddPermanentGuest(uint sequence, string playerName) => BuildString(sequence, AddPermanentGuestOpcode, playerName); public static byte[] BuildRemovePermanentGuest(uint sequence, string playerName) => BuildString(sequence, RemovePermanentGuestOpcode, playerName); public static byte[] BuildRemoveAllPermanentGuests(uint sequence) => BuildParameterless(sequence, RemoveAllPermanentGuestsOpcode); public static byte[] BuildChangeStoragePermission( uint sequence, string playerName, bool enabled) => BuildStringUInt32( sequence, ChangeStoragePermissionOpcode, playerName, enabled ? 1u : 0u); public static byte[] BuildAddAllStoragePermission(uint sequence) => BuildParameterless(sequence, AddAllStoragePermissionOpcode); public static byte[] BuildRemoveAllStoragePermission(uint sequence) => BuildParameterless(sequence, RemoveAllStoragePermissionOpcode); public static byte[] BuildRequestFullGuestList(uint sequence) => BuildParameterless(sequence, RequestFullGuestListOpcode); public static byte[] BuildBootSpecificHouseGuest( uint sequence, string playerName) => BuildString(sequence, BootSpecificHouseGuestOpcode, playerName); public static byte[] BuildBootEveryone(uint sequence) => BuildParameterless(sequence, BootEveryoneOpcode); public static byte[] BuildSetHooksVisibility(uint sequence, bool visible) => BuildUInt32(sequence, SetHooksVisibilityOpcode, visible ? 1u : 0u); public static byte[] BuildModifyAllegianceGuestPermission( uint sequence, bool enabled) => BuildUInt32( sequence, ModifyAllegianceGuestPermissionOpcode, enabled ? 1u : 0u); public static byte[] BuildModifyAllegianceStoragePermission( uint sequence, bool enabled) => BuildUInt32( sequence, ModifyAllegianceStoragePermissionOpcode, enabled ? 1u : 0u); // Queries the local player's house info (owned house data, or a // no-house status) — GameActionHouseQuery.Handle: no payload read. public static byte[] BuildHouseQuery(uint sequence) => BuildParameterless(sequence, HouseQueryOpcode); 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[] BuildStringUInt32( uint sequence, uint opcode, string value, uint number) { byte[] packed = PackString16L(value); byte[] body = CreateBody(sequence, opcode, packed.Length + 4); packed.CopyTo(body, 12); BinaryPrimitives.WriteUInt32LittleEndian( body.AsSpan(12 + packed.Length), number); return body; } private static byte[] BuildUInt32String( uint sequence, uint opcode, uint number, string value) { byte[] packed = PackString16L(value); byte[] body = CreateBody(sequence, opcode, 4 + packed.Length); BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), number); packed.CopyTo(body, 16); return body; } private static byte[] BuildTwoStrings( uint sequence, uint opcode, string first, string second) { byte[] packedFirst = PackString16L(first); byte[] packedSecond = PackString16L(second); byte[] body = CreateBody( sequence, opcode, packedFirst.Length + packedSecond.Length); packedFirst.CopyTo(body, 12); packedSecond.CopyTo(body, 12 + packedFirst.Length); 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; } }