fix(chat): Campaign CH user-gate round 2 -- portal notice rerouted to SpewBox, verbatim /help extraction, jump-in-air evidence

Item 2: retail's portal-space "In Portal Space..." notice is the SpewBox
(ECM_UI::SendNotice_DisplayStringInfo(0x1A,...) -> AddTextToScroll(str,
0x1A, 1, 0), hardcoded to the SpewBox per the decomp), not a dedicated
centered overlay. PortalWaitNoticeController and its lease are deleted;
PortalTunnelPresentation's per-rotation-segment cadence now writes
straight into RuntimeCommunicationState.AddText(ClientLocal) -- the
SpewBox's own dedupe-at-index-0 handles the repetition exactly as
retail's does. Register row AP-184 records the surface fix and the AP-178
scope extension.

Items 4+5: /help text was partially fabricated -- the user caught the
"/help death" meta-message. Generalized
tools/pdb-extract/sweep_weenie_strings.py to decode narrow
PStringBase<char> literals (the ClientCommunicationSystem::Help* family's
shape) alongside its original UTF-16LE support, then swept every
HelpXxxGroup function's exact byte extent against the PDB-paired
acclient.exe. 4 of 7 group topics (death/status/text/allegiances) are now
complete verbatim listings; the other 3 (channels/chatting/commands) keep
an honest UNVERIFIED note citing HelpStupidChannelHack @0x0056f290 (a
genuinely undecodable BN-mislabeled-fragment mechanism) instead of the
old fabricated sentinel. 7 of ~35 channel one-liners are also now
verbatim. ISSUES.md #364 tracks the remainder;
RetailCommandHelpTableTests.cs pins every result byte-exact.

Item 1: jump-in-air refusal still silent live is NOT reproduced and NOT
speculatively fixed. Exhaustive static re-audit found the mechanism
correct by construction (single-writer OnWalkable, exactly-once-per-frame
Update()/Capture(), no interfering edge-history resets). A live headless
repro (new jump-probe bot policy, real ACE connect) was blocked --
probeaccount2 has no character, and the graphical client already owned
testaccount this session so the task's own fallback rule forbade using
it. Two temporary probes are left behind ACDREAM_PROBE_JUMP=1 (blocked
entirely in Headless by the existing multi-session static-state guard --
graphical-only for the next round).

Item 3 confirmed fixed, no regression. Item 6 (resize: no diagonal
cursors, cannot grow Y from bottom-right) folded into CH6a's existing
scope.

Full Release suite: 12,267 passed / 4 skipped / 0 failed (up from
12,221/4/0).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-10 08:40:24 +02:00
parent a485425743
commit c1f1582576
14 changed files with 788 additions and 254 deletions

View file

@ -22,10 +22,9 @@ namespace AcDream.UI.Abstractions.Panels.Chat;
/// <c>docs/research/named-retail/acclient_2013_pseudo_c.txt</c> by the
/// recipe in the command-registry doc §5 (scan each <c>Help*</c>
/// function's byte extent for <c>push imm32</c> into <c>.rdata</c>). Entries
/// that could not be recovered this way (the 7 group-index headers'
/// summary text, @day's confirmation lines, @render's option list) are
/// NOT fabricated — they are simply absent from this table; the lookup
/// falls through to a generic "no detailed help" line rather than guess.
/// that could not be recovered this way (@render's option list) are NOT
/// fabricated — they are simply absent from this table; the lookup falls
/// through to a generic "no detailed help" line rather than guess.
/// </para>
///
/// <para>
@ -36,9 +35,28 @@ namespace AcDream.UI.Abstractions.Panels.Chat;
/// one-liners in <see cref="ByVerb"/> ("Sends text to your Fellowship
/// channel.", etc.) are acdream-authored SUMMARIES, not individually
/// hand-extracted retail strings — retail's own per-channel help text was
/// not recovered this slice. Recovering them (or deleting the class-doc
/// overclaim) is future work; this comment now says so honestly instead
/// of leaving the contradiction standing.
/// not recovered this slice.
/// </para>
///
/// <para>
/// <b>Campaign CH user-gate round 2, item 3 (2026-08-09):</b> the user
/// 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.
/// </para>
/// </summary>
public static class RetailCommandHelpTable
@ -154,11 +172,138 @@ public static class RetailCommandHelpTable
// The 7 retail-registered "group index" nodes — retail registers them
// with a NULL func (like @mr/@pr); typing one bare reaches the server.
// Only @help <group> shows anything, and only this generic pointer —
// the exact per-group summary/listing text (retail's HelpXxxGroup
// functions) was not extracted this slice; see ISSUES.md.
private const string GroupNodeNotExtracted =
"This is a retail help-topic group; acdream has not yet extracted its exact retail listing text. Typing this verb alone (without @help) reaches the server as literal text, matching retail's null-handler registration.";
// Only @help <group> shows anything.
//
// Campaign CH user-gate round 2, item 3 (2026-08-09): the user caught
// that "/help death" printed an acdream META-MESSAGE instead of
// retail's real listing. Extracted verbatim via
// tools/pdb-extract/sweep_weenie_strings.py (generalized this round for
// narrow PStringBase<char> literals — the Help* family uses 8-bit ASCII,
// not the UTF-16LE ECM_UI notice shape the script originally targeted)
// against the PDB-paired C:\Users\erikn\Downloads\acclient.exe. Method:
// each HelpXxxGroup function (ClientCommunicationSystem::HelpXxxGroup,
// decomp §5) is a single `if (arg2 != Summary_HelpType) { <DETAIL:
// straight-line concatenation of subcommand summaries> } else { <the
// 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.
private const string ChannelsGroupSummary =
"@help channels - How to communicate with people in your allegiance or fellowship.";
// acclient_2013_pseudo_c.txt:391002 (0x0057b1b0). HelpChattingGroup's
// own one-liner — not used standalone below (its Detail branch does
// NOT prepend it, matching every other group), kept for documentation
// parity with the other two Summary consts above/below.
private const string ChattingGroupSummaryVerbatim =
"@help chatting - How to chat publically and privately.";
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
// string is exclusive to the Summary_HelpType branch, i.e. what a
// PARENT listing shows about "death" as one entry — @help death itself
// never shows it).
public const string DeathGroupDetail =
"@permit - Commands to give or revoke permission for others to loot your corpse.\n"
+ "@consent - Commands to help you manage the corpse-looting permissions that others give you.\n"
+ "@corpse - Displays the location of your last outdoor death.\n"
+ "@die - Kills your character and leaves a corpse, returning you to your lifestone.\n"
+ "@lifestone - Returns you to the last lifestone you used without killing you.\n"
+ "@marketplace - Teleports you to the Marketplace of Dereth.\n"
+ "@pkarena - Teleports you to the PK Arena. You must be PK to use this command.\n"
+ "@pklarena - Teleports you to the PKL Arena. You must be PKL to use this command.\n";
// acclient_2013_pseudo_c.txt:392030-392093 (HelpStatusGroup). Same
// shape as HelpDeathGroup — Detail branch, no leading group-summary
// line. The trailing space before "\n" on the @day line is retail's
// own byte content (data_0x7de280), not an acdream typo.
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"
+ "@endurance - Explains how endurance affects your character.\n"
+ "@framerate - Toggles the framerate display.\n"
+ "@loc - Displays your current position.\n"
+ "@pklite - Sets your status to Player Killer Lite. Type @help pklite for more details.\n"
+ "@version - Tells you what version of the software you are using.\n";
// acclient_2013_pseudo_c.txt:392209-392279 (HelpTextGroup).
public const string TextGroupDetail =
"@clear - Clears the chat box of all text.\n"
+ "@filter - Commands to filter out incoming messages.\n"
+ "@unfilter - Commands to remove filters from incoming messages.\n"
+ "@loadfile - Reads in the given text file and executes each line in the chat entry field.\n"
+ "@log - Commands to echo chat text to a logfile.\n"
+ "@title <new title> - Sets the title of the popup chat window.\n";
// acclient_2013_pseudo_c.txt:393033-393059 (HelpAllegiancesGroup) —
// the shortest group, only 2 lines.
public const string AllegiancesGroupDetail =
"@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;
// 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.
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"
+ "@retell - Sends some text to the last person you @tell'd.\n"
+ "@say - Says some text to everyone around you.\n"
+ "@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"
+ "@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.
public const string CommandsGroupDetail =
CommandsGroupSummary + GroupDetailUnverifiedSuffix;
private static readonly FrozenDictionary<string, string> ByVerb =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
@ -181,16 +326,24 @@ public static class RetailCommandHelpTable
["log"] = Log,
["render"] = Render,
["motd"] = Motd,
["commands"] = GroupNodeNotExtracted,
["allegiances"] = GroupNodeNotExtracted,
["channels"] = GroupNodeNotExtracted,
["chatting"] = GroupNodeNotExtracted,
["death"] = GroupNodeNotExtracted,
["status"] = GroupNodeNotExtracted,
["text"] = GroupNodeNotExtracted,
// Channel verbs — one line each, generated rather than
// hand-extracted (retail's per-channel help strings were not
// individually recovered this slice).
["commands"] = CommandsGroupDetail,
["allegiances"] = AllegiancesGroupDetail,
["channels"] = ChannelsGroupDetail,
["chatting"] = ChattingGroupDetail,
["death"] = DeathGroupDetail,
["status"] = StatusGroupDetail,
["text"] = TextGroupDetail,
// Channel verbs. Campaign CH user-gate round 2, item 3: 7 of
// these are now VERBATIM retail text (each its own
// HelpTurbineChat_Xxx function, decomp §5 sweep,
// acclient_2013_pseudo_c.txt:387294-387411/387410 area,
// addresses 0x577620-0x577850) — the "Also: @alias" suffix is
// retail's own text, not an acdream addition. The remaining
// ones (f/fellow(s)/fellowship/g/group/party, m/monarch,
// p/patron, v/vassal(s), c/covassal(s)/co-vassals) still route
// through the unresolved HelpStupidChannelHack pooled-string
// mechanism (see the class remarks above) and stay
// acdream-authored summaries — an honest gap, not a silent one.
["f"] = "Sends text to your Fellowship channel.",
["fellow"] = "Sends text to your Fellowship channel.",
["fellows"] = "Sends text to your Fellowship channel.",
@ -198,22 +351,22 @@ public static class RetailCommandHelpTable
["g"] = "Sends text to your Fellowship channel.",
["group"] = "Sends text to your Fellowship channel.",
["party"] = "Sends text to your Fellowship channel.",
["a"] = "Sends text to your Allegiance chat room.",
["guild"] = "Sends text to your Allegiance chat room.",
["gu"] = "Sends text to your Allegiance chat room.",
["a"] = "@a - Sends a message to your Allegiance. Also: @guild, @gu",
["guild"] = "@a - Sends a message to your Allegiance. Also: @guild, @gu",
["gu"] = "@a - Sends a message to your Allegiance. Also: @guild, @gu",
["ab"] = "Broadcasts text to your entire allegiance (monarch/speaker permission). Also @allegiance broadcast.",
["general"] = "Sends text to the General chat room.",
["cg"] = "Sends text to the General chat room.",
["trade"] = "Sends text to the Trade chat room.",
["ct"] = "Sends text to the Trade chat room.",
["lfg"] = "Sends text to the Looking-For-Group chat room.",
["clfg"] = "Sends text to the Looking-For-Group chat room.",
["roleplay"] = "Sends text to the Roleplay chat room.",
["crp"] = "Sends text to the Roleplay chat room.",
["society"] = "Sends text to your Society chat room.",
["soc"] = "Sends text to your Society chat room.",
["olthoi"] = "Sends text to the Olthoi Player Killer chat room.",
["o"] = "Sends text to the Olthoi Player Killer chat room.",
["general"] = "@general - Sends a message to the global General chat channel. Also: @cg",
["cg"] = "@general - Sends a message to the global General chat channel. Also: @cg",
["trade"] = "@trade - Sends a message to the global Trade chat channel. Also: @ct",
["ct"] = "@trade - Sends a message to the global Trade chat channel. Also: @ct",
["lfg"] = "@lfg - Sends a message to the global Looking For Group (LFG) chat channel. Also: @clfg",
["clfg"] = "@lfg - Sends a message to the global Looking For Group (LFG) chat channel. Also: @clfg",
["roleplay"] = "@roleplay - Sends a message to the global Roleplay chat channel. Also: @crp",
["crp"] = "@roleplay - Sends a message to the global Roleplay chat channel. Also: @crp",
["society"] = "@society - Sends a message to the your Society chat channel. Also: @soc",
["soc"] = "@society - Sends a message to the your Society chat channel. Also: @soc",
["olthoi"] = "@olthoi - If you are an Olthoi, sends a message to the global Olthoi chat channel. Also: @o",
["o"] = "@olthoi - If you are an Olthoi, sends a message to the global Olthoi chat channel. Also: @o",
["m"] = "Sends text to your Monarch.",
["monarch"] = "Sends text to your Monarch.",
["p"] = "Sends text to your Patron.",