feat(chat): port retail client command families
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>
This commit is contained in:
parent
5a45a7ac7f
commit
9ea579bdd0
47 changed files with 6659 additions and 99 deletions
|
|
@ -0,0 +1,343 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue