fix(chat): round 4 — no user-visible meta text, real /help groups, indicator buttons toggle

Item 3 (#364): every honesty marker is now gone from user-visible /help
text. AllegianceOverview/HouseOverview's "[IMPLEMENTED]" tags and trailing
"Subcommands NOT marked..." sentences, and Day/Log/Render/Motd's appended
"NOT YET IMPLEMENTED in acdream" tails, are removed; the underlying retail
text is corrected/completed against the pseudo-C's own pristine
consolidated data dumps (Log and Motd had been silently truncated; Render
was entirely acdream-authored and is replaced with the real retail usage
string). The three PARTIAL /help group topics (channels/chatting/commands)
are now COMPLETE verbatim listings: HelpStupidChannelHack's three
"vtable slot" operands, previously believed undecodable, are the same
pooled/mislabeled-data artifact this campaign has hit before (AP-113's
precedent) — reading the function's own disassembly for the push imm32
preceding each constructor call resolves all three directly. messagetypes
is now a real ported construction (IsLegalChannel's 14-id whitelist +
LogTextTypeToString's name table + the exact join/wrap format) instead of
an acdream summary. Register row AP-184 retired.

Item 5: the main window's 1/2/3/4 indicator buttons now toggle their
floating chat window on click, per the user's retail memory overruling
the earlier decomp-only reading. UIElement_Button::HandleButtonClick has
its own generic click-driven action dispatch (property 0x12) reaching the
same DoVisibilityToggleAction the Alt+1..4 keybinds use; the button
fixture confirms this half is genuinely armed, but the floating-window
fixture authors no matching listener-registration property, so the
generic mechanism has no proven target in the data on hand. Per
CLAUDE.md, the user's retail memory is the axiom regardless:
ChatWindowController.BindIndicatorClicks wires each indicator's click
through the same ToggleFloatingChatWindow chokepoint the keybinds use,
as explicit user-directed retail behavior. SetIndicatorOpen stays the
sole writer of the Selected mirror so the visual stays consistent
through the click round trip.

Full reconciliation in docs/research/2026-08-09-chat-retail-window-shell.md
§1.4. Campaign plan gets the round-4 findings section; items 1+2
(text-style) are under parallel research, item 4 passed, item 6 deferred
to the settings track.

Suite: 12,579 passed / 4 skipped / 0 failed (Release, complete solution),
up from baseline 12,553/4/0 — net +26 tests, zero regressions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-10 18:46:34 +02:00
parent 8b166f3ea2
commit 5b54387b8e
10 changed files with 1129 additions and 239 deletions

View file

@ -1,4 +1,5 @@
using System.Collections.Frozen;
using AcDream.Core.Chat;
namespace AcDream.UI.Abstractions.Panels.Chat;
@ -53,20 +54,125 @@ namespace AcDream.UI.Abstractions.Panels.Chat;
/// caught that <c>/help death</c> printed an acdream META-MESSAGE ("This is
/// a retail help-topic group; acdream has not yet extracted its exact
/// retail listing text…") instead of retail's real listing — this was the
/// class-doc overclaim's SHARP end. All 7 <c>HelpXxxGroup</c> nodes are now
/// resolved: <see cref="DeathGroupDetail"/>, <see cref="StatusGroupDetail"/>,
/// <see cref="TextGroupDetail"/>, and <see cref="AllegiancesGroupDetail"/>
/// are COMPLETE verbatim listings; <see cref="ChannelsGroupDetail"/>,
/// <see cref="ChattingGroupDetail"/>, and <see cref="CommandsGroupDetail"/>
/// are PARTIAL (their own summary line, plus — for chatting — 7 of 8
/// entries) with an explicit UNVERIFIED note citing
/// <c>ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290</c> for
/// the un-decodable remainder, never a guess. 7 of the ~35 channel
/// one-liners (a/guild/gu, general/cg, trade/ct, lfg/clfg, roleplay/crp,
/// society/soc, olthoi/o) are now verbatim too; the rest (fellowship/
/// monarch/patron/vassals/covassal family) route through the same
/// unresolved mechanism and remain acdream summaries. See the remarks on
/// <see cref="ChannelsGroupSummary"/> for the full extraction method.
/// class-doc overclaim's SHARP end. All 7 <c>HelpXxxGroup</c> nodes were
/// resolved that round: <see cref="DeathGroupDetail"/>,
/// <see cref="StatusGroupDetail"/>, <see cref="TextGroupDetail"/>, and
/// <see cref="AllegiancesGroupDetail"/> were COMPLETE verbatim listings;
/// <see cref="ChannelsGroupDetail"/>, <see cref="ChattingGroupDetail"/>, and
/// <see cref="CommandsGroupDetail"/> were left PARTIAL with an UNVERIFIED
/// note. 7 of the ~35 channel one-liners (a/guild/gu, general/cg, trade/ct,
/// lfg/clfg, roleplay/crp, society/soc, olthoi/o) were verbatim too; the
/// rest (fellowship/monarch/patron/vassals/covassal family, as standalone
/// <c>/help f</c>-style lookups) still route through the unresolved
/// mechanism below and remain acdream summaries — that part of the gap is
/// unchanged this round; see <c>ByVerb</c> and
/// <c>RetailCommandHelpTableTests.ChannelVerb_StillAcdreamSummary_UnresolvedChannelHackFamily</c>
/// for the record of exactly which verbs. (Those verbs' OWN standalone help
/// registration was never confirmed independently of the group-listing
/// mechanism decoded below, so they are left untouched rather than
/// speculatively upgraded.)
/// </para>
///
/// <para>
/// <b>Round 4 (2026-08-10): the 3 PARTIAL groups are now COMPLETE, and the
/// meta-notice text itself is GONE from every user-visible string.</b> The
/// blocker was never really <c>HelpStupidChannelHack @0x0056f290</c>'s own
/// code — the fragments Binary Ninja renders as vtable-slot dereferences
/// (<c>&amp;ClientCommunicationSystem::`vftable'.RecvNotice_StartBarberNotice</c>
/// etc.) are the SAME pooled/mislabeled-data artifact this campaign has hit
/// before (AP-186's own precedent): they are DATA pointers into
/// <c>.rdata</c>, not real vtable dispatch, and dereferencing their actual
/// operand addresses (found by reading <c>HelpStupidChannelHack</c>'s own
/// disassembly for the <c>push imm32</c> immediately preceding each
/// constructor call, since BN's line-grouped rendering hides the true
/// instruction order) resolves cleanly: the function builds
/// <c>"@" + tag + " - Sends a broadcast to your " + ChannelName + ".\n"</c>
/// where <c>tag</c> is a single character sliced out of a shared wide
/// literal <c>U"fvpca"</c> (reading a WIDE string through a NARROW
/// <c>char*</c> naturally truncates to one character at the first zero high
/// byte — the "hack" the function's own retail name calls out) and
/// <c>ChannelName</c> comes from <c>ChannelSystem::GetChannelName</c>'s own
/// literal switch table (also swept directly, not guessed:
/// <c>data_0x7d0478</c>="Allegiance", plus "Co-vassals"/"Monarch"/"Patron"/
/// "Vassals"/"Fellowship" already visible in that function's straight-line
/// decompiled body). See <see cref="AllegianceBroadcastLine"/> through
/// <see cref="FellowshipBroadcastLine"/> for the six resolved lines and
/// their per-line address citations.
///
/// <para>
/// With that decoded, <see cref="ChannelsGroupDetail"/> (which is ENTIRELY
/// six <c>HelpStupidChannelHack</c> calls, confirmed by reading
/// <c>HelpChannelsGroup</c>'s Detail branch directly) is now a COMPLETE
/// verbatim listing. <see cref="ChattingGroupDetail"/> needed one more
/// piece: its "@reply" entry does not call a per-tag summary — it calls
/// <c>ClientCommunicationSystem::HelpReply @0x00577A50</c> with
/// <c>Summary_HelpType</c>, and that function (read directly, not assumed)
/// unconditionally concatenates THREE lines regardless of which tag it was
/// passed (reply, then pr, then mr) — a genuine retail quirk, ported
/// faithfully per CLAUDE.md's "do not fix the decompiled code" rule; see
/// <see cref="ReplySummaryLine"/>/<see cref="PatronReplySummaryLine"/>/
/// <see cref="MonarchReplySummaryLine"/>. <see cref="CommandsGroupDetail"/>
/// (retail's <c>HelpAllGroup</c>, the "lists all commands" topic) is a
/// straight-line concatenation of EVERY other group's Detail branch plus a
/// handful of its own short one-liners (all swept directly from
/// <c>0x0057E7F0</c>-<c>0x0057EBA0</c>) — including a CONFIRMED retail
/// duplicate (the saveui/loadui short-summary pair appears twice with no
/// conditional between the two occurrences, ported as-is) and one line
/// (<c>@say</c>) whose own retail literal has NO trailing newline, unlike
/// every sibling line — also ported as-is; retail's own output runs
/// <c>@say</c> straight into <c>@tell</c> with no line break. Zero
/// remaining UNVERIFIED notes in any of the three former PARTIAL groups.
/// Closes ISSUES.md #364.
/// </para>
///
/// <para>
/// <b>The meta-marker sweep.</b> Every honesty marker that had leaked into
/// USER-VISIBLE text — the "[IMPLEMENTED]"/"[IMPLEMENTED, also @x]" tags
/// littered through <see cref="AllegianceOverview"/>/<see cref="HouseOverview"/>,
/// the "NOT YET IMPLEMENTED in acdream" sentences appended to
/// <see cref="Day"/>/<see cref="Log"/>/<see cref="Render"/>/<see cref="Motd"/>,
/// and the "Subcommands NOT marked [IMPLEMENTED]…" trailer on both overview
/// blocks — is now GONE from the strings themselves. Per CLAUDE.md, honesty
/// markers belong in code comments and the divergence register, never in
/// text a player can read. Every one of those consts is now checked against
/// the raw retail data dump the pseudo-C file itself carries for long
/// consolidated string tables (e.g. <c>char const data_7dfb58[0x879] =
/// "@allegiance boot …</c>) — those blocks are ALREADY fully decoded by
/// Binary Ninja with no BN artifact to work around, so they were read
/// directly rather than re-swept, and confirmed the "[IMPLEMENTED]" tags
/// were 100% acdream additions with no retail counterpart (not even a
/// stray character). <see cref="Log"/> and <see cref="Motd"/> also picked
/// up MORE retail text than they had before while their meta tails were
/// being removed — the prior extractions had silently dropped extra
/// sentences/paragraphs present in the same data block (<c>data_7df7f8</c>
/// and <c>data_7da300</c> respectively); <see cref="Render"/>'s previous
/// text was entirely acdream-authored ("Forwards to the client's
/// render-option surface…") and is replaced with the REAL retail usage
/// string (<c>render_option_usage</c> @ <c>0x0079B878</c>, found by
/// grepping the pseudo-C for a literal <c>@render</c> string instead of
/// trusting the earlier "not decodable" note).
/// </para>
///
/// <para>
/// <b><c>messagetypes</c> is now a real construction, not an acdream
/// summary.</b> Retail's <c>HelpMessageTypes @0x0056E5D0</c> really does
/// build its text at runtime — but by walking a FIXED whitelist
/// (<c>LogTextTypeEnumMapper::IsLegalChannel @0x006AFF40</c>, 14 ids) and a
/// FIXED per-id name table (<c>LogTextTypeEnumMapper::LogTextTypeToString
/// @0x006AFF90</c>, a literal switch), joined with <c>", "</c>
/// (<c>data_0x794334</c>) and wrapped in <c>"Squelch channels are as
/// follows:\n %s\n"</c> (<c>gmCCommunicationSystem::GetListofSquelchChannels
/// @0x00589EB0</c> /
/// <c>gmCCommunicationSystem::ConvertSQToPString @0x00589A80</c>'s
/// <c>arg3!=0</c> "enumerate every legal channel" branch — the
/// <c>SquelchInfo</c> it iterates is a SYNTHETIC all-squelched object built
/// solely to walk every legal id, not the player's actual squelch state).
/// Since the whitelist and the name table are both fixed, the output is
/// itself a compile-time constant — but <see cref="MessageTypesDetail"/>
/// PORTS the construction (the ordered id/name table +
/// <see cref="BuildMessageTypesDetail"/>'s exact join/prefix logic) rather
/// than hand-authoring the already-known joined string, so a future
/// whitelist change is a one-line table edit instead of a re-derivation.
/// </para>
///
/// <para>
@ -188,20 +294,65 @@ public static class RetailCommandHelpTable
public const string PatronReply =
"@pr <text> - Sends the text to the last vassal who used @p to send you a message.";
// acclient_2013_pseudo_c.txt:1031093 (data_7de280).
public const string Day =
"@day - A toggle that lightens the outdoor landscape. Note that this command may take several seconds to take effect. NOT YET IMPLEMENTED in acdream — no sky/time-of-day override hook exists yet; the command reaches the server as literal text.";
// acclient_2013_pseudo_c.txt:1031093 (char const data_7de280[0x77] =
// "@day - A…\n", 0) -- BN already fully decodes this consolidated data
// block, no vtable-slot artifact to work around; read directly rather
// than re-swept. Round 4 (2026-08-10): dropped the acdream
// "NOT YET IMPLEMENTED in acdream" tail -- honesty markers belong in
// comments/the register, never in text a player can read (CLAUDE.md).
// acdream's client-side "@day" implementation status, if any, is
// tracked in the divergence register, not here. Shared with
// StatusGroupDetail's own "@day" line -- same data block, same text.
public const string DayLine =
"@day - A toggle that lightens the outdoor landscape. Note that this command may take several seconds to take effect. \n";
// acclient_2013_pseudo_c.txt:1031192-1031198 (data_7df7f8/data_7dfac4).
public const string Day = DayLine;
// acclient_2013_pseudo_c.txt:1031192-1031198 (char const
// data_7df7f8[0x2ca] = "@log <name> - …\n" "@log AClog.txt - …\n" "@log
// - …\n", 0) -- read directly off the pseudo-C's own consolidated data
// dump. Round 4 (2026-08-10): the prior extraction had silently
// dropped the middle sentence ("Choose the file you are copying to by
// naming it in the command.") and BOTH trailing paragraphs (the
// AClog.txt example and the "stop the process" line) in addition to
// carrying the "NOT YET IMPLEMENTED" meta tail -- all three retail
// sentences are restored here, verbatim, with no acdream commentary.
public const string Log =
"@log <name> - Echoes chat text to a logfile. All the information that appears in your chat window after you type this command will be copied into a text file. If this file already exists, it will add the additional text to the end of it. To turn off logging, simply retype @log. NOT YET IMPLEMENTED in acdream — the command reaches the server as literal text.";
"@log <name> - Echoes chat text to a logfile. All the information that appears in your chat window after you type this command will be copied into a text file. Choose the file you are copying to by naming it in the command. If this file already exists, it will add the additional text to the end of it. To turn off logging, simply retype @log.\n"
+ "@log AClog.txt - Echoes chat text to a log file named Aclog.txt in your Asheron's Call directory. After you use this command, all the information that appears in your chat window will be written to a file in your Asheron's Call directory named Aclog.txt.\n"
+ "@log - If you are currently copying the text in your chat window to a logfile, this command will stop the process.\n";
// Round 4 (2026-08-10): the prior text here ("Forwards to the client's
// render-option surface (retail: SmartBox::HandleRenderOption). NOT YET
// IMPLEMENTED in acdream…") was entirely acdream-authored, not retail
// text -- the class remarks previously (incorrectly) called @render's
// option list "not decodable". A direct grep for a literal "@render"
// string in the pseudo-C finds it: `char const render_option_usage[0x98]
// @ 0x0079B878 = "Usage:\n@render <option> <value>\n radius # :
// set landscape radius (between 5 and 25)\n fov # : set
// field of view (between 10 and 160)\n"` -- the real retail usage text
// shown by SmartBox::HandleRenderOption for bad/missing arguments
// (acclient_2013_pseudo_c.txt:94759/94767), swept verbatim, no
// paraphrase.
public const string Render =
"@render [options] - Forwards to the client's render-option surface (retail: SmartBox::HandleRenderOption). NOT YET IMPLEMENTED in acdream — there is no SmartBox equivalent; the command reaches the server as literal text.";
"Usage:\n"
+ "@render <option> <value>\n"
+ " radius # : set landscape radius (between 5 and 25)\n"
+ " fov # : set field of view (between 10 and 160)\n";
// acclient_2013_pseudo_c.txt:1030670-1030676 (data_7da300/data_7da3e0).
// acclient_2013_pseudo_c.txt:1030670-1030676 (char const
// data_7da300[0xdb] = "@allegiance motd - …\n" "@allegiance motd set
// …\n" "@allegiance motd clear- …\n", 0) -- read directly off the
// pseudo-C's own consolidated data dump. Round 4 (2026-08-10): the
// prior extraction paraphrased this into a single acdream sentence and
// appended a "NOT YET IMPLEMENTED in acdream (TS-68)" meta tail; restored
// to retail's real 3-line shape, including the "clear-" (no space
// before the dash) typo confirmed present in the raw data -- ported as
// found per CLAUDE.md, not "fixed".
public const string Motd =
"@allegiance motd - Displays the message of the day for your allegiance. @allegiance motd set <text> - Sets the MOTD. Can only be used by monarchs. @allegiance motd clear - Clears the MOTD. Can only be used by monarchs. NOT YET IMPLEMENTED in acdream (TS-68) — the command reaches the server as literal text.";
"@allegiance motd - Displays the message of the day for your allegiance.\n"
+ "@allegiance motd set <text> - Sets the MOTD. Can only be used by monarchs.\n"
+ "@allegiance motd clear- Clears the MOTD. Can only be used by monarchs.\n";
// Split out of AllegianceOverview's own data block (data_0x7dfb58) at
// the consolidated review round (2026-08-10) so the const declaration
@ -218,18 +369,30 @@ public static class RetailCommandHelpTable
// extraction dropped — confirmed present in the SAME data block
// (data_0x7dfb58) this const already cites, not a new source. Restored
// verbatim, including the retail leading space.
//
// Round 4 (2026-08-10): every "[IMPLEMENTED]"/"[IMPLEMENTED, also
// @x]"/"[IMPLEMENTED as @ab]" tag AND the trailing "Subcommands NOT
// marked [IMPLEMENTED]…" sentence below were acdream additions with NO
// retail counterpart — confirmed by reading data_7dfb58's own raw
// consolidated dump (acclient_2013_pseudo_c.txt:1031210-1031230), which
// BN already fully decodes with no vtable-slot artifact to work around.
// The pristine dump's "broadcast" line ends "...Also: @ab\n" (no
// bracket) and the "hometown" line ends "...tied to one.\n" (no
// bracket, no alias mention at all) — both removed here. TS-68's
// implemented-vs-not-yet-implemented tracking lives in ISSUES.md and
// the divergence register now, not in this user-visible string.
public const string AllegianceOverview =
"@allegiance - Commands to help manage your allegiance.\n"
+ "@allegiance boot [-account] <name> - Removes a character from your allegiance.\n"
+ "@allegiance ban <add/remove> <name> - Bans all characters on the given character's account from your allegiance (and boots them too!)\n"
+ "@allegiance ban list - List the characters whose accounts are banned from your allegiance.\n"
+ AllegianceWarningLine
+ "@allegiance info <name> - Requests information on a member of your allegiance. [IMPLEMENTED]\n"
+ "@allegiance info <name> - Requests information on a member of your allegiance.\n"
+ "@allegiance chat <on/off> - Turn allegiance chat on and off.\n"
+ "@allegiance chat kick <name>[, <reason>] - Kick a player temporarily from the allegiance chat room.\n"
+ "@allegiance chat gag <name> - Gags a player so that they cannot see or speak in the allegiance chat room for 5 minutes.\n"
+ "@allegiance chat ungag <name> - Ungags a gagged allegiance member so that they may once again see and speak in the allegiance chat room.\n"
+ "@allegiance broadcast <message> - Broadcast a message to the entire allegiance. Limited to 10/day. Also: @ab [IMPLEMENTED as @ab]\n"
+ "@allegiance broadcast <message> - Broadcast a message to the entire allegiance. Limited to 10/day. Also: @ab\n"
+ "@allegiance officer <add/set> <level #> <name> - Assigns the position of officer, with the given level of permissions, to the named character.\n"
+ "@allegiance officer <remove> <name> - Removed the named character as an allegiance officer.\n"
+ "@allegiance officer clear - Clears all officer positions.\n"
@ -240,15 +403,35 @@ public static class RetailCommandHelpTable
+ "@allegiance name <set/clear> - Displays, sets, or clears the name of your allegiance.\n"
+ "@allegiance lock <on/off/toggle/check> - Locks, unlocks, or displays the locked state of your allegiance.\n"
+ "@allegiance lock bypass <clear/name> - Sets, clears, or displays a single character as an approved vassal. That character may then swear into a locked allegiance.\n"
+ "@allegiance hometown - Recalls you to your allegiance bindstone, if your allegiance has tied to one. [IMPLEMENTED, also @alh/@ah]\n"
+ "@allegiance motd - Displays or sets the message of the day for your allegiance.\n"
+ "Subcommands NOT marked [IMPLEMENTED] are not yet locally executed (TS-68) and reach the server as literal text.";
+ "@allegiance hometown - Recalls you to your allegiance bindstone, if your allegiance has tied to one.\n"
+ "@allegiance motd - Displays or sets the message of the day for your allegiance.\n";
// acclient_2013_pseudo_c.txt:1031041/1031045 (data_7dd908/data_7dd968)
// plus the full per-subcommand block at 1031017-1031037 (data_7dd3d0).
// acclient_2013_pseudo_c.txt:1031041 (char const data_7dd908[0x5c] =
// "@house - Commands that help you manage your house, including guest
// and storage management.\n", 0) -- the group-intro one-liner, shared
// verbatim below with CommandsGroupDetail's own "@house" fragment (same
// underlying retail string, per HelpAllGroup's own direct call to
// PStringBase::PStringBase with this exact literal).
public const string HouseOneLiner =
"@house - Commands that help you manage your house, including guest and storage management.\n";
// acclient_2013_pseudo_c.txt:1031017-1031037 (char const
// data_7dd3d0[0x534] = "@house abandon - …\n" … "@house available - See
// @hslist\n", 0) -- read directly off the pseudo-C's own consolidated
// data dump; BN already fully decodes this block with no vtable-slot
// artifact to work around. Round 4 (2026-08-10): every
// "[IMPLEMENTED]"/"[IMPLEMENTED, also @x]"/"[IMPLEMENTED, alias of
// mansion_recall]" tag AND the trailing "Subcommands NOT marked
// [IMPLEMENTED]…" sentence below were acdream additions with no retail
// counterpart. The "@house available" line's retail literal is exactly
// "@house available - See @hslist\n" — no trailing period, and none of
// the bracketed "[see @hslist, IMPLEMENTED]" text the old version
// appended. TS-68's implemented-vs-not-yet-implemented tracking lives
// in ISSUES.md and the divergence register now, not in this
// user-visible string.
public const string HouseOverview =
"@house - Commands that help you manage your house, including guest and storage management.\n"
+ "@house abandon - Abandons your house. [IMPLEMENTED]\n"
HouseOneLiner
+ "@house abandon - Abandons your house.\n"
+ "@house boot <name> - Removes a player from your house.\n"
+ "@house boot -all - Removes everyone from your house.\n"
+ "@house guest add <name> - Adds players to your house guest list.\n"
@ -257,7 +440,7 @@ public static class RetailCommandHelpTable
+ "@house guest remove_allegiance - Removes your allegiance from the guest list.\n"
+ "@house guest remove_all - Removes all guests from your house guest list.\n"
+ "@house guest list - Shows the current guest list.\n"
+ "@house recall - Teleports you to your house. [IMPLEMENTED, also @hor/@hr]\n"
+ "@house recall - Teleports you to your house.\n"
+ "@house storage add <name> - Gives a player permission to use your house storage.\n"
+ "@house storage remove <name> - Removes permission to use your house storage from a player.\n"
+ "@house storage add_allegiance - Grants storage permission to your allegiance.\n"
@ -266,10 +449,9 @@ public static class RetailCommandHelpTable
+ "@house open - Creates an open house.\n"
+ "@house close - Closes your house.\n"
+ "@house hooks on|off - Makes the hooks in your house visible or invisible.\n"
+ "@house mansion_recall - Teleports you to your allegiance mansion or villa. [IMPLEMENTED, also @hom/@hoa]\n"
+ "@house alleg_recall - Teleports you to your allegiance mansion or villa. [IMPLEMENTED, alias of mansion_recall]\n"
+ "@house available - See @hslist [see @hslist, IMPLEMENTED]\n"
+ "Subcommands NOT marked [IMPLEMENTED] are not yet locally executed (TS-68) and reach the server as literal text.";
+ "@house mansion_recall - Teleports you to your allegiance mansion or villa.\n"
+ "@house alleg_recall - Teleports you to your allegiance mansion or villa.\n"
+ "@house available - See @hslist\n";
// The 7 retail-registered "group index" nodes — retail registers them
// with a NULL func (like @mr/@pr); typing one bare reaches the server.
@ -288,23 +470,17 @@ public static class RetailCommandHelpTable
// group's own one-line summary> }`; the exact byte extent (this
// function's start VA to the next function's start VA) was read from
// the pseudo-C's own function-header addresses, and every extent was
// swept with --ascii-only. Four groups decode COMPLETELY (every
// subcommand summary is either an inline literal in the group function
// itself or a direct `HelpXxx(this, Summary_HelpType, "@tag", arg4)`
// call whose OWN summary text was independently swept from HelpXxx's
// own extent): death, status, text, allegiances. Three groups
// (commands, channels, chatting) delegate part or all of their detail
// text to ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290,
// which builds its output from THREE string fragments Binary Ninja
// mis-attributes to unrelated vtable slots
// (`ClientCommunicationSystem::\`vftable'.RecvNotice_StartBarberNotice`
// etc. — the classic BN pooled/mislabeled-data artifact, not real
// vtable dispatch) concatenated around a live channel-name lookup
// (`ChannelSystem::GetChannelName`) — genuinely not decodable via this
// sweep with confidence. Those three keep an honest UNVERIFIED fallback
// (still real retail text where available — each group's own one-line
// summary IS fully decoded and included) rather than inventing the
// missing portion. Tracked as ISSUES.md #364.
// swept with --ascii-only. Four groups decoded COMPLETELY that round
// (every subcommand summary is either an inline literal in the group
// function itself or a direct `HelpXxx(this, Summary_HelpType, "@tag",
// arg4)` call whose OWN summary text was independently swept from
// HelpXxx's own extent): death, status, text, allegiances.
//
// Round 4 (2026-08-10): the remaining three (commands, channels,
// chatting) are ALSO now COMPLETE — see the class remarks' "3 PARTIAL
// groups are now COMPLETE" paragraph for the full HelpStupidChannelHack
// decode. Nothing in this file emits an UNVERIFIED note for any of the
// 7 groups any more.
private const string ChannelsGroupSummary =
"@help channels - How to communicate with people in your allegiance or fellowship.";
@ -318,9 +494,6 @@ public static class RetailCommandHelpTable
private const string CommandsGroupSummary =
"@help commands - Lists all commands.";
private const string GroupDetailUnverifiedSuffix =
" UNVERIFIED: the rest of this group's retail listing routes through ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290, which builds its text from BN-mislabeled data fragments around a live channel-name lookup — not decodable with confidence from a static string sweep. See docs/research/2026-08-09-chat-retail-command-registry.md §5 and ISSUES.md #364.";
// acclient_2013_pseudo_c.txt:391365-391436 (HelpDeathGroup, decomp
// §5 sweep, addresses 0x57b913-0x57ba56). The Detail branch is a
// straight-line concatenation with NO leading group-summary line (that
@ -344,7 +517,7 @@ public static class RetailCommandHelpTable
public const string StatusGroupDetail =
"@age - Displays your total gameplay time.\n"
+ "@birth - Displays when your character was created.\n"
+ "@day - A toggle that lightens the outdoor landscape. Note that this command may take several seconds to take effect. \n"
+ DayLine
+ "@endurance - Explains how endurance affects your character.\n"
+ "@framerate - Toggles the framerate display.\n"
+ "@loc - Displays your current position.\n"
@ -366,45 +539,132 @@ public static class RetailCommandHelpTable
"@allegiance - Commands to help manage your allegiance.\n"
+ "@allegiance motd - Displays or sets the message of the day for your allegiance, see @help motd for more information.\n";
// Partial: own summary (fully verbatim, swept) + the honest UNVERIFIED
// note for the HelpStupidChannelHack-delegated remainder. HelpChannelsGroup's
// Detail branch (acclient_2013_pseudo_c.txt:387166-387194) is ENTIRELY
// 6 HelpStupidChannelHack calls -- none of its own text is decodable.
public const string ChannelsGroupDetail =
ChannelsGroupSummary + GroupDetailUnverifiedSuffix;
// ── HelpStupidChannelHack, decoded (round 4, 2026-08-10) ────────────
//
// ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290 builds
// one line as `"@" + tag + " - Sends a broadcast to your " +
// ChannelName + ".\n"`. Read directly off the function's own
// disassembly (the `push imm32` immediately preceding each
// PStringBase::PStringBase constructor call, since BN's line-grouped
// pseudo-C hides the real instruction order and mislabels each pushed
// address as an unrelated vtable slot):
// - "@" @ data_0x7d0424, " - Sends a broadcast to your " @
// data_0x7d0428, trailing ".\n" @ data_0x7d0448 -- all three swept
// via tools/pdb-extract/sweep_weenie_strings.py --anchor against
// the PDB-paired C:\Users\erikn\Downloads\acclient.exe.
// - `tag` is one character sliced out of a shared wide literal
// `U"fvpca"` (plus one more, `U"mh,."`, for 'm') -- reading a WIDE
// string through a NARROW char* naturally truncates to one
// character at the first zero high byte, which is the "hack" the
// function's own retail name calls out.
// - `ChannelName` comes from `ChannelSystem::GetChannelID(tag)` (read
// directly: 'a'=0x2000000, 'c'=0x1000000, 'm'=0x4000, 'p'=0x2000,
// 'v'=0x1000, 'f'=0x800) then `ChannelSystem::GetChannelName(id)`'s
// own literal switch table (also read directly, not guessed):
// 0x2000="Patron", 0x4000="Monarch", 0x800="Fellowship",
// 0x1000="Vassals", 0x1000000="Co-vassals", and 0x2000000 resolves
// through one more mislabeled vtable slot that sweeps to
// data_0x7d0478="Allegiance".
public const string AllegianceBroadcastLine = "@a - Sends a broadcast to your Allegiance.\n";
public const string CovassalBroadcastLine = "@c - Sends a broadcast to your Co-vassals.\n";
public const string MonarchBroadcastLine = "@m - Sends a broadcast to your Monarch.\n";
public const string PatronBroadcastLine = "@p - Sends a broadcast to your Patron.\n";
public const string VassalBroadcastLine = "@v - Sends a broadcast to your Vassals.\n";
public const string FellowshipBroadcastLine = "@f - Sends a broadcast to your Fellowship.\n";
// Partial: HelpChattingGroup's Detail branch
// (acclient_2013_pseudo_c.txt:390909-390984) resolves 7 of 8 entries
// verbatim (chat/notell/reply/retell/say/tell/afk, each an inline
// literal or a direct HelpXxx(Summary_HelpType,...) call whose own text
// was independently swept) -- only the 5 HelpStupidChannelHack calls in
// the middle (channel-alias entries) are unresolved.
// HelpChannelsGroup's Detail branch (acclient_2013_pseudo_c.txt:387166-
// 387194) is ENTIRELY 6 HelpStupidChannelHack calls, read in this exact
// call order (a, c, m, p, v, f) -- no leading group-summary line (that
// string is exclusive to the Summary_HelpType branch). COMPLETE
// verbatim listing as of round 4; no UNVERIFIED remainder.
public const string ChannelsGroupDetail =
AllegianceBroadcastLine
+ CovassalBroadcastLine
+ MonarchBroadcastLine
+ PatronBroadcastLine
+ VassalBroadcastLine
+ FellowshipBroadcastLine;
// ClientCommunicationSystem::HelpReply @0x00577A50's Summary_HelpType
// branch (read directly, not assumed): it unconditionally concatenates
// THREE lines -- reply, then pr, then mr -- regardless of which tag it
// was called with. This is a genuine retail quirk (the shared handler
// ignores its own `arg3` tag parameter entirely in both branches),
// ported faithfully per CLAUDE.md's "do not fix the decompiled code"
// rule. Swept verbatim against the PDB-paired
// C:\Users\erikn\Downloads\acclient.exe, --range 0x577a50 0x577bd0.
public const string ReplySummaryLine = "@reply - Sends some text to the last person who @tell'd you.\n";
public const string PatronReplySummaryLine = "@pr - Sends some text to the last person who @p'd you.\n";
public const string MonarchReplySummaryLine = "@mr - Sends some text to the last person who @m'd you.\n";
// HelpChattingGroup's Detail branch (acclient_2013_pseudo_c.txt:390909-
// 390998) read in full, exact source order: chat, notell,
// HelpReply(Summary) [reply+pr+mr, see above], retell, say, tell, the
// SAME 6-line HelpStupidChannelHack block as ChannelsGroupDetail, afk.
// COMPLETE verbatim listing as of round 4. The "@say" line's own retail
// literal (data_0x7dadb4, swept) genuinely has NO trailing newline --
// confirmed by direct byte inspection, not a copy/paste drop -- so
// retail's own output runs "@say"'s line straight into "@tell"'s with
// no line break; ported as found.
public const string ChattingGroupDetail =
"@chat - Sets whether or not you receive normal chat.\n"
+ "@notell - Sets whether or not you receive @tell's.\n"
+ "@reply - Sends some text to the last person who @tell'd you.\n"
+ ReplySummaryLine
+ PatronReplySummaryLine
+ MonarchReplySummaryLine
+ "@retell - Sends some text to the last person you @tell'd.\n"
+ "@say - Says some text to everyone around you.\n"
+ "@say - Says some text to everyone around you."
+ "@tell - Sends a private message to another character.\n"
+ "[5 more retail channel-alias one-liners not yet extracted verbatim -- ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290, UNVERIFIED; see the class remarks above.]\n"
+ ChannelsGroupDetail
+ "@afk - Set your away-from-keyboard status.\n";
// HelpAllGroup's Detail branch (acclient_2013_pseudo_c.txt:394006-394176)
// recursively calls HelpAllegiancesGroup AND HelpChattingGroup in Detail
// mode (not Summary), plus the same 5 HelpStupidChannelHack calls
// directly, plus HelpDeathGroup in Detail mode, plus several more direct
// entries, plus a CONFIRMED retail duplicate (the saveui/loadui pair
// appears twice in the straight-line decompiled body with no
// conditional between the two occurrences -- verified at
// acclient_2013_pseudo_c.txt:394089-394137, not an acdream artifact).
// The nesting compounds two independent HelpStupidChannelHack gaps
// (its own 5 calls + HelpChattingGroup's 5) with enough structural
// uncertainty (unconfirmed HelpType arg on the trailing @status/@text
// calls) that a full reconstruction risks presenting an inaccurate
// listing as verbatim. Kept UNVERIFIED with its own summary (fully
// decoded) rather than risk that.
// Short one-liners HelpAllGroup builds itself (distinct retail literals
// from the catalog's own longer Detail text for the same verbs --
// e.g. this @saveui line is NOT SaveUiDetail's text; both are genuine
// retail strings, just from two different call sites). Swept verbatim,
// --range 0x57e7f0 0x57eba0 against the PDB-paired
// C:\Users\erikn\Downloads\acclient.exe.
public const string EmoteAndEmotesShortSummary =
"@emote - Performs a text emote.\n@emotes - Lists all standard emotes.\n";
public const string FillCompsShortSummary = "@fillcomps - Helps you buy components in bulk.\n";
public const string SaveUiShortSummary = "@saveui <filename> - Saves the current user interface.\n";
public const string LoadUiShortSummary = "@loadui <filename> - Loads a previously saved user interface.\n";
public const string FriendsShortSummary = "@friends - Helps you manage your friends list.\n";
public const string SquelchGroupShortSummary =
"@squelch - Squelches a character or account.\n"
+ "@unsquelch - Unsquelches a squelched character or account.\n"
+ "@messagetypes - Lists all types of messages that can be squelched or filtered.\n";
// HelpAllGroup's Detail branch (acclient_2013_pseudo_c.txt:394006-394205,
// VA 0x0057E7FA-0x0057EB8E) read in full, exact source order:
// HelpAllegiancesGroup(Detail), the SAME 6-line HelpStupidChannelHack
// block, HelpChattingGroup(Detail), HelpDeathGroup(Detail), the emote
// one-liner pair, fillcomps, saveui(short), loadui(short), saveui(short)
// AGAIN, loadui(short) AGAIN, lockui, friends(short), the house
// one-liner, the squelch/unsquelch/messagetypes one-liner trio,
// HelpStatusGroup(Detail), HelpTextGroup(Detail). The saveui/loadui
// duplicate is a CONFIRMED retail duplicate -- the straight-line
// decompiled body repeats both calls with no conditional between the
// two occurrences (verified directly, not an acdream artifact) --
// ported as found. COMPLETE verbatim listing as of round 4; no
// UNVERIFIED remainder. Closes ISSUES.md #364.
public const string CommandsGroupDetail =
CommandsGroupSummary + GroupDetailUnverifiedSuffix;
AllegiancesGroupDetail
+ ChannelsGroupDetail
+ ChattingGroupDetail
+ DeathGroupDetail
+ EmoteAndEmotesShortSummary
+ FillCompsShortSummary
+ SaveUiShortSummary
+ LoadUiShortSummary
+ SaveUiShortSummary
+ LoadUiShortSummary
+ LockUiDetail
+ FriendsShortSummary
+ HouseOneLiner
+ SquelchGroupShortSummary
+ StatusGroupDetail
+ TextGroupDetail;
// Campaign CH user-gate round 3 (2026-08-10), finding (b): DoHelp's
// bare-/help "else" branch (arg2<=0, acclient_2013_pseudo_c.txt:395089-
@ -495,20 +755,28 @@ public static class RetailCommandHelpTable
// falling back to the catalog's invented summary.
//
// <c>messagetypes</c>/<c>message_types</c>/<c>msgtypes</c>/<c>msg_types</c>
// are the one leaf verb NOT extracted this round:
// were the one leaf verb not extracted at the consolidated-review round:
// `HelpMessageTypes @0x0056e5d0` calls
// `gmCCommunicationSystem::GetListofSquelchChannels(arg4)` to build its
// text from a live enum table at runtime — there is no static string to
// sweep. It keeps its acdream summary, honestly UNVERIFIED rather than
// guessed at.
// text from a live enum table at runtime, not a static string. Round 4
// (2026-08-10) ported that live construction for real instead of
// leaving an acdream summary in its place — see
// <see cref="MessageTypesDetail"/> and <see cref="BuildMessageTypesDetail"/>
// below for the full trace (IsLegalChannel's fixed whitelist +
// LogTextTypeToString's fixed name table + the exact join/wrap format,
// all read directly off the decompiled bodies). Because both the
// whitelist and the name table are fixed data, the resulting text is
// itself a compile-time constant — this is "port the construction," not
// "guess the result."
//
// Coverage this round: 42 of the 47 catalog Definitions got a verbatim
// Coverage this round: 43 of the 47 catalog Definitions have a verbatim
// Detail override (<see cref="CatalogVerbDetailByVerb"/>, several
// sharing one extraction per the CmdHashData note above), 4 are
// CONFIRMED-NULL (<see cref="CatalogVerbsWithNoRetailHelp"/>), and 1
// (messagetypes and its 3 aliases) remains an honest acdream summary —
// <c>RetailClientCommandHelpCoverageTests</c> pins these counts so a
// future extraction pass (or an accidental regression) is caught.
// sharing one extraction per the CmdHashData note above, plus
// messagetypes' real construction), and 4 are CONFIRMED-NULL
// (<see cref="CatalogVerbsWithNoRetailHelp"/>) — zero remain an
// unverified acdream summary. <c>RetailCommandHelpTableTests</c> pins
// these counts so a future extraction pass (or an accidental
// regression) is caught.
public const string LifestoneDetail =
"@lifestone - Returns you to the last lifestone you used without killing you.\n";
@ -617,6 +885,66 @@ public static class RetailCommandHelpTable
public const string HslistDetail =
"@hslist <house type> - Lists the number and, if appropriate, positions of houses currently available for purchase. Types include: Apartment, Cottage, Villa, Mansion\n";
// ── messagetypes: a real construction, not a hand-authored string ───
//
// Retail's HelpMessageTypes @0x0056E5D0 -> GetListofSquelchChannels
// @0x00589EB0 -> ConvertSQToPString @0x00589A80 (the `arg3 != 0`
// "enumerate every legal channel" branch) chain, read directly:
// 1. Iterate i = 0..0x21 in ascending order.
// 2. Keep i only if LogTextTypeEnumMapper::IsLegalChannel(i) @
// 0x006AFF40 -- a literal switch over exactly 14 ids: 0x02, 0x03,
// 0x06, 0x07, 0x0C, 0x10, 0x11, 0x12, 0x13, 0x15, 0x16, 0x17,
// 0x18, 0x19. (The SquelchInfo the real function iterates is a
// SYNTHETIC all-squelched object built solely to walk every legal
// id -- SquelchInfo::SquelchInfo(..., 1) -- not the player's
// actual squelch state, so this is genuinely a fixed enumeration,
// not a live/variable one.)
// 3. Get each kept id's display name via
// LogTextTypeEnumMapper::LogTextTypeToString @0x006AFF90 -- also a
// literal switch, read directly (not the C# enum member spelling,
// which differs by underscores for two entries): Speech, Tell,
// Combat, Magic, Emote, Appraisal, Spellcasting, Allegiance,
// Fellowship, Combat_Enemy, Combat_Self, Recall, Craft, Salvaging.
// 4. Join with ", " (data_0x794334, swept) and wrap in
// "Squelch channels are as follows:\n %s\n"
// (data_0x7e2fb8, swept off GetListofSquelchChannels' own
// sprintf call).
// Both the whitelist and the name table are fixed data, so the joined
// result is itself a compile-time constant -- SquelchLegalChannels +
// BuildMessageTypesDetail port the ENUMERATION rather than
// hand-authoring the already-known joined string, so a future
// whitelist change is a one-line table edit.
private static readonly (RetailLogTextType Type, string Name)[] SquelchLegalChannels =
{
(RetailLogTextType.Speech, "Speech"),
(RetailLogTextType.Tell, "Tell"),
(RetailLogTextType.Combat, "Combat"),
(RetailLogTextType.Magic, "Magic"),
(RetailLogTextType.Emote, "Emote"),
(RetailLogTextType.Appraisal, "Appraisal"),
(RetailLogTextType.Spellcasting, "Spellcasting"),
(RetailLogTextType.Allegiance, "Allegiance"),
(RetailLogTextType.Fellowship, "Fellowship"),
(RetailLogTextType.CombatEnemy, "Combat_Enemy"),
(RetailLogTextType.CombatSelf, "Combat_Self"),
(RetailLogTextType.Recall, "Recall"),
(RetailLogTextType.Craft, "Craft"),
(RetailLogTextType.Salvaging, "Salvaging"),
};
private static string BuildMessageTypesDetail() =>
"Squelch channels are as follows:\n "
+ string.Join(", ", Array.ConvertAll(SquelchLegalChannels, c => c.Name))
+ "\n";
/// <summary>
/// Retail's real <c>/help messagetypes</c> text (and its 3 aliases),
/// ported from the live construction retail itself performs -- see the
/// remarks above <see cref="SquelchLegalChannels"/>. No longer an
/// acdream-authored summary.
/// </summary>
public static readonly string MessageTypesDetail = BuildMessageTypesDetail();
private static readonly FrozenDictionary<string, string> CatalogVerbDetailByVerb =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
@ -671,10 +999,13 @@ public static class RetailCommandHelpTable
["unsquelch"] = SquelchDetail,
["filter"] = FilterDetail,
["unfilter"] = UnfilterDetail,
// messagetypes/message_types/msgtypes/msg_types intentionally
// absent — HelpMessageTypes builds its text from a live enum
// table at runtime (GetListofSquelchChannels), not a static
// string; see the class remarks above.
// Round 4 (2026-08-10): messagetypes/message_types/msgtypes/
// msg_types now resolve to MessageTypesDetail, the ported live
// construction — see the remarks above SquelchLegalChannels.
["messagetypes"] = MessageTypesDetail,
["message_types"] = MessageTypesDetail,
["msgtypes"] = MessageTypesDetail,
["msg_types"] = MessageTypesDetail,
["fillcomps"] = FillCompsDetail,
["endurance"] = EnduranceDetail,
["speaker"] = SpeakerDetail,