acdream/docs/research/2026-07-13-retail-client-command-families-pseudocode.md
Erik 43f7c7807c fix #213: complete suicide and framerate presentation
Translate suicide response 0x004A as retail's successful self-kill notice. Port /framerate onto the authored SmartBox FPS element with live two-decimal FPS and DEG values, keep diagnostic window chrome independent, and synchronize command-driven settings without discarding panel drafts.

Co-Authored-By: Codex <codex@openai.com>
2026-07-13 15:50:47 +02:00

13 KiB
Raw Blame History

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.

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)
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

marketplace(arguments):
    if no arguments: send marketplace action
    else: display local usage
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.

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.

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.

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
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}"
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
clear(arguments):
    source = current_chat_source
    if first argument equals "all", ignoring case: source = all_sources
    clear(source)
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
friends(arguments):
    no arguments: display all friends
    "online": display only online friends
    "add <name>": add_friend(name)
    "remove <name>": 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.

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
afk(arguments):
    no arguments or "on": send opcode 0x000F with true, unless already away
    "off": send opcode 0x000F with false, unless already present
    "msg <text>": trim/join text, enforce retail's 191-character limit,
                  send opcode 0x0010 with String16L text, and echo the new text
    otherwise: display AFK usage
consent(arguments):
    "on" / "off": toggle the local accept-loot-permits option and echo it
    "who": send opcode 0x0217
    "clear": send opcode 0x0216
    "remove <name>": 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.

parse_squelch(arguments):
    default category = All
    consume options before the target:
        -reply uses the most recent teller
        -account chooses account-wide storage
        -<message type> 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 -<message type> 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
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
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.