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>
383 lines
13 KiB
Markdown
383 lines
13 KiB
Markdown
# 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.
|