acdream/docs/research/2026-08-09-chat-retail-command-registry.md
Erik e306c979ae docs: Campaign CH R1/R2/R4 research + ledger correction (CH1 = 172c6f9a)
Commits the command-registry, interface-text (SpewBox), and
side-channels-vs-ACE research docs (R3 color-table landed with CH1).
Corrects the CH1 ledger SHA the implementer recorded pre-amend.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 15:25:29 +02:00

399 lines
29 KiB
Markdown

# Retail client slash-command registry — complete enumeration + acdream audit
Date: 2026-08-09
Status: RESEARCH ONLY. No production code was changed.
Oracle: `docs/research/named-retail/acclient_2013_pseudo_c.txt` (Sept 2013 EoR
build) plus byte-level decode of the PDB-paired binary
`C:\Users\erikn\Downloads\acclient.exe` (v11.4186, CodeView GUID
`9e847e2f-777c-4bd9-886c-22256bb87f32`). Every verb string in this document was
read out of `.rdata` at the exact `push imm32` operand feeding
`PStringBase<char>::PStringBase<char>` inside the registration loop — none were
inferred from Binary Ninja's symbolic rendering, which mislabels several of the
pooled single-character strings as wide-string slices
(`&*U"fvpca"[4]`) and two as vtable fields.
Extends `docs/research/2026-07-13-retail-client-command-families-pseudocode.md`,
which already proved out the families acdream ported (lifestone/marketplace/
arena/age/birth/framerate/lockui/die/loc/corpse/clear/UI-layout/friends/afk/
consent/squelch/filter/emote/fillcomps). Those pseudocode blocks are not
repeated here.
---
## 1. Registry mechanism
There is **one** command table and **two** functions that populate it:
`ClientCommunicationSystem::m_hashCommands`, an
`IntrusiveHashTable<CaseInsensitiveStringBase<PStringBase<char>>, CmdHashData*>`
sized to 100 buckets in the ctor at `0x0058555D`.
`ClientCommunicationSystem::InitializeCommands @ 0x00581970` (spanning to
`0x00585520`) registers **116** entries; `CmdHashData::CmdHashData @ 0x0056ED30`
stores `+0x00` verb, `+0x08` `func` (`uint8_t (*)(this, int argc, char** argv)`),
`+0x0C` unused-in-practice, `+0x10` `help`
(`uint8_t (*)(this, HelpType, char const*, PStringBase<char>*)`), `+0x14` unused.
`ClientCommunicationSystem::StartupTurbineChatSystem @ 0x0057EFB0` runs only when
the server enables Turbine Chat; it **removes** the `a` entry and adds 15
entries (`a`, `guild`, `gu`, `general`, `cg`, `trade`, `ct`, `lfg`, `clfg`,
`roleplay`, `crp`, `society`, `soc`, `olthoi`, `o`), i.e. 14 net-new verbs.
Nothing else ever calls `IntrusiveHashTable<…CmdHashData*>::add` — verified by
grepping every call site (18 in `0x0057xxxx`, all inside
`StartupTurbineChatSystem` plus the two hash-table helpers themselves; 116 in
`0x0058xxxx`, all inside `InitializeCommands`).
Dispatch is `ClientCommunicationSystem::OnChatCommand @ 0x00581320`
`ClientCommunicationSystem::DoCommand @ 0x0057E2E0`. `OnChatCommand` switches on
`firstChar - 0x2F`: `'/'` (case 0) is **rewritten in place to `'@'`** and falls
into `DoCommand`; `'@'` (case 0x11) enters `DoCommand` directly; `':'` and `';'`
(cases 0x0B/0x0C) have their first character replaced with a space and the whole
line prefixed with the literal `"@emote"` before `DoCommand` — that is retail's
emote shorthand. Anything else routes to `PublicChat` / talk-focus.
`DoCommand` splits the line on `" \t"` (`PSUtils::FindAllWords`, delimiter set at
`0x007E0F5C`), takes `words[0].substring(1)` as the verb, **right-trims `','`**
from it (trim char set `0x0079452C`, `trim(left=0, right=1)`) so `@f, hello`
works, then looks the verb up. If the entry is absent **or its `func` is NULL**,
it calls `ClientCommunicationSystem::DoChannelCommand @ 0x005774A0`, which tries
`ChannelSystem::GetChannelID @ 0x005CF1F0` on the verb and, on a hit, sends
`CM_Communication::Event_ChannelBroadcast(id, joinedArgs)`. Only if *that* also
fails does the client fall back to `CM_Communication::Event_Talk` with the
**original `@`-prefixed line** — which is exactly the passthrough ACE relies on
(`references/ACE/Source/ACE.Server/Network/GameAction/Actions/GameActionTalk.cs:21`,
`if (message.StartsWith("@"))``CommandManager`). A registered handler that
returns 0 raises `HandleFailureEvent(0x26, "")`.
---
## 2. Complete retail command table
One row per handler. `A` in the Turbine column = the row exists only after
`StartupTurbineChatSystem`. acdream paths are relative to the repo root.
### 2.1 Help / group nodes
| Verb | Aliases | Handler @addr | Args | Behavior | acdream status |
|---|---|---|---|---|---|
| `help` | `?` | `DoHelp @ 0x0057F9E0` | `[command\|group]` | No args: prints the group index + "Note: You may substitute a forward slash (/) for the at symbol (@)." With an arg: looks the verb up and calls its `help` fn. | **PARTIAL**`ChatCommandRouter.TryHandleLocalPresentationCommand` (`src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs:96`) prints one flat blob for `/help`, `/?`, `/h`. No `@help <command>`, no group topics. `/h` is an acdream invention (not a retail verb). |
| `commands` | — | *(NULL func)*, help `HelpAllGroup @ 0x0057E7F0` | — | Help-topic node only. Typing it alone falls through to channel lookup then server. | MISSING |
| `allegiances` | — | *(NULL func)*, `HelpAllegiancesGroup @ 0x0057D4C0` | — | Help topic. | MISSING |
| `channels` | — | *(NULL func)*, `HelpChannelsGroup @ 0x005773E0` | — | Help topic. | MISSING |
| `chatting` | — | *(NULL func)*, `HelpChattingGroup @ 0x0057B1A0` | — | Help topic. | MISSING |
| `death` | — | *(NULL func)*, `HelpDeathGroup @ 0x0057B8B0` | — | Help topic. | MISSING |
| `status` | — | *(NULL func)*, `HelpStatusGroup @ 0x0057C410` | — | Help topic. | MISSING |
| `text` | — | *(NULL func)*, `HelpTextGroup @ 0x0057C6C0` | — | Help topic. | MISSING |
### 2.2 Local chat routing
| Verb | Aliases | Handler @addr | Args | Behavior | acdream status |
|---|---|---|---|---|---|
| `say` | `s` | `DoSay @ 0x00581240` | `<text>` | Joins + trims args, `PublicChat`. Empty → "You must specify the text you wish to say." | IMPLEMENTED-EQUIVALENT — `ChatInputParser` `SayAliases = {"/say","/s"}` (`ChatInputParser.cs:41`). Not a catalog command; routed as `SendChatCmd(Say)`. |
| `tell` | `t`, `send`, `whisper`, `w` | `DoTell @ 0x00577E40` | `<name>, <text>` | Splits on the **first comma**; name = left, text = right. Sets `SetLastTelleeName`, sends `CM_Communication::Event_TalkDirectByName`. Retail help is explicit: "you must put a comma after the character's name." | **PARTIAL**`ChatInputParser.TellAliases = {"/tell","/t"}` only; `send`/`whisper`/`w` MISSING. acdream splits on **whitespace first** and merely trims a trailing comma off the target (`ChatInputParser.cs:208-220`), so a multi-word name (`/t Lord Gnarly, hi`) resolves the wrong target where retail resolves it correctly. |
| `reply` | `r`, `rp` | `DoReply @ 0x00577910` | `<text>` | Sends `Event_TalkDirect` to `gmCCommunicationSystem::GetLastTeller()` (a **guid**, not a name). No teller → "Someone must @tell you first!" | **PARTIAL/DIVERGENT**`ReplyAliases = {"/reply","/r"}`; `rp` is MISSING **and is bound to Roleplay in acdream** (`ChatInputParser.cs:78`). acdream replies by *name*, retail by guid. |
| `mr` | — | **NULL func**, help `HelpReply @ 0x00577A50` | `<text>` | Documented as "reply to the last person who @m'd you (monarchs only)" but the 2013 build registers it with a **null function pointer** (verified at `0x00583041`: `xor edi,edi; xor ebp,ebp` before the ctor call — arg3 is 0). It therefore falls through to channel lookup (miss) and is sent to the server as text. A retail defect, not an acdream one. | MISSING (and **do not** implement client-side — retail does not) |
| `pr` | — | **NULL func**, help `HelpReply` | `<text>` | Same as `mr` (verified at `0x005830C1`). | MISSING (same note) |
| `retell` | `rt` | `DoReTell @ 0x00577BD0` | `<text>` | Re-sends to the last person **you** tell'd. | **PARTIAL**`RetellAliases = {"/retell"}`; `rt` MISSING. |
| `chat` | — | `DoChatToggle @ 0x0056FAD0` | `on\|off` | `on``Event_ModifyGlobalSquelch(remove, 2)`; `off``Event_ModifyGlobalSquelch(add, 2)` — i.e. it is a global **Speech** filter, not a separate flag. | MISSING (cheap: acdream already has `ModifyGlobalSquelch`) |
| `notell` | — | `DoNoTell @ 0x0056FBD0` | `on\|off` | Same mechanism with message type **3 (Tell)**. | MISSING (same) |
| `join` | — | `DoJoinChat @ 0x0056F510` | `<Allegiance\|General\|Trade\|LFG\|Roleplay\|Society\|Soc>` | Sets the matching `PlayerModule::SetHear*Chat(true)` Turbine-room flag. | MISSING |
| `leave` | — | `DoLeaveChat @ 0x0056F7F0` | same tags | Clears the matching flag. | MISSING |
| `index` | — | `DoChannelIndex @ 0x0056E640` | — | `CM_Communication::Event_ChannelIndex()`. ACE: `GameActionType.IndexChannels`. | MISSING |
| `clist` | — | `DoChannelList @ 0x0057A9B0` | `<channel>` | `GetChannelID` then `Event_ChannelList(id)`; bad tag → WeenieError `0x422`; no arg → "Please specify the channel name." ACE: `ListChannels`. | MISSING |
| `on` | — | `DoChannelOn @ 0x0057AA80` | `<channel>` | `Event_AddToChannel(id)`. ACE: `AddChannel`. | MISSING |
| `off` | — | `DoChannelOff @ 0x0057AB50` | `<channel>` | `Event_RemoveFromChannel(id)`. ACE: `RemoveChannel`. | MISSING |
| `title` | — | `DoTitle @ 0x0057A640` | `<text>` | Sets the popup chat window's title. Pure local UI. | MISSING |
| `log` | — | `DoSetOutput @ 0x0057E4F0` | `[filename]` | Toggles chat-to-file logging; appends `.txt`; "Copying chat to %s. Run command again with no arguments to turn off logging." | MISSING |
| `clear` | — | `DoClear @ 0x0056E600` | `[all]` | IMPLEMENTED | **IMPLEMENTED**`RetailClientCommandCatalog.cs:115`, `ClientCommandController.cs:165`. |
| `filter` / `unfilter` | — | `DoFilter @ 0x0057C860` / `DoUnFilter @ 0x0057C880` | `-<type>` | IMPLEMENTED | **IMPLEMENTED**`ClientCommandController.ExecuteGlobalFilter`. |
| `messagetypes` | `message_types`, `msgtypes`, `msg_types` | `DoMessageTypes @ 0x0057A010` | — | Lists squelch/filter categories. | **PARTIAL** — only `messagetypes` registered (`RetailClientCommandCatalog.cs:251`); the three underscore/short aliases are MISSING. |
| `loadfile` | — | `DoLoadFile @ 0x00581870` / `LoadFile @ 0x00581710` | `<file>` | `fopen` the file, then feed every line back through `OnChatCommand` after `MakeLoadFileVariableSubstitutions`. A script player. No arg → "You must provide a file name."; open failure → "Cannot open file %hs". | MISSING (see §3 risk note) |
### 2.3 Chat channels (`DoStupidChannelHack @ 0x0057B130` → `DoChannelCommand`)
All of these are one handler: no args → "You must specify the text you wish to
say."; otherwise `ChannelSystem::GetChannelID(verb)` then
`CM_Communication::Event_ChannelBroadcast(id, text)`.
| Channel | Registered verbs | Channel id | acdream status |
|---|---|---|---|
| Allegiance | `a`, `ab` (also `allegiance`/`all` **as a command**, see §2.4) | `0x02000000` | `/a` IMPLEMENTED-EQUIVALENT (`ChatInputParser.cs:63`). `ab` MISSING. **`/allegiance` is bound to the Allegiance channel in acdream but is a COMMAND in retail** — divergence. |
| Co-vassals | `co-vassals`, `covassals`, `covassal`, `c` | `0x01000000` | `covassals` OK; `c`, `covassal`, `co-vassals` MISSING. acdream's `/cv` is an invention. |
| Monarch | `monarch`, `m` | `0x4000` | IMPLEMENTED-EQUIVALENT |
| Patron | `patron`, `p` | `0x2000` | IMPLEMENTED-EQUIVALENT |
| Vassals | `vassals`, `vassal`, `v` | `0x1000` | `vassals`, `v` OK; `vassal` MISSING. |
| Fellowship | `fellowship`, `fellows`, `fellow`, `f`, `group`, `g`, `party` | `0x800` | **DIVERGENT** — retail's `g` and `group` and `party` are **Fellowship**. acdream maps `/g` to **General** (`ChatInputParser.cs:57`). `fellows`, `group`, `party` MISSING. |
`ChannelSystem::GetChannelID` also resolves 22 tags that are **not** in the
command table. Because `DoCommand` falls through to `DoChannelCommand` for any
unregistered verb, these still work as channel broadcasts:
| Tags | Channel id |
|---|---|
| `av`, `av1`, `advocate`, `advocate1` | `0x08` |
| `av2`, `advocate2` | `0x10` |
| `av3`, `advocate3` | `0x20` |
| `abuse` | `0x01` |
| `ad`, `admin` | `0x02` |
| `au`, `audit` | `0x04` |
| `sent`, `sentinel` | `0x200` |
| `celestialhand`, `celhan` | `0x08000000` |
| `eldrytchweb`, `eldweb` | `0x10000000` |
| `radiantblood`, `radblo` | `0x20000000` |
| `ol` (and `olthoi` pre-Turbine) | `0x40000000` |
| `help` | `0x400`**explicitly rejected** by `DoChannelCommand` at `0x005774FD` (`id == 0 || id == 0x400` → return 0) |
All 22 are SERVER-PASSTHROUGH in acdream today (they reach ACE as `@abuse …`
text rather than a channel broadcast).
### 2.4 Turbine Chat (registered only by `StartupTurbineChatSystem @ 0x0057EFB0`)
Each sends `ClientCommunicationSystem::SendTurbineChat @ 0x0057DB10` with the
matching `ChatTypeEnum` and the player's `PlayerModule::Hear*Chat` gate.
| Verb | Aliases | Handler @addr | acdream status |
|---|---|---|---|
| `a` | `guild`, `gu` | `DoTurbineChat_Allegiance @ 0x0057EBA0` | `/a` routed as a plain Allegiance channel message; `guild`/`gu` MISSING |
| `general` | `cg` | `DoTurbineChat_General @ 0x0057EC50` | `general` OK; `cg` MISSING. acdream's `/gen` is an invention. |
| `trade` | `ct` | `DoTurbineChat_Trade @ 0x0057ECE0` | `trade` OK; `ct` MISSING. `/tr` is an invention. |
| `lfg` | `clfg` | `DoTurbineChat_LFG @ 0x0057ED70` | `lfg` OK; `clfg` MISSING. `/lookingforgroup` is an invention. |
| `roleplay` | `crp` | `DoTurbineChat_Roleplay @ 0x0057EE00` | `roleplay` OK; `crp` MISSING. `/role` and `/rp` are inventions (`/rp` collides with retail's reply alias). |
| `society` | `soc` | `DoTurbineChat_Society @ 0x0057EF20` | `society` OK; `soc` MISSING |
| `olthoi` | `o` | `DoTurbineChat_Olthoi @ 0x0057EE90` | `olthoi` OK; `o` MISSING |
### 2.5 Allegiance management
| Verb | Aliases | Handler @addr | Args | Behavior | acdream status |
|---|---|---|---|---|---|
| `allegiance` | `all` | `DoAllegiance @ 0x0057D5A0` | `<sub> [args]` | Subcommand dispatcher. Verbs read out of the handler: `boot` (`DoAllegianceBoot @ 0x0057AEF0`), `info` (`DoAllegianceInfo @ 0x00576560`), `chat`/`ch` (`DoAllegianceChat @ 0x00575CB0`), `broadcast`/`br` (`DoAllegianceBroadcast @ 0x005761F0`), `ban` (`DoAllegianceBan @ 0x005762A0`), `officer` (`DoAllegianceOfficer @ 0x00576650`), `title` (`DoAllegianceOfficerTitle @ 0x00576A10`), `hometown`/`ho` (`DoAllegianceHometown @ 0x0056EF10`), `motd` (`DoMotd @ 0x00577150`), `name` (`DoAllegianceName @ 0x00576C80`), `lock` (`DoAllegianceLock @ 0x00576E70`), `house` (`DoAllegianceHouse @ 0x0056EF70`). | MISSING (and `/allegiance` currently mis-bound as a chat channel) |
| `ab` | — | `DoAllegianceBroadcast @ 0x005761F0` | `<text>` | Monarch broadcast to the whole allegiance. | MISSING |
| `alh` | `ah` | `DoAllegianceHometown @ 0x0056EF10` | — | ACE: `GameActionType.RecallAllegianceHometown`. | MISSING |
| `motd` | — | `DoMotd @ 0x00577150` | `[set <text>\|clear]` | Displays, sets (monarch-only), or clears the allegiance MOTD. | MISSING |
| `speaker` | — | `DoSpeaker @ 0x0057DAB0` | — | Prints exactly "This command is no longer in use, please see @allegiance officer.\n". Pure local. | MISSING (trivial) |
### 2.5b Housing
| Verb | Aliases | Handler @addr | Args | Behavior | acdream status |
|---|---|---|---|---|---|
| `house` | `hou` | `DoHouse @ 0x00580860` | `<sub> [args]` | Subcommand verbs read out of the handler: `open`, `close`, `recall`/`re`, `mansion_recall`/`alleg_recall`/`ma`, `storage` (`DoHouseStorage @ 0x00579560`), `remove`, `boot` (`DoHouseBoot @ 0x00579970`), `boot_all`, `remove_all`, `guest` (`DoHouseGuests @ 0x005791A0`), `abandon`, `available`, `hooks`, `on`, `off`. | **PARTIAL**`RetailClientCommandCatalog.TryMatch` (`RetailClientCommandCatalog.cs:279-301`) recognizes only `recall`, `mansion_recall`, `alleg_recall`. `hou`, `re`, `ma` and all 12 other subcommands are MISSING, **and the catalog swallows them locally** with an invalid-args message instead of letting them reach ACE. |
| `hor` | `hr` | `DoHouseRecall @ 0x00570450` | — | IMPLEMENTED | **IMPLEMENTED** |
| `hom` | `hoa` | `DoMansionRecall @ 0x005704B0` | — | IMPLEMENTED | **IMPLEMENTED** |
| `hslist` | — | `DoHouseAvailableList @ 0x00570510` | `<apartment\|cottage\|villa\|mansion>` | Lists available houses. ACE: `GameActionType.ListAvailableHouses`. | MISSING |
### 2.6 Death / recall / PK
| Verb | Aliases | Handler @addr | acdream status |
|---|---|---|---|
| `lifestone` | `lif`, `ls` | `DoLifestone @ 0x0056FC70` | **IMPLEMENTED** |
| `marketplace` | `mar`, `mp` | `DoMarketplace @ 0x0056FCE0` | **IMPLEMENTED** |
| `pkarena` | `pka` | `DoPKArena @ 0x005788D0` | **IMPLEMENTED** |
| `pklarena` | `pla` | `DoPKLArena @ 0x005789D0` | **IMPLEMENTED** |
| `pklite` | `pkl` | `DoPKLite @ 0x0057A490` | **PARTIAL**`pkl` alias MISSING (`RetailClientCommandCatalog.cs:218` registers only `pklite`) |
| `die` | — | `DoDie @ 0x00580050` | **IMPLEMENTED** |
| `corpse` | `cor` | `DoCorpse @ 0x00578220` | **IMPLEMENTED** |
| `consent` | — | `DoConsent @ 0x0057DDA0` | **IMPLEMENTED** |
| `permit` | — | `DoPermit @ 0x005785A0` | `add <name>` / `remove <name>` — grants/revokes corpse-loot permission. ACE: `AddPlayerPermission` / `RemovePlayerPermission`. **MISSING** |
### 2.7 Status / display
| Verb | Aliases | Handler @addr | Args | Behavior | acdream status |
|---|---|---|---|---|---|
| `age` | — | `DoAge @ 0x0057C5A0` | — | **IMPLEMENTED** |
| `birth` | — | `DoBirth @ 0x0056E5F0` | — | **IMPLEMENTED** |
| `day` | — | `DoDay @ 0x005706F0` | — | Toggles `LScape::m_fAlwaysDaylight` via `LScape::SetDay`, persists with `PlayerModule::SetPersistentAtDay`, echoes "Let there be light!" when enabling. Pure local. | MISSING |
| `endurance` | — | `DoEndurance @ 0x0057C5F0` | — | Prints a fixed paragraph beginning "The endurance attribute has a nu…". Pure local text. | MISSING (trivial) |
| `framerate` | — | `DoFrameRate @ 0x005707D0` | — | **IMPLEMENTED** |
| `loc` | — | `DoLoc @ 0x0057A250` | — | **IMPLEMENTED** |
| `version` | — | `DoVersion @ 0x0057E1B0` | — | Prints "Client version %s\n"; additionally prints "Using Turbine Chat.\n" when `IsUsingTurbineChat()`, and extra lines when `PlayerIsPSR()`. | **PARTIAL**`ClientCommandController.cs:136` prints the version line only. |
| `render` | — | `DoRenderOption @ 0x0057E120` | `[options]` | Forwards to `SmartBox::HandleRenderOption`, prints its two out-strings. Pure local dev/render toggle. | MISSING |
### 2.8 Interface layout, emotes, social, components
| Verb | Aliases | Handler @addr | acdream status |
|---|---|---|---|
| `saveui` / `loadui` / `saveautoui` / `loadautoui` / `lockui` | — | `0x0056FFF0` / `0x00570150` / `0x005702B0` / `0x00570330` / `0x005703B0` | **IMPLEMENTED** |
| `emote` | `e`, `em`, `me` | `DoEmote @ 0x00578AD0` | **IMPLEMENTED** |
| `emotes` | — | `DoEmoteList @ 0x0057BB30` | **IMPLEMENTED** |
| `afk` | — | `DoAFK @ 0x0057B3F0` | **IMPLEMENTED** |
| `friends` / `friends_add` / `friends_remove` | — | `0x0057BC00` / `0x00578FB0` / `0x00579080` | **IMPLEMENTED** |
| `squelch` / `unsquelch` | — | `0x0057BF50` / `0x0057C070` | **IMPLEMENTED** |
| `fillcomps` | — | `DoFillComponents @ 0x0056FD50` | **IMPLEMENTED** |
### 2.9 Prefixes (not verbs)
| Prefix | Site | Behavior | acdream status |
|---|---|---|---|
| `/` | `OnChatCommand @ 0x00581431` | Rewritten to `@`, then dispatched. | **IMPLEMENTED**`RetailClientCommandCatalog.TryMatch` accepts both (`RetailClientCommandCatalog.cs:267`). |
| `@` | `OnChatCommand @ 0x00581444` | Dispatched directly. | **IMPLEMENTED** |
| `:` , `;` | `OnChatCommand @ 0x0058144D` | First char replaced with a space, line prefixed with `"@emote"`, then dispatched — `:waves``@emote waves`. | **MISSING** |
| trailing `,` on the verb | `DoCommand @ 0x0057E3CD` | Right-trimmed, so `@f, hi``@f hi`. | **MISSING** — acdream's verb token keeps the comma, so `/f, hi` does not match any verb and is shipped to ACE as `@f,`. |
---
## 3. Prioritized missing-command list
Effort key: **L** = pure-local (no wire message); **W-have** = wire path already
exists in acdream; **W-new** = needs a new game action.
### Tier 1 — correctness bugs in what already ships (do these first)
1. **`/g` is bound to General; retail binds it to Fellowship** (`0x800`).
`ChatInputParser.cs:57`. One-line fix, but it silently sends fellowship
chatter to a global channel today. **L**
2. **`/rp` is bound to Roleplay; retail binds it to `reply`.** `ChatInputParser.cs:78`.
Same class of bug (a private reply becomes a global broadcast). **L**
3. **`/allegiance <text>` is bound to the Allegiance channel; retail's
`allegiance`/`all` is the allegiance *management command*.** Channel verbs are
`a`/`ab`/`guild`/`gu`. **L**
4. **`/house <anything-but-recall>` is swallowed locally.** `RetailClientCommandCatalog.cs:288-296`
returns a match with `HasValidArguments:false` for every unrecognized
subcommand, so `@house open`, `@house guest add X`, `@house abandon` never
reach ACE. Until `DoHouse` is ported, unrecognized `house` subcommands must
fall through to `SendServerCommandCmd`. **L**
5. **Verb-trailing-comma trim.** `@f, hi` / `@t Bob, hi` — retail right-trims `,`
off the verb in `DoCommand`. **L**
6. **`/tell` splits on the first comma, not the first space.** `ChatInputParser.cs:208`.
Multi-word names break today. **L**
### Tier 2 — pure-local commands, no wire work
7. `:` / `;` emote prefixes (`OnChatCommand` case 0x0B/0x0C). **L**
8. `@day` — daylight toggle + `SetPersistentAtDay`. **L**
9. `@endurance` — fixed help paragraph. **L**
10. `@speaker` — fixed deprecation line. **L**
11. `@title <text>` — chat window title. **L**
12. `@log [file]` — chat-to-file logging. **L**
13. `@version` — add the "Using Turbine Chat." line. **L**
14. `@render``SmartBox::HandleRenderOption` equivalent (acdream has no SmartBox;
map to the existing quality/debug toggles or leave as a documented divergence). **L**
15. `@help <command>` / `@help <group>` + the 7 group nodes (`commands`,
`allegiances`, `channels`, `chatting`, `death`, `status`, `text`). The exact
retail help strings for all 57 `Help*` functions are recoverable from the
binary — see §4 for the extraction recipe. **L**
16. **Missing aliases on already-implemented commands** (one dictionary edit
each): `pkl`, `hou`, `message_types`, `msgtypes`, `msg_types`, `rt`, `send`,
`whisper`, `w`, `vassal`, `covassal`, `co-vassals`, `c`, `fellows`, `group`,
`party`, `guild`, `gu`, `cg`, `ct`, `clfg`, `crp`, `soc`, `o`, `ab`.
Also delete the non-retail inventions `gen`, `cv`, `lookingforgroup`, `tr`,
`role`, `h`. **L**
### Tier 3 — wire messages acdream already has
17. `@chat on|off` and `@notell on|off` — both are `ModifyGlobalSquelch` with
message types 2 and 3. acdream already wires `ModifyGlobalSquelch` for
`/filter`. **W-have**
18. `@join` / `@leave <room>` — set `PlayerModule::SetHear*Chat`; these ride the
existing character-options path. **W-have**
### Tier 4 — new game actions (ACE-side handlers all exist)
19. `@permit add|remove <name>` — ACE `AddPlayerPermission` / `RemovePlayerPermission`. **W-new**
20. `@hslist <type>` — ACE `ListAvailableHouses`. **W-new**
21. `@index` / `@clist` / `@on` / `@off` — ACE `IndexChannels`, `ListChannels`,
`AddChannel`, `RemoveChannel`. **W-new** (four small parameterless/one-dword actions)
22. `@allegiance <sub>` — the 12-subcommand dispatcher. ACE handlers exist for
every one (`AllegianceInfoRequest`, `AllegianceChatBoot`, `AddAllegianceBan`,
`SetAllegianceOfficer`, `SetAllegianceOfficerTitle`, `RecallAllegianceHometown`,
`SetAllegianceName`, `DoAllegianceLockAction`, `DoAllegianceHouseAction`, …).
Largest single item; deserves its own slice. **W-new**
23. `@house <sub>` — 15 subcommands; ACE handlers exist (`SetOpenHouseStatus`,
`ChangeStoragePermission`, `BootSpecificHouseGuest`, `HouseBootAll`,
`AbandonHouse`, `HouseQuery`, `ModifyAllegianceGuestPermission`, …). **W-new**
24. `@motd [set|clear]` — allegiance MOTD. **W-new**
25. `@ab` — allegiance broadcast. **W-new**
26. `@alh` / `@ah``RecallAllegianceHometown`. **W-new**
27. Turbine-chat verbs as *Turbine* sends (`SendTurbineChat`) rather than plain
`ChannelBroadcast`. acdream already ships the 0xF7DE TurbineChat path, so
this is a routing decision, not new wire work. **W-have**
### Explicitly do NOT implement
- `@mr`, `@pr` — registered with a **null function pointer** in the 2013 build.
Retail ships them as help text only; the command itself falls through to the
server. Implementing them client-side would be a divergence.
- The 22 fallback-only channel tags (`abuse`, `admin`, `audit`, `advocate*`,
`sentinel`, `celhan`, `eldweb`, `radblo`, `ol`, …) are GM/faction channels.
They are reachable in retail only through `DoChannelCommand`'s fallback; the
cheapest faithful port is to add the `GetChannelID` table and let the existing
unknown-verb path consult it before falling back to `SendServerCommandCmd`.
- `@loadfile` — a client-side script player that re-enters `OnChatCommand` for
every line of an arbitrary file. Faithful, but it is a scripting surface;
acdream already has a designed plugin API for that, so porting `@loadfile`
should be an explicit product decision, not a parity checkbox.
---
## 4. Counts
| Measure | Count |
|---|---|
| Verbs registered by `InitializeCommands @ 0x00581970` | **116** |
| Verbs added by `StartupTurbineChatSystem @ 0x0057EFB0` | 15 (14 net-new; `a` is replaced) |
| **Total registered verbs** | **130** |
| …of which have a **null** `func` (help-only nodes) | 9 (`commands`, `allegiances`, `channels`, `chatting`, `death`, `status`, `text`, `mr`, `pr`) |
| Distinct handler functions behind those verbs | 68 (61 base + 7 Turbine) |
| Additional verbs reachable via the `GetChannelID` fallback (unregistered) | 22 |
| **Total client-parsed verbs** | **152** |
| Prefixes with special parsing | 4 (`/`, `@`, `:`, `;`) |
acdream, against the 130 registered verbs. Every verb is in exactly one row, so
the column sums to 130.
| Status | Verbs | Where |
|---|---|---|
| IMPLEMENTED — typed `ExecuteClientCommandCmd` | **45** (33 `ClientCommandId` values) | `RetailClientCommandCatalog.cs` + `ClientCommandController.cs` |
| PARTIAL via typed catalog | **1**`house` (3 of 15 subcommands, and it swallows the rest) | `RetailClientCommandCatalog.cs:279` |
| IMPLEMENTED-EQUIVALENT — chat alias → `SendChatCmd` | **21** | `ChatInputParser.cs` |
| PARTIAL via chat alias (argument shape wrong) | **3**`tell` (space-split, not comma-split), `reply` (by name, not guid), `retell` | `ChatInputParser.cs` |
| PARTIAL via local presentation | **2**`help`, `?` (flat blob; no per-command or group help) | `ChatCommandRouter.cs:96` |
| DIVERGENT — bound, wrong target | **3**`g`→General (retail Fellowship), `rp`→Roleplay (retail reply), `allegiance`→channel (retail command) | `ChatInputParser.cs` |
| **MISSING** | **55** | — |
| **Total registered** | **130** | |
Plus **22** unregistered `GetChannelID` fallback tags, all MISSING → **77**
client-parsed verbs unimplemented out of 152.
Reading note: the §2 tables mark `messagetypes`, `pklite` and `version` PARTIAL
because an *alias* or an *output line* is missing. The counts above are
per-verb, so those three verbs sit in IMPLEMENTED while their missing aliases
(`message_types`, `msgtypes`, `msg_types`, `pkl`) are counted as four separate
MISSING verbs.
Everything not in the table above reaches ACE correctly as
`SendServerCommandCmd` → Talk (`ChatCommandRouter.cs:62`), which matches retail's
own final fallback (`DoCommand``Event_Talk`) except that retail consults the
channel-tag table first.
acdream verbs with **no retail counterpart** (candidates for removal):
`/h`, `/gen`, `/cv`, `/lookingforgroup`, `/tr`, `/role`.
---
## 5. Reproduction recipes
Verb table (authoritative, resolves BN's mislabeled pooled strings):
```
# scan InitializeCommands for `push imm32` operands that point at short
# ASCII strings in .rdata — one per registered verb, in registration order
python pushes.py 581970 585520 # 116 verbs
python pushes.py 57efb0 57f9e0 # 15 Turbine verbs
```
Handler pairing: `sed -n '396604,399712p' acclient_2013_pseudo_c.txt` and read
the `CmdHashData::CmdHashData(ptr, &name, <func>, nullptr)` call (later blocks)
or the `*(esi + 8) = <func>` / `*(esi + 0x10) = <help>` stores (first ~22
blocks — BN renders the same code two different ways).
Exact retail help text for all 57 `Help*` functions: scan each
`ClientCommunicationSystem::Help*` symbol's byte extent for `push imm32` into
`.rdata` and read the C string. This recovered, verbatim, every usage line and
group listing quoted in §2 (e.g. the `@tell` comma requirement, the `@afk msg`
192-character limit, the `@hslist` house types).
`ChannelSystem::GetChannelID @ 0x005CF1F0`: alternate `push imm32` (tag) and
`mov eax, imm32` (channel id) down the function; the id immediately precedes the
next tag group.
Cross-checks performed: ACE `GameActionTalk.cs:21` (the `@`-passthrough
contract) and the `GameActionType` enumeration (confirms server-side handlers
exist for every Tier-4 item). holtburger's `commands.rs` was consulted and
**rejected as an oracle for aliases** — it binds `/g` to Allegiance and `/p` to
Fellowship, which matches neither retail nor acdream.