acdream/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs
Erik 5b54387b8e 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>
2026-08-10 18:46:34 +02:00

653 lines
33 KiB
C#

using AcDream.UI.Abstractions.Panels.Chat;
using Xunit;
namespace AcDream.UI.Abstractions.Tests.Panels.Chat;
/// <summary>
/// Campaign CH user-gate round 2, item 3 (2026-08-09): pins a sample of
/// <see cref="RetailCommandHelpTable"/>'s extracted strings byte-exact
/// against the values recovered from the PDB-paired
/// <c>C:\Users\erikn\Downloads\acclient.exe</c> via
/// <c>tools/pdb-extract/sweep_weenie_strings.py --ascii-only</c> (generalized
/// this round for narrow <c>PStringBase&lt;char&gt;</c> literals — see the
/// script's own docstring). Covers all four COMPLETE group listings
/// (death/status/text/allegiances — <c>/help death</c> was the user's own
/// example of the fabricated meta-message this round replaces), the three
/// PARTIAL groups' honest UNVERIFIED fallback, and a sample of the newly
/// verbatim channel one-liners.
/// </summary>
public sealed class RetailCommandHelpTableTests
{
[Fact]
public void DeathGroup_IsCompleteVerbatimListing_NoLeadingSummaryLine()
{
// acclient_2013_pseudo_c.txt:391365-391436 (HelpDeathGroup Detail
// branch) -- straight-line concatenation, no "@help death - ..."
// prefix (that string is exclusive to the Summary_HelpType branch).
Assert.Equal(
"@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",
RetailCommandHelpTable.DeathGroupDetail);
Assert.True(RetailCommandHelpTable.TryGetHelpText("death", out string viaLookup));
Assert.Equal(RetailCommandHelpTable.DeathGroupDetail, viaLookup);
Assert.DoesNotContain("has not yet extracted", viaLookup);
}
[Fact]
public void StatusGroup_IsCompleteVerbatimListing_PreservesRetailTrailingSpace()
{
// The trailing space before "\n" on the @day line is retail's own
// byte content (data_0x7de280) -- not an acdream typo.
Assert.Equal(
"@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",
RetailCommandHelpTable.StatusGroupDetail);
}
[Fact]
public void TextGroup_IsCompleteVerbatimListing()
{
Assert.Equal(
"@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",
RetailCommandHelpTable.TextGroupDetail);
}
[Fact]
public void AllegiancesGroup_IsCompleteVerbatimListing_TwoLines()
{
Assert.Equal(
"@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",
RetailCommandHelpTable.AllegiancesGroupDetail);
}
// ── Round 4 (2026-08-10): the 3 PARTIAL groups are now COMPLETE ─────
// Closes ISSUES.md #364. No UNVERIFIED note, no meta-fabrication, ever.
[Fact]
public void ChannelsGroup_IsCompleteVerbatimListing_SixBroadcastLines()
{
// ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290,
// decoded: "@" + tag + " - Sends a broadcast to your " +
// ChannelName + ".\n", read in HelpChannelsGroup's own call order
// (a, c, m, p, v, f).
Assert.Equal(
"@a - Sends a broadcast to your Allegiance.\n"
+ "@c - Sends a broadcast to your Co-vassals.\n"
+ "@m - Sends a broadcast to your Monarch.\n"
+ "@p - Sends a broadcast to your Patron.\n"
+ "@v - Sends a broadcast to your Vassals.\n"
+ "@f - Sends a broadcast to your Fellowship.\n",
RetailCommandHelpTable.ChannelsGroupDetail);
Assert.True(RetailCommandHelpTable.TryGetHelpText("channels", out string viaLookup));
Assert.Equal(RetailCommandHelpTable.ChannelsGroupDetail, viaLookup);
Assert.DoesNotContain("UNVERIFIED", viaLookup);
Assert.DoesNotContain("not yet extracted", viaLookup);
}
[Fact]
public void ChattingGroup_IsCompleteVerbatimListing_ThirteenLines()
{
// HelpChattingGroup's Detail branch, exact source order: chat,
// notell, HelpReply(Summary) [reply+pr+mr], retell, say, tell, the
// same 6-line broadcast block as ChannelsGroupDetail, afk. The
// "@say" line's own retail literal has NO trailing newline
// (confirmed by direct byte sweep) -- retail's own output runs it
// straight into "@tell"'s line with no line break; ported as found.
Assert.Equal(
"@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"
+ "@pr - Sends some text to the last person who @p'd you.\n"
+ "@mr - Sends some text to the last person who @m'd you.\n"
+ "@retell - Sends some text to the last person you @tell'd.\n"
+ "@say - Says some text to everyone around you."
+ "@tell - Sends a private message to another character.\n"
+ "@a - Sends a broadcast to your Allegiance.\n"
+ "@c - Sends a broadcast to your Co-vassals.\n"
+ "@m - Sends a broadcast to your Monarch.\n"
+ "@p - Sends a broadcast to your Patron.\n"
+ "@v - Sends a broadcast to your Vassals.\n"
+ "@f - Sends a broadcast to your Fellowship.\n"
+ "@afk - Set your away-from-keyboard status.\n",
RetailCommandHelpTable.ChattingGroupDetail);
Assert.True(RetailCommandHelpTable.TryGetHelpText("chatting", out string viaLookup));
Assert.Equal(RetailCommandHelpTable.ChattingGroupDetail, viaLookup);
Assert.DoesNotContain("UNVERIFIED", viaLookup);
Assert.DoesNotContain("not yet extracted verbatim", viaLookup);
}
[Fact]
public void CommandsGroup_IsCompleteVerbatimListing_ConcatenatesEveryOtherGroup()
{
// HelpAllGroup's Detail branch: HelpAllegiancesGroup(Detail), the
// same 6-line broadcast block, HelpChattingGroup(Detail),
// HelpDeathGroup(Detail), emote+emotes, fillcomps, saveui(short),
// loadui(short), saveui(short) AGAIN, loadui(short) AGAIN
// (a CONFIRMED retail duplicate), lockui, friends(short), the
// house one-liner, squelch+unsquelch+messagetypes, then
// HelpStatusGroup(Detail), HelpTextGroup(Detail).
string expected =
RetailCommandHelpTable.AllegiancesGroupDetail
+ RetailCommandHelpTable.ChannelsGroupDetail
+ RetailCommandHelpTable.ChattingGroupDetail
+ RetailCommandHelpTable.DeathGroupDetail
+ "@emote - Performs a text emote.\n@emotes - Lists all standard emotes.\n"
+ "@fillcomps - Helps you buy components in bulk.\n"
+ "@saveui <filename> - Saves the current user interface.\n"
+ "@loadui <filename> - Loads a previously saved user interface.\n"
+ "@saveui <filename> - Saves the current user interface.\n"
+ "@loadui <filename> - Loads a previously saved user interface.\n"
+ "@lockui - Toggles the locked state of the UI layout.\n"
+ "@friends - Helps you manage your friends list.\n"
+ "@house - Commands that help you manage your house, including guest and storage management.\n"
+ "@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"
+ RetailCommandHelpTable.StatusGroupDetail
+ RetailCommandHelpTable.TextGroupDetail;
Assert.Equal(RetailCommandHelpTable.CommandsGroupDetail, expected);
Assert.True(RetailCommandHelpTable.TryGetHelpText("commands", out string viaLookup));
Assert.Equal(RetailCommandHelpTable.CommandsGroupDetail, viaLookup);
Assert.DoesNotContain("UNVERIFIED", viaLookup);
Assert.DoesNotContain("not yet extracted", viaLookup);
}
// Campaign CH user-gate round 2, item 3: 7 of the ~35 channel
// one-liners are now verbatim retail text (HelpTurbineChat_Xxx,
// acclient_2013_pseudo_c.txt:387294-387411, addresses
// 0x577620-0x577850) -- the "Also: @alias" suffix is retail's own text.
[Theory]
[InlineData("a", "@a - Sends a message to your Allegiance. Also: @guild, @gu")]
[InlineData("guild", "@a - Sends a message to your Allegiance. Also: @guild, @gu")]
[InlineData("general", "@general - Sends a message to the global General chat channel. Also: @cg")]
[InlineData("trade", "@trade - Sends a message to the global Trade chat channel. Also: @ct")]
[InlineData("lfg", "@lfg - Sends a message to the global Looking For Group (LFG) chat channel. Also: @clfg")]
[InlineData("roleplay", "@roleplay - Sends a message to the global Roleplay chat channel. Also: @crp")]
[InlineData("society", "@society - Sends a message to the your Society chat channel. Also: @soc")]
[InlineData("olthoi", "@olthoi - If you are an Olthoi, sends a message to the global Olthoi chat channel. Also: @o")]
public void ChannelVerb_VerbatimOneLiner_MatchesExactly(string verb, string expected)
{
Assert.True(RetailCommandHelpTable.TryGetHelpText(verb, out string text));
Assert.Equal(expected, text);
}
// The fellowship/monarch/patron/vassals/covassal family still routes
// through the unresolved HelpStupidChannelHack pooled-string mechanism
// (the "fvpca" BN artifact) and remains an honest acdream summary, not
// a claimed-verbatim string. Pinned so a future accidental "upgrade" to
// a fabricated retail-looking string doesn't slip in unnoticed.
[Theory]
[InlineData("f", "Sends text to your Fellowship channel.")]
[InlineData("monarch", "Sends text to your Monarch.")]
[InlineData("patron", "Sends text to your Patron.")]
[InlineData("vassal", "Sends text to your Vassals.")]
[InlineData("covassal", "Sends text to your Co-vassals.")]
public void ChannelVerb_StillAcdreamSummary_UnresolvedChannelHackFamily(
string verb, string expected)
{
Assert.True(RetailCommandHelpTable.TryGetHelpText(verb, out string text));
Assert.Equal(expected, text);
}
// ── Campaign CH user-gate round 3 (2026-08-10) ──────────────────────
// The round 2 pass extracted the individual STRINGS byte-exact; this
// round traces DoHelp's complete PRINT SEQUENCE — see the class
// remarks. These pin the newly-swept literals byte-exact against
// tools/pdb-extract/sweep_weenie_strings.py's output.
[Fact]
public void HelpPrefixNote_HasRetailsLeadingAndTrailingBlankLines()
{
// acclient_2013_pseudo_c.txt:394980 (data_0x7e11e0) -- the original
// round-2 extraction dropped the leading "\n" and the trailing
// "\n\n" that are part of retail's own literal.
Assert.Equal(
"\nNote: You may substitute a forward slash (/) for the at symbol (@).\n\n",
RetailCommandHelpTable.HelpPrefixNote);
}
[Fact]
public void ForMoreInformationPrefix_KeepsRetailsUnsubstitutedPlaceholderVerbatim()
{
// acclient_2013_pseudo_c.txt:395087 (data_0x7e1178) -- "<command>"
// is retail's own literal text, not a format placeholder acdream
// failed to substitute; the decomp shows no sprintf/substitution
// call between this literal and its use.
Assert.Equal(
"For more information, type @help <command>.\n",
RetailCommandHelpTable.ForMoreInformationPrefix);
}
[Fact]
public void UnknownCommand_MatchesRetailsExactFallbackText()
{
Assert.Equal("Unknown command", RetailCommandHelpTable.UnknownCommand);
}
[Fact]
public void AvailableHelpListing_MatchesDoHelpsCompleteThirteenItemSequence()
{
// acclient_2013_pseudo_c.txt:395091-395249 (DoHelp bare-arg "else"
// branch), swept whole via sweep_weenie_strings.py --range
// 0x57f9e0 0x57fe7e --ascii-only against the PDB-paired
// C:\Users\erikn\Downloads\acclient.exe (verified MATCH). Exact
// source order: header, then allegiances/channels/chatting/death/
// emote/fillcomps/friends/house/squelch/status/text/commands.
Assert.Equal(
"Available help:\n"
+ "@help allegiances - Commands to help you deal with your Allegiance.\n"
+ "@help channels - How to communicate with people in your allegiance or fellowship.\n"
+ "@help chatting - How to chat publically and privately.\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"
+ "@help commands - Lists all commands.\n",
RetailCommandHelpTable.AvailableHelpListing);
}
[Fact]
public void DeathGroup_ThroughRouter_PrintsRetailsCompleteTwoEntryShape()
{
// Campaign CH user-gate round 3, finding (c): "/help death"'s
// CONTENT was already byte-exact (round 2); what was still wrong
// was the SHAPE. Retail's DoHelp prints two scroll entries for any
// resolved verb: HelpPrefixNote, then ForMoreInformationPrefix
// concatenated directly onto the verb's own Detail text (no blank
// line, no third entry). Exercised at the router level (not just
// the table) so this is the actual /help death user experience,
// not merely the table's stored string.
var log = new AcDream.Core.Chat.ChatLog();
var vm = new ChatVM(log, displayLimit: 50);
var bus = new RecordingCommandBus();
var outcome = ChatCommandRouter.Submit(
"/help death", vm, bus, ChatChannelKind.Say);
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
var entries = log.Snapshot();
Assert.Equal(2, entries.Length);
Assert.Equal(RetailCommandHelpTable.HelpPrefixNote, entries[0].Text);
Assert.Equal(
RetailCommandHelpTable.ForMoreInformationPrefix
+ RetailCommandHelpTable.DeathGroupDetail,
entries[1].Text);
}
// ── Consolidated-review round (2026-08-10), SHOULD-FIX 1 ────────────
// Catalog leaf-verb Detail extraction: /help die and /help lifestone
// pinned byte-exact through the router (the actual user-visible
// outcome, not just the table's stored string — same pattern as
// DeathGroup_ThroughRouter_PrintsRetailsCompleteTwoEntryShape above),
// plus a coverage-count assertion so a future extraction pass (or an
// accidental regression that silently drops an override) is caught.
[Fact]
public void HelpDie_ThroughRouter_PrintsRetailsExactDetailText()
{
// HelpDie @0x005784a0: Summary @0x005784ad ("Kills your character
// and leaves a corpse..."), Detail @0x005784b4 (the long paragraph
// below) -- DoHelp always calls Detail_HelpType(2) for a resolved
// verb, never Summary. Byte-swept against the PDB-paired
// C:\Users\erikn\Downloads\acclient.exe (verified MATCH).
var log = new AcDream.Core.Chat.ChatLog();
var vm = new ChatVM(log, displayLimit: 50);
var bus = new RecordingCommandBus();
var outcome = ChatCommandRouter.Submit(
"/help die", vm, bus, ChatChannelKind.Say);
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
var entries = log.Snapshot();
Assert.Equal(2, entries.Length);
Assert.Equal(RetailCommandHelpTable.HelpPrefixNote, entries[0].Text);
Assert.Equal(
RetailCommandHelpTable.ForMoreInformationPrefix
+ RetailCommandHelpTable.DieDetail,
entries[1].Text);
Assert.Equal(
"@die - If you wish to kill your character and leave a corpse, "
+ "you may use the @die command. This will result in your "
+ "character's death, you will leave behind a corpse with some "
+ "of your items, and you will appear at your lifestone. If "
+ "you wish to travel to your lifestone without leaving behind "
+ "a corpse, you may use the @lifestone command.\n",
RetailCommandHelpTable.DieDetail);
}
[Fact]
public void HelpLifestone_ThroughRouter_PrintsRetailsExactDetailText()
{
// HelpLifestone @0x00578500 is UNCONDITIONAL (no Summary/Detail
// branch at all) -- the same string prints for every HelpType,
// confirmed by reading the decompiled body directly (no if/else).
var log = new AcDream.Core.Chat.ChatLog();
var vm = new ChatVM(log, displayLimit: 50);
var bus = new RecordingCommandBus();
var outcome = ChatCommandRouter.Submit(
"/help lifestone", vm, bus, ChatChannelKind.Say);
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
var entries = log.Snapshot();
Assert.Equal(2, entries.Length);
Assert.Equal(RetailCommandHelpTable.HelpPrefixNote, entries[0].Text);
Assert.Equal(
RetailCommandHelpTable.ForMoreInformationPrefix
+ RetailCommandHelpTable.LifestoneDetail,
entries[1].Text);
Assert.Equal(
"@lifestone - Returns you to the last lifestone you used without killing you.\n",
RetailCommandHelpTable.LifestoneDetail);
}
[Fact]
public void HelpAlh_RoutesToTheSameHelpAllegianceTextAsHelpAllegiance()
{
// alh/ah are registered with HelpAllegiance as their OWN help
// pointer (confirmed via InitializeCommands's field-store
// registration pattern, not the CmdHashData-constructor pattern) --
// the exact same function "allegiance"/"all" use, so /help alh must
// show byte-identical text to /help allegiance, not a separate
// (possibly stale) copy.
Assert.True(RetailCommandHelpTable.TryGetCatalogVerbDetailText(
"alh", out string alhText));
Assert.Equal(RetailCommandHelpTable.AllegianceOverview, alhText);
}
[Theory]
[InlineData("index")]
[InlineData("clist")]
[InlineData("on")]
[InlineData("off")]
public void ConfirmedNullHelpVerb_ThroughRouter_ShowsUnknownCommandNotCatalogSummary(
string verb)
{
// index/clist/on/off are registered in InitializeCommands with a
// plain `int32_t var_3c_NN = 0;` local -- no preceding Help-pointer
// assignment, unlike every extracted verb above -- confirming a
// genuinely NULL help function pointer. Retail's own DoHelp skips
// its help-callback branch entirely for these and falls to the
// SAME "Unknown command" 0x1A text an unregistered verb gets, even
// though the verb dispatches fine for ordinary (non-help) use.
// Showing the catalog's own invented summary here would be
// retail-inaccurate.
var log = new AcDream.Core.Chat.ChatLog();
var vm = new ChatVM(log, displayLimit: 50);
var bus = new RecordingCommandBus();
var outcome = ChatCommandRouter.Submit(
$"/help {verb}", vm, bus, ChatChannelKind.Say);
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
var entries = log.Snapshot();
Assert.Single(entries);
Assert.Equal(RetailCommandHelpTable.UnknownCommand, entries[0].Text);
Assert.Equal(
(uint)AcDream.Core.Chat.RetailLogTextType.ClientLocal,
entries[0].LogTextType);
}
[Fact]
public void CatalogLeafVerbCoverage_ExtractedVsConfirmedNullVsUnverified_MatchesConsolidatedReviewCount()
{
// Coverage-count assertion (SHOULD-FIX 1's explicit acceptance
// criterion): dedupe RetailClientCommandCatalog.KnownVerbs down to
// one entry per distinct Definition via its (unique-per-Definition)
// catalog summary text, excluding the two meta-dispatchers
// ("house"/"hou" and "allegiance"/"all" resolve subcommand-by-
// subcommand, not as a single leaf Definition), then classify each
// distinct Definition into exactly one bucket. A future extraction
// pass should bump ExtractedCount down UnverifiedCount and update
// this pin in the same commit; an accidental regression that drops
// an override without updating CatalogVerbsWithNoRetailHelp would
// fail this test instead of silently reverting to a summary.
var seenSummaries = new HashSet<string>(StringComparer.Ordinal);
int extractedCount = 0;
int confirmedNullCount = 0;
int unverifiedCount = 0;
foreach (string verb in RetailClientCommandCatalog.KnownVerbs)
{
if (verb is "house" or "hou" or "allegiance" or "all")
continue;
Assert.True(
RetailClientCommandCatalog.TryGetHelpText(verb, out string summary),
$"catalog verb '{verb}' has no summary text");
if (!seenSummaries.Add(summary))
continue; // an alias of a Definition already counted
bool confirmedNull = RetailCommandHelpTable.CatalogVerbsWithNoRetailHelp
.Contains(verb);
bool extracted = RetailCommandHelpTable.TryGetCatalogVerbDetailText(
verb, out _);
// A verb must never be BOTH confirmed-null AND carry an
// extracted override -- that would mean ChatCommandRouter's
// ordering (confirmed-null checked first) is hiding a real
// Detail text, or the two tables disagree about the same verb.
Assert.False(
confirmedNull && extracted,
$"'{verb}' is both confirmed-null and has an extracted Detail override");
if (confirmedNull)
confirmedNullCount++;
else if (extracted)
extractedCount++;
else
unverifiedCount++;
}
// Measured, not hand-counted -- 47 distinct leaf Definitions total
// (squelch/unsquelch and friends/friends_add/friends_remove each
// count separately here since they are DISTINCT Definitions with
// their own summary text, even though several share ONE extracted
// Detail text -- see the CmdHashData note in RetailCommandHelpTable's
// class remarks). Round 4 (2026-08-10): messagetypes moved from
// unverified to extracted (its real live-construction is now
// ported -- see RetailCommandHelpTable.MessageTypesDetail) --
// 43/4/0, zero remaining unverified leaf verbs.
Assert.Equal(43, extractedCount);
Assert.Equal(4, confirmedNullCount);
Assert.Equal(0, unverifiedCount);
}
// ── Round 4 (2026-08-10): no user-visible meta-markers, anywhere ────
// Honesty markers ("IMPLEMENTED", "NOT YET IMPLEMENTED", "UNVERIFIED",
// "has not yet extracted") belong in code comments/docs/the divergence
// register per CLAUDE.md -- never in a string a player can read.
public static IEnumerable<object[]> AllUserVisibleStrings()
{
yield return new object[] { nameof(RetailCommandHelpTable.Day), RetailCommandHelpTable.Day };
yield return new object[] { nameof(RetailCommandHelpTable.Log), RetailCommandHelpTable.Log };
yield return new object[] { nameof(RetailCommandHelpTable.Render), RetailCommandHelpTable.Render };
yield return new object[] { nameof(RetailCommandHelpTable.Motd), RetailCommandHelpTable.Motd };
yield return new object[]
{
nameof(RetailCommandHelpTable.AllegianceOverview), RetailCommandHelpTable.AllegianceOverview,
};
yield return new object[]
{
nameof(RetailCommandHelpTable.HouseOverview), RetailCommandHelpTable.HouseOverview,
};
yield return new object[]
{
nameof(RetailCommandHelpTable.ChannelsGroupDetail), RetailCommandHelpTable.ChannelsGroupDetail,
};
yield return new object[]
{
nameof(RetailCommandHelpTable.ChattingGroupDetail), RetailCommandHelpTable.ChattingGroupDetail,
};
yield return new object[]
{
nameof(RetailCommandHelpTable.CommandsGroupDetail), RetailCommandHelpTable.CommandsGroupDetail,
};
yield return new object[]
{
nameof(RetailCommandHelpTable.MessageTypesDetail), RetailCommandHelpTable.MessageTypesDetail,
};
yield return new object[]
{
nameof(RetailCommandHelpTable.AvailableHelpListing), RetailCommandHelpTable.AvailableHelpListing,
};
}
[Theory]
[MemberData(nameof(AllUserVisibleStrings))]
public void UserVisibleString_CarriesNoHonestyMarker(string _, string text)
{
Assert.DoesNotContain("IMPLEMENTED", text);
Assert.DoesNotContain("UNVERIFIED", text);
Assert.DoesNotContain("has not yet extracted", text);
Assert.DoesNotContain("acdream has not", text);
Assert.DoesNotContain("not yet extracted", text);
Assert.DoesNotContain("NOT YET", text);
}
[Fact]
public void AllegianceOverview_NoBracketAnnotations_MatchesPristineRetailData()
{
// Read directly off data_7dfb58's own consolidated dump
// (acclient_2013_pseudo_c.txt:1031210-1031230) -- BN already fully
// decodes this block, no vtable-slot artifact to work around.
Assert.Contains("Also: @ab\n", RetailCommandHelpTable.AllegianceOverview);
Assert.DoesNotContain("[IMPLEMENTED", RetailCommandHelpTable.AllegianceOverview);
Assert.Contains(
"@allegiance hometown - Recalls you to your allegiance bindstone, if your allegiance has tied to one.\n",
RetailCommandHelpTable.AllegianceOverview);
Assert.DoesNotContain("Subcommands NOT marked", RetailCommandHelpTable.AllegianceOverview);
}
[Fact]
public void HouseOverview_NoBracketAnnotations_MatchesPristineRetailData()
{
// Read directly off data_7dd3d0's own consolidated dump
// (acclient_2013_pseudo_c.txt:1031017-1031037).
Assert.Contains(
"@house available - See @hslist\n",
RetailCommandHelpTable.HouseOverview);
Assert.Contains(
"@house recall - Teleports you to your house.\n",
RetailCommandHelpTable.HouseOverview);
Assert.DoesNotContain("[IMPLEMENTED", RetailCommandHelpTable.HouseOverview);
Assert.DoesNotContain("Subcommands NOT marked", RetailCommandHelpTable.HouseOverview);
}
[Fact]
public void Log_IsCompleteThreeParagraphRetailText()
{
// char const data_7df7f8[0x2ca] -- the prior extraction had
// silently dropped the middle sentence and both trailing
// paragraphs in addition to carrying the meta tail.
Assert.Equal(
"@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",
RetailCommandHelpTable.Log);
}
[Fact]
public void Render_IsRealRetailUsageText_NotAcdreamDescription()
{
// char const render_option_usage[0x98] @ 0x0079B878 -- the prior
// text ("Forwards to the client's render-option surface…") was
// entirely acdream-authored; this is the real retail literal.
Assert.Equal(
"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",
RetailCommandHelpTable.Render);
}
[Fact]
public void Motd_IsCompleteThreeLineRetailText_PreservesRetailTypo()
{
// char const data_7da300[0xdb] -- retail's own "clear-" (no space
// before the dash) is byte-confirmed, not an acdream typo.
Assert.Equal(
"@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",
RetailCommandHelpTable.Motd);
}
[Fact]
public void MessageTypesDetail_IsRealConstructionFromLegalChannelWhitelist()
{
// LogTextTypeEnumMapper::IsLegalChannel @0x006AFF40 (14-id
// whitelist) + LogTextTypeEnumMapper::LogTextTypeToString
// @0x006AFF90 (per-id name) + the ", " join (data_0x794334) +
// GetListofSquelchChannels' own "Squelch channels are as
// follows:\n %s\n" wrap (data_0x7e2fb8), all swept/read directly.
Assert.Equal(
"Squelch channels are as follows:\n"
+ " Speech, Tell, Combat, Magic, Emote, Appraisal, Spellcasting, Allegiance, Fellowship, Combat_Enemy, Combat_Self, Recall, Craft, Salvaging\n",
RetailCommandHelpTable.MessageTypesDetail);
Assert.True(RetailCommandHelpTable.TryGetCatalogVerbDetailText(
"messagetypes", out string messagetypes));
Assert.Equal(RetailCommandHelpTable.MessageTypesDetail, messagetypes);
foreach (string alias in new[] { "message_types", "msgtypes", "msg_types" })
{
Assert.True(RetailCommandHelpTable.TryGetCatalogVerbDetailText(alias, out string aliasText));
Assert.Equal(RetailCommandHelpTable.MessageTypesDetail, aliasText);
}
}
[Fact]
public void HelpMessageTypes_ThroughRouter_PrintsRetailsExactDetailText()
{
var log = new AcDream.Core.Chat.ChatLog();
var vm = new ChatVM(log, displayLimit: 50);
var bus = new RecordingCommandBus();
var outcome = ChatCommandRouter.Submit(
"/help messagetypes", vm, bus, ChatChannelKind.Say);
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
var entries = log.Snapshot();
Assert.Equal(2, entries.Length);
Assert.Equal(RetailCommandHelpTable.HelpPrefixNote, entries[0].Text);
Assert.Equal(
RetailCommandHelpTable.ForMoreInformationPrefix
+ RetailCommandHelpTable.MessageTypesDetail,
entries[1].Text);
}
private sealed class RecordingCommandBus : ICommandBus
{
public List<object> Published { get; } = new();
public void Publish<T>(T command) where T : notnull => Published.Add(command);
}
}