# Retail client command families Date: 2026-07-13 ## Scope This note extends the first lifestone vertical slice with the exact command families approved for porting. The named September 2013 retail client is the behavioral oracle; ACClientLib is used as an independent wire-name and payload cross-check. ## Sources - `ClientCommunicationSystem::DoMarketplace @ 0x0056FCE0` - `ClientCommunicationSystem::DoLockUI @ 0x005703B0` - `ClientCommunicationSystem::DoFrameRate @ 0x005707D0` - `gmSmartBoxUI::UpdateFPSMeter @ 0x004D63A0` - `gmSmartBoxUI::RecvNotice_SetFramerateDisplay @ 0x004D65E0` - `gmSmartBoxUI::UseTime @ 0x004D6E30` - `ClientCommunicationSystem::DoDie @ 0x00580050` - `ClientCommunicationSystem::DieDialogCallback @ 0x0057BA70` - `CM_Character::Event_Suicide @ 0x006A1B00` - `ClientCommunicationSystem::DoPKArena @ 0x005788D0` - `ClientCommunicationSystem::DoPKLArena @ 0x005789D0` - `ClientCommunicationSystem::DoAge @ 0x0057C5A0` - `ClientCommunicationSystem::DoBirth @ 0x0056E5F0` - `CM_Character::Event_QueryAge @ 0x006A1620` - `CM_Character::Event_QueryBirth @ 0x006A16F0` - `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` - `CM_Character::DispatchUI_QueryAgeResponse @ 0x006A2E40` - `ClientCommunicationSystem::Handle_Character__QueryAgeResponse @ 0x005711D0` - retail command-table construction at `0x00581900..0x00585100` Independent protocol cross-check: - `references/acclientlib/UtilityBelt.Common/MessageTypes.cs` - `references/acclientlib/UtilityBelt.Common/Enums/Enums.cs` - `references/acclientlib/UtilityBelt.Scripting/Enums/BusyAction.cs` - ACE `Source/ACE.Server/Network/GameAction/Actions/` handlers for `SetAFKMode`, `ModifyCharacterSquelch`, `QueryAge`, `ConfirmationResponse`, `SetDesiredComponentLevel`, `AddFriend`, and the teleport families - ACE `GameEventConfirmationRequest`, `GameEventFriendsListUpdate`, and `Network/Structure/SquelchDB`/`SquelchInfo` writers ACE's self age/birth handlers describe the trailing four zero bytes as an empty aligned `String16L`; named retail describes the same self-command bytes as object id zero. The wire bytes are identical for this path, and the named retail interpretation remains authoritative. ## Packet pseudocode All actions use the normal `0xF7B1` game-action envelope and the next UI sequence number. ```text send_parameterless_action(opcode): packet = [0xF7B1, next_sequence(), opcode] send packet to the weenie server marketplace(): send_parameterless_action(0x028D) pk_arena(): send_parameterless_action(0x0027) pkl_arena(): send_parameterless_action(0x0026) house_recall(): send_parameterless_action(0x0262) mansion_recall(): send_parameterless_action(0x0278) ``` ```text send_character_query(opcode, object_id): packet = [0xF7B1, next_sequence(), opcode, object_id] send packet to the weenie server query_age(): send_character_query(0x01C2, selected_player_or_zero) query_birth(): send_character_query(0x01C4, selected_player_or_zero) ``` For the ordinary self-command, `selected_player_or_zero` is zero. Retail's privileged PSR path may use the currently selected player; acdream does not claim that privileged mode. ## Command pseudocode ```text marketplace(arguments): if no arguments: send marketplace action else: display local usage ``` ```text pk_arena(arguments): if arguments: display local usage else if local player is not full PK: display retail failure 0x055F else: send PK-arena action pkl_arena(arguments): if arguments: display local usage else if local player is not PKLite: display retail failure 0x0560 else: send PKLite-arena action ``` Retail derives both eligibility checks from the local player's `PublicWeenieDesc` bits (`IsPK` / `IsPKLite`), not from a guessed server capability. ```text framerate(arguments): if arguments: display local usage else: show_framerate = !show_framerate notify gmSmartBoxUI if enabled: resolve UIElement_Text 0x10000047 from LayoutDesc 0x2100000F show it and update it immediately else: hide it and clear the active FPS-display pointer smartbox_use_time(): if the FPS-display pointer is active: update the display with two-precision values: "FPS: {Render.GetFramerate():F2}" "DEG: {active_degrade_multiplier:F2}" lockui(arguments): if arguments: display local usage else: ui_locked = !ui_locked broadcast the UI-lock state change ``` The two-line template is portal string-table entry `0x0DCFFF73` in table `0x23000001`; its authored text fragments are `FPS: ` and `\nDEG: `. The display is the exact 128×36 SmartBox text element at `(1,1)`, using font `0x4000001A`. Retail registers only the exact command name `/framerate`; `/framrate` is not an alias. ```text die(arguments): if arguments: display local usage else: show the authored local confirmation dialog if accepted: send parameterless game action 0x0279 suicide_response(weenie_error): if code == 0x004A: display "Ack! You killed yourself!" ``` `0x004A` is an informational success response, despite arriving through the WeenieError message family. Retail's own error-string table contains that exact text; exposing the numeric fallback makes a successful `/die` look like a failure. ```text query_age_response(name, duration): if name is empty: display "You have played for {duration}." else: display "{name} has played for {duration}." ``` The age-response payload is two aligned CP-1252 `String16L` values: target name (empty for self), then the already-formatted duration. Birth has no dedicated response event in the retail registry; the server supplies its result through the existing communication/system-text path. ## Location and corpse pseudocode Sources: - `ClientCommunicationSystem::DoLoc @ 0x0057A250` - `Position::ToString @ 0x005A9330` - `ClientCommunicationSystem::DoCorpse @ 0x00578220` - `LandDefs::CellidToCoordinateString @ 0x005A9CB0` ```text loc(arguments): if arguments: display local usage else if player position has no cell: display "Not in valid cell!" else: position_text = format( "0x%08X [%f %f %f] %f %f %f %f", cell, x, y, z, quaternion_w, quaternion_x, quaternion_y, quaternion_z) display "Your location is: {position_text}" ``` ```text corpse(arguments): // Retail ignores arguments for this command. last_outside = player quality PositionType.LastOutsideDeath (0x0E) if absent: display "We're sorry, but we have no record of your last outside corpse location." else: coordinates = cell_id_to_coordinate_string(last_outside.cell) display "The last time you died outside, your corpse was located at ({coordinates})." ``` The corpse command reads the position already carried by PlayerDescription; it does not infer the corpse location from a nearby corpse object or the most recent death chat line. ## Chat-local and UI-layout pseudocode Sources: - `ClientCommunicationSystem::DoClear @ 0x0056E600` - `ClientCommunicationSystem::DoSaveUI @ 0x0056FFF0` - `ClientCommunicationSystem::DoLoadUI @ 0x00570150` - `ClientCommunicationSystem::DoSaveAutoUI @ 0x005702B0` - `ClientCommunicationSystem::DoLoadAutoUI @ 0x00570330` ```text clear(arguments): source = current_chat_source if first argument equals "all", ignoring case: source = all_sources clear(source) ``` ```text save_ui(arguments): reject more than one argument with the retail usage message name = first argument, or the empty/default profile reject names longer than 16 characters save every attached window's current placement and visibility to name load_ui(arguments): apply the same argument rules as save_ui restore every attached window from name save_auto_ui(arguments): reject arguments save every attached window under the current character and resolution load_auto_ui(arguments): reject arguments restore every attached window for the current character and resolution ``` The original serializes `UIElement_Position` records. acdream stores the same observable placement/visibility values in its typed layout store; the storage format is deliberately modern and remains tracked as divergence IA-15. ## Friends pseudocode Sources: - `ClientCommunicationSystem::DoFriends @ 0x0057BC00` - `ClientCommunicationSystem::DoFriendsAdd @ 0x00578FB0` - `ClientCommunicationSystem::DoFriendsRemove @ 0x00579080` - `gmFriendsUI::Request_AddFriend @ 0x0048D240` - `CM_Login::AddFriend @ 0x006A3020` - `CM_Login::RemoveFriend @ 0x006A30A0` - `CM_Login::ClearFriends @ 0x006A3110` ```text friends(arguments): no arguments: display all friends "online": display only online friends "add ": add_friend(name) "remove ": remove_friend(name) "old": request the legacy server friends listing otherwise: display "Invalid friends command specified." add_friend(name): require a non-empty name if local list already has 50 entries: display WeenieError 0x0561 else send opcode 0x0018 followed by String16L name remove_friend(name): require a non-empty name if name equals "-all": send opcode 0x0025 and clear the local list else if a case-insensitive local match exists: send opcode 0x0017 followed by the friend's object id else display WeenieError 0x0563 ``` The server's `FriendsListUpdate` event is authoritative. Full updates replace the list; add/remove/login-state updates mutate it by object id. Display order is the received retail list order. ## Away, consent, and message-filter pseudocode Sources: - `ClientCommunicationSystem::DoAFK @ 0x0057B3F0` - `ClientCommunicationSystem::DoConsent @ 0x0057DDA0` - `ClientCommunicationSystem::ProcessSquelchArgs @ 0x00579C30` - `ClientCommunicationSystem::DoSquelch @ 0x0057BF50` - `ClientCommunicationSystem::DoUnSquelch @ 0x0057C070` - `ClientCommunicationSystem::DoFilter @ 0x0057C860` - `ClientCommunicationSystem::DoUnFilter @ 0x0057C880` - `ClientCommunicationSystem::PerformGlobalSquelchMod @ 0x0057C2D0` - `LogTextTypeEnumMapper::IsLegalChannel @ 0x006AFF40` ```text afk(arguments): no arguments or "on": send opcode 0x000F with true, unless already away "off": send opcode 0x000F with false, unless already present "msg ": trim/join text, enforce retail's 191-character limit, send opcode 0x0010 with String16L text, and echo the new text otherwise: display AFK usage ``` ```text consent(arguments): "on" / "off": toggle the local accept-loot-permits option and echo it "who": send opcode 0x0217 "clear": send opcode 0x0216 "remove ": send opcode 0x0218 followed by String16L name otherwise: display the retail invalid-command text ``` The option is CharacterOption bit `0x00080000`; retail's default mask `0x50C4A54A` leaves it clear. ```text parse_squelch(arguments): default category = All consume options before the target: -reply uses the most recent teller -account chooses account-wide storage - chooses a legal LogTextType remaining joined text is the target name squelch / unsquelch: with no arguments: display the authoritative local squelch database account target: send opcode 0x0059, add/remove flag, String16L name character target: send opcode 0x0058, add/remove flag, zero guid, String16L name, category filter / unfilter: with no arguments: display global filters require exactly a legal - option send opcode 0x005B, add/remove flag, category ``` Legal retail filter types are Speech, Tell, Combat, Magic, Emote, Appraisal, Spellcasting, Allegiance, Fellowship, Combat_Enemy, Combat_Self, Recall, Craft, and Salvaging. The inbound `SetSquelchDB` packet replaces the local database; command output reads that state rather than inventing client-only state. ## Emote and spell-component pseudocode Sources: - `ClientCommunicationSystem::DoEmote @ 0x00578AD0` - `CM_Communication::Event_Emote @ 0x006A4120` - `ClientCommunicationSystem::DoFillComponents @ 0x0056FD50` - `gmVendorUI::FillComponentList @ 0x004C4540` - `MagicEnumMapper::SpellComponentCategoryFromString @ 0x0056E290` ```text emote(arguments): text = join all arguments if text is non-empty: send opcode 0x01DF followed by String16L text emotes(arguments): if arguments: display usage else: display the retail standard-emote help list ``` ```text fillcomps(arguments): reject more than two arguments if first argument is "clear": send opcode 0x0224, component id 0, amount 0xFFFFFFFF clear desired component levels and echo "Component list cleared." return parse optional component category and optional positive maximum price if no vendor is open: display "You need an open vendor." else for each desired component in the chosen category: needed = desired level - currently held count find the component in vendor stock and add min(needed, stock) to buy list stop before exceeding the maximum price and report the limit ``` Accepted category spellings are Scarab(s), Herb(s), PowderedGem(s) or Powder(s), AlchemicalSubstance(s) or Potion(s), Talisman(s), Taper(s), and Pea(s). Component buying remains an operation on the vendor controller; the command parser does not duplicate vendor inventory or price logic.