fix(chat): Campaign CH round 3 — SpewBox flush-top/font, /help exact print sequence
User-gate round 3 findings (a)-(c):
(a) SpewBox: TopOffset moves from the round-1 60px placeholder to 0 (flush
to the viewport top). SpewBoxController never wired DatFont/Font at all
before this round, so it silently rendered through the 15px debug
BitmapFont fallback; it now resolves retail dat Font 0x40000025
(MaxCharHeight=11px) through a new RetailUiRuntime.Assets accessor —
the smallest font id confirmed in use by any currently-imported retail
LayoutDesc fixture, cross-referenced against every
tests/AcDream.App.Tests/UI/Layout/fixtures/*.json dump and confirmed
against the installed DAT via AcDream.Cli dump-font-atlas. It is also the
chat window's own smallest font (the 0x2100006F floating-window 1/2/3/4
indicator badges), so both selection criteria the brief offered agree.
Both remain best-available approximations, not resolved retail values —
register row AP-178 updated accordingly.
(b)/(c) /help and /help death: round 2 extracted the individual retail
strings byte-exact but never traced ClientCommunicationSystem::DoHelp's
complete print sequence. Byte-swept DoHelp's own range plus the five
Summary-branch functions it calls into (HelpEmote/HelpSquelch/
HelpStatusGroup/HelpTextGroup/HelpAllGroup) against the PDB-paired
acclient.exe. Retail's real shape: bare /help prints exactly TWO scroll
entries (HelpPrefixNote, then the 13-item AvailableHelpListing built from
DoHelp's own literals and each group's Summary_HelpType branch, in exact
source order) — not the acdream-invented cheat sheet BuildHelpText()
built before. Any resolved /help <verb> gets the SAME two-entry shape:
HelpPrefixNote, then ForMoreInformationPrefix concatenated directly onto
the verb's own Detail text (retail's own unsubstituted "<command>"
literal, ported verbatim). ChatCommandRouter.EmitVerbHelp applies this
uniformly to every resolved verb, not just death. An unresolved verb now
shows retail's real "Unknown command" fallback text; that fallback types
0x1A (ClientLocal), which retail routes to the SpewBox exclusively — a
gap ChatVM's UI.Abstractions layer can't yet reach, filed as ISSUES #367
/ register AP-186 rather than left silently unregistered.
Jump-in-air (round 2's open item 1) was root-caused and fixed separately
at a5a7eb4f between rounds — recorded in the campaign ledger.
Debug suite (all projects): 12,329 passed / 4 skipped / 1 failed — the
one failure is issue #351, a pre-existing Debug-only streaming flake
confirmed reproducing identically on the pristine pre-round-3 commit via
git stash, not a regression. Release verification covers every project
reachable without rebuilding AcDream.App: a live client process (PID
15064) held its own Release binaries locked for the session and was not
killed per project policy — AcDream.UI.Abstractions.Tests (867/867, the
layer both /help fixes live in) plus every other non-App-dependent
project, all 0 failed. AcDream.App/AcDream.App.Tests/AcDream.Core.Tests
(the SpewBox fix's layer) are green in Debug only this session.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a5a7eb4fb6
commit
98de4f5ab3
14 changed files with 665 additions and 105 deletions
|
|
@ -184,7 +184,7 @@ public static class ChatCommandRouter
|
|||
{
|
||||
if (EqAny(trimmed, "/help", "/?", "@help", "@?"))
|
||||
{
|
||||
vm.ShowSystemMessage(BuildHelpText());
|
||||
EmitBareHelp(vm);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -195,25 +195,64 @@ public static class ChatCommandRouter
|
|||
if (StartsWithAny(trimmed, "/help ", "@help ", "/? ", "@? "))
|
||||
{
|
||||
string verb = trimmed[(trimmed.IndexOf(' ') + 1)..].Trim();
|
||||
vm.ShowSystemMessage(BuildVerbHelpText(verb));
|
||||
EmitVerbHelp(verb, vm);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string BuildVerbHelpText(string verb)
|
||||
/// <summary>
|
||||
/// Bare <c>/help</c> — retail's <c>DoHelp</c> arg2<=0 branch. TWO
|
||||
/// separate scroll entries, never one concatenated blob (Campaign CH
|
||||
/// user-gate round 3, finding (b) — see <see cref="RetailCommandHelpTable"/>'s
|
||||
/// class remarks for the full print-sequence trace).
|
||||
/// </summary>
|
||||
private static void EmitBareHelp(ChatVM vm)
|
||||
{
|
||||
vm.ShowSystemMessage(RetailCommandHelpTable.HelpPrefixNote);
|
||||
vm.ShowSystemMessage(RetailCommandHelpTable.AvailableHelpListing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>/help <verb></c> — retail's <c>DoHelp</c> arg2>0 branch.
|
||||
/// Campaign CH user-gate round 3, finding (c): a resolved verb gets the
|
||||
/// SAME two-entry shape as the bare listing (Note, then
|
||||
/// <see cref="RetailCommandHelpTable.ForMoreInformationPrefix"/>
|
||||
/// immediately concatenated — NOT a third entry — with the verb's own
|
||||
/// Detail text); an unresolved verb gets retail's real
|
||||
/// <see cref="RetailCommandHelpTable.UnknownCommand"/> text instead of
|
||||
/// an acdream-invented "No help available" message.
|
||||
/// </summary>
|
||||
private static void EmitVerbHelp(string verb, ChatVM vm)
|
||||
{
|
||||
if (verb.Length == 0)
|
||||
return BuildHelpText();
|
||||
{
|
||||
EmitBareHelp(vm);
|
||||
return;
|
||||
}
|
||||
|
||||
string normalized = verb.TrimStart('/', '@');
|
||||
if (RetailClientCommandCatalog.TryGetHelpText(normalized, out string catalogText))
|
||||
return catalogText;
|
||||
if (RetailCommandHelpTable.TryGetHelpText(normalized, out string tableText))
|
||||
return tableText;
|
||||
{
|
||||
vm.ShowSystemMessage(RetailCommandHelpTable.HelpPrefixNote);
|
||||
vm.ShowSystemMessage(RetailCommandHelpTable.ForMoreInformationPrefix + catalogText);
|
||||
return;
|
||||
}
|
||||
|
||||
return $"No help available for '{verb}'.";
|
||||
if (RetailCommandHelpTable.TryGetHelpText(normalized, out string tableText))
|
||||
{
|
||||
vm.ShowSystemMessage(RetailCommandHelpTable.HelpPrefixNote);
|
||||
vm.ShowSystemMessage(RetailCommandHelpTable.ForMoreInformationPrefix + tableText);
|
||||
return;
|
||||
}
|
||||
|
||||
// Retail types this 0x1A (ClientLocal) -> SpewBox-only; ChatVM has
|
||||
// no SpewBox routing capability yet, so this still renders via the
|
||||
// chat scroll — a pre-existing gap, not new this round. See the
|
||||
// class remarks on RetailCommandHelpTable.UnknownCommand and
|
||||
// ISSUES.md #367.
|
||||
vm.ShowSystemMessage(RetailCommandHelpTable.UnknownCommand);
|
||||
}
|
||||
|
||||
private static bool EqAny(string value, params string[] options)
|
||||
|
|
@ -237,14 +276,4 @@ public static class ChatCommandRouter
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string BuildHelpText() =>
|
||||
$"{RetailCommandHelpTable.HelpPrefixNote}\n" +
|
||||
"Chat: /say (default), /tell <name>, <text>, /reply, /retell\n" +
|
||||
"Channels: /general /trade /fellowship /a (allegiance room)\n" +
|
||||
" /patron /vassals /monarch /covassals\n" +
|
||||
" /lfg /roleplay /society /olthoi\n" +
|
||||
"Client: /help [command] (this) /clear /framerate /loc\n" +
|
||||
$" {RetailClientCommandCatalog.BuildHelpText()}\n" +
|
||||
"Server: type @acehelp or @acecommands for ACE's full list.";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ public static class RetailClientCommandCatalog
|
|||
// (data_7de2f8) — the first paragraph only; the full multi-paragraph
|
||||
// block is reproduced verbatim by DoEndurance itself and is long
|
||||
// enough that only the opening line is duplicated here as a teaser —
|
||||
// BuildHelpText below uses the SAME text callers already see through
|
||||
// this is the SAME text callers already see through
|
||||
// ClientCommandController.
|
||||
private static readonly Definition Endurance = NoArguments(
|
||||
ClientCommandId.Endurance,
|
||||
|
|
@ -668,52 +668,6 @@ public static class RetailClientCommandCatalog
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Help line generated from the same definition routing uses.</summary>
|
||||
public static string BuildHelpText() => string.Join("\n ",
|
||||
Lifestone.HelpText,
|
||||
Marketplace.HelpText,
|
||||
PkArena.HelpText,
|
||||
PkLiteArena.HelpText,
|
||||
PkLite.HelpText,
|
||||
HouseRecall.HelpText,
|
||||
MansionRecall.HelpText,
|
||||
HouseAbandon.HelpText,
|
||||
QueryAge.HelpText,
|
||||
QueryBirth.HelpText,
|
||||
FrameRate.HelpText,
|
||||
LockUi.HelpText,
|
||||
Version.HelpText,
|
||||
Location.HelpText,
|
||||
Corpse.HelpText,
|
||||
Die.HelpText,
|
||||
Clear.HelpText,
|
||||
SaveUi.HelpText,
|
||||
LoadUi.HelpText,
|
||||
SaveAutoUi.HelpText,
|
||||
LoadAutoUi.HelpText,
|
||||
Away.HelpText,
|
||||
Consent.HelpText,
|
||||
Emote.HelpText,
|
||||
Emotes.HelpText,
|
||||
Friends.HelpText,
|
||||
Squelch.HelpText,
|
||||
Unsquelch.HelpText,
|
||||
Filter.HelpText,
|
||||
Unfilter.HelpText,
|
||||
MessageTypes.HelpText,
|
||||
FillComponents.HelpText,
|
||||
Endurance.HelpText,
|
||||
Speaker.HelpText,
|
||||
SetTitle.HelpText,
|
||||
ChatToggle.HelpText,
|
||||
NoTellToggle.HelpText,
|
||||
JoinChannel.HelpText,
|
||||
LeaveChannel.HelpText,
|
||||
Permit.HelpText,
|
||||
HouseAvailableList.HelpText,
|
||||
AllegianceHometown.HelpText,
|
||||
AllegianceInfo.HelpText);
|
||||
|
||||
/// <summary>
|
||||
/// Every verb string this catalog dispatches, INCLUDING the
|
||||
/// specially-parsed "house"/"hou"/"allegiance"/"all" verbs (which are
|
||||
|
|
|
|||
|
|
@ -58,6 +58,48 @@ namespace AcDream.UI.Abstractions.Panels.Chat;
|
|||
/// unresolved mechanism and remain acdream summaries. See the remarks on
|
||||
/// <see cref="ChannelsGroupSummary"/> for the full extraction method.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Campaign CH user-gate round 3 (2026-08-10), findings (b)/(c):</b> the
|
||||
/// round 2 pass extracted the individual STRINGS byte-exact but never
|
||||
/// traced <c>ClientCommunicationSystem::DoHelp @0x0057f9e0</c>'s complete
|
||||
/// PRINT SEQUENCE — so the CONTENT was right while the SHAPE (how many
|
||||
/// transcript entries, in what order, with what wrapping) was still
|
||||
/// acdream-invented. Traced this round via a byte-sweep of DoHelp's own
|
||||
/// range (0x57f9e0-0x57fe7e) plus every Summary-branch it calls into
|
||||
/// (HelpEmote/HelpSquelch/HelpStatusGroup/HelpTextGroup/HelpAllGroup),
|
||||
/// against the PDB-paired <c>C:\Users\erikn\Downloads\acclient.exe</c>
|
||||
/// (verified MATCH). DoHelp ALWAYS prints via exactly two
|
||||
/// <c>AddTextToScroll</c> calls (never one concatenated blob), both typed
|
||||
/// <c>0</c> (informational):
|
||||
/// <list type="number">
|
||||
/// <item>the SAME <see cref="HelpPrefixNote"/> line, unconditionally —
|
||||
/// for the bare listing AND for every successful <c>/help <verb></c>
|
||||
/// lookup;</item>
|
||||
/// <item>bare <c>/help</c>: <see cref="AvailableHelpListing"/> (13 items,
|
||||
/// straight-line concatenation of 8 inline literals plus 5 delegated to
|
||||
/// each group's own Summary_HelpType branch, in DoHelp's exact source
|
||||
/// order). <c>/help <verb></c> when the verb resolves:
|
||||
/// <see cref="ForMoreInformationPrefix"/> immediately followed by the
|
||||
/// verb's own Detail-branch text (e.g. <see cref="DeathGroupDetail"/>) —
|
||||
/// ONE string, no blank line between the prefix and the listing, because
|
||||
/// retail's own handler call APPENDS into the same accumulator the
|
||||
/// prefix was built into (<c>ClientCommunicationSystem::DoHelp</c>,
|
||||
/// arg2>0 branch, the <c>eax_35(2, var_18, &var_10)</c> call).</item>
|
||||
/// </list>
|
||||
/// When the verb does NOT resolve, DoHelp prints ONE entry,
|
||||
/// <see cref="UnknownCommand"/>, typed <c>0x1A</c> (<c>ClientLocal</c>) —
|
||||
/// retail routes that type to the SpewBox exclusively, never the chat
|
||||
/// window (<c>docs/research/2026-08-09-chat-retail-interface-text.md</c>
|
||||
/// §2.1/§2.2). <c>ChatCommandRouter</c> operates on <c>ChatVM</c>
|
||||
/// (<c>AcDream.UI.Abstractions</c>), which has no SpewBox routing
|
||||
/// capability — wiring that would mean threading a
|
||||
/// <c>RuntimeCommunicationState</c>-shaped dependency down into a layer
|
||||
/// that must stay presentation/Runtime-independent, out of this round's
|
||||
/// scope. The unknown-verb fallback therefore still renders via the chat
|
||||
/// scroll, a pre-existing (not newly introduced) gap now tracked at
|
||||
/// ISSUES.md #367 instead of silently continuing unregistered.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class RetailCommandHelpTable
|
||||
{
|
||||
|
|
@ -73,10 +115,42 @@ public static class RetailCommandHelpTable
|
|||
public const string Retell =
|
||||
"@retell <text> - Sends the text to the last person you @tell'd. You may also use @rt.";
|
||||
|
||||
// acclient_2013_pseudo_c.txt:1031564 (data_7e11e0), the "Note:" line
|
||||
// DoHelp @0x0057F9E0 prints alongside the bare group index.
|
||||
// acclient_2013_pseudo_c.txt:394980 (data_0x7e11e0). RE-SWEPT byte-exact
|
||||
// Campaign CH user-gate round 3 (2026-08-10) via sweep_weenie_strings.py
|
||||
// --range 0x57f9e0 0x57fe7e --ascii-only against the PDB-paired
|
||||
// C:\Users\erikn\Downloads\acclient.exe (verified MATCH). The ORIGINAL
|
||||
// extraction dropped the leading blank line and the trailing double
|
||||
// newline that are part of retail's own literal — this is DoHelp's
|
||||
// OWN, always-first scroll entry (a dedicated AddTextToScroll call,
|
||||
// never concatenated with what follows), printed unconditionally both
|
||||
// for the bare /help listing and for every successful /help <verb>
|
||||
// lookup. See the class remarks for the complete print-sequence trace.
|
||||
public const string HelpPrefixNote =
|
||||
"Note: You may substitute a forward slash (/) for the at symbol (@).";
|
||||
"\nNote: You may substitute a forward slash (/) for the at symbol (@).\n\n";
|
||||
|
||||
// acclient_2013_pseudo_c.txt:395087 (data_0x7e1178), swept alongside
|
||||
// HelpPrefixNote above. DoHelp's arg2>0 (verb-specified) branch builds
|
||||
// this as the START of its second scroll entry, then the resolved
|
||||
// verb's own Detail-branch handler APPENDS its listing directly onto
|
||||
// the SAME accumulator (ClientCommunicationSystem::DoHelp, the
|
||||
// `eax_35(2, var_18, &var_10)` call) -- so the retail-faithful port is
|
||||
// string concatenation with NO separator, not two entries. The literal
|
||||
// text "<command>" is NOT a format placeholder -- the decomp shows a
|
||||
// straight PStringBase construction with no sprintf/substitution call
|
||||
// between this literal and its use, so retail never actually inserts
|
||||
// the real verb name here. Ported verbatim per CLAUDE.md's "do not fix
|
||||
// the decompiled code" rule, even though it reads like an unfinished
|
||||
// dev message.
|
||||
public const string ForMoreInformationPrefix =
|
||||
"For more information, type @help <command>.\n";
|
||||
|
||||
// acclient_2013_pseudo_c.txt:395052 (u"Unknown command", UTF-16LE) --
|
||||
// DoHelp's fallback when the verb hash lookup fails, or resolves to an
|
||||
// entry with no registered help callback. Retail types this 0x1A
|
||||
// (ClientLocal) -- SpewBox-only; see the class remarks' routing note
|
||||
// and ISSUES.md #367 for why ChatCommandRouter still shows it in the
|
||||
// chat scroll.
|
||||
public const string UnknownCommand = "Unknown command";
|
||||
|
||||
// @mr/@pr are registered with a NULL function pointer in the 2013
|
||||
// build (verified at 0x00583041/0x005830C1 — arg3 is 0), so they never
|
||||
|
|
@ -305,6 +379,40 @@ public static class RetailCommandHelpTable
|
|||
public const string CommandsGroupDetail =
|
||||
CommandsGroupSummary + GroupDetailUnverifiedSuffix;
|
||||
|
||||
// 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-
|
||||
// 395268), swept whole against C:\Users\erikn\Downloads\acclient.exe
|
||||
// (--range 0x57f9e0 0x57fe7e --ascii-only, verified MATCH). ONE
|
||||
// straight-line concatenation of 13 items, in this exact source order:
|
||||
// the "Available help:\n" header, then 8 items DoHelp builds from its
|
||||
// OWN inline literals (allegiances/channels/chatting/death/fillcomps/
|
||||
// friends/house — swept directly), interleaved with 5 items DoHelp
|
||||
// builds by calling each group's OWN Summary_HelpType branch
|
||||
// (HelpEmote/HelpSquelch/HelpStatusGroup/HelpTextGroup/HelpAllGroup —
|
||||
// each independently swept from its own function range; every one of
|
||||
// those 5 functions has the identical
|
||||
// `if (arg2 != Summary_HelpType) {Detail} else {"@help X - ..."}`
|
||||
// shape HelpEmote makes explicit at acclient_2013_pseudo_c.txt:388664).
|
||||
// channels/chatting/commands' summary lines are reused from the
|
||||
// consts above (independently cross-validated: both extractions agree
|
||||
// byte-for-byte). Prints as DoHelp's SECOND scroll entry, right after
|
||||
// HelpPrefixNote's own — see the class remarks and
|
||||
// ChatCommandRouter's bare-/help handling.
|
||||
public const string AvailableHelpListing =
|
||||
"Available help:\n"
|
||||
+ "@help allegiances - Commands to help you deal with your Allegiance.\n"
|
||||
+ ChannelsGroupSummary + "\n"
|
||||
+ ChattingGroupSummaryVerbatim + "\n"
|
||||
+ "@help death - Commands for making, finding, and looting corpses.\n"
|
||||
+ "@help emote - How to perform text and action emotes.\n"
|
||||
+ "@help fillcomps - A command to help you buy components in bulk.\n"
|
||||
+ "@help friends - Commands to help you manage your friends list.\n"
|
||||
+ "@help house - Commands that help you manage your house, including guest and storage management.\n"
|
||||
+ "@help squelch - Commands that let you block out messages from other players.\n"
|
||||
+ "@help status - Commands that display useful information.\n"
|
||||
+ "@help text - Commands that help you manage your text window.\n"
|
||||
+ CommandsGroupSummary + "\n";
|
||||
|
||||
private static readonly FrozenDictionary<string, string> ByVerb =
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue