Expand the typed client-command boundary across travel, character queries, local UI and layout controls, AFK and consent, emotes, friends, squelch and filters, and fill-components. Preserve retail packet layouts and queue ownership, import the confirmation dialog, and keep authoritative social state in Core. Co-Authored-By: Codex <codex@openai.com>
343 lines
12 KiB
Markdown
343 lines
12 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`
|
|
- `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 UI so the display changes immediately
|
|
|
|
lockui(arguments):
|
|
if arguments: display local usage
|
|
else:
|
|
ui_locked = !ui_locked
|
|
broadcast the UI-lock state change
|
|
```
|
|
|
|
```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.
|