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

383 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 <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.
## 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 <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
```
```text
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.
```text
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`
```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.