using AcDream.UI.Abstractions.Panels.Chat; using Xunit; namespace AcDream.UI.Abstractions.Tests.Panels.Chat; /// /// Campaign CH user-gate round 2, item 3 (2026-08-09): pins a sample of /// 's extracted strings byte-exact /// against the values recovered from the PDB-paired /// C:\Users\erikn\Downloads\acclient.exe via /// tools/pdb-extract/sweep_weenie_strings.py --ascii-only (generalized /// this round for narrow PStringBase<char> literals — see the /// script's own docstring). Covers all four COMPLETE group listings /// (death/status/text/allegiances — /help death 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. /// 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 - 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); } [Theory] [InlineData("channels")] [InlineData("commands")] public void UnresolvedGroups_ContainVerbatimSummaryPlusHonestUnverifiedNote_NoMetaFabrication( string verb) { Assert.True(RetailCommandHelpTable.TryGetHelpText(verb, out string text)); // The old sentinel this round retired -- must never come back. Assert.DoesNotContain("has not yet extracted", text); Assert.Contains("UNVERIFIED", text); Assert.Contains("HelpStupidChannelHack @0x0056f290", text); } [Fact] public void ChannelsGroup_SummaryLineIsVerbatim() { Assert.True(RetailCommandHelpTable.TryGetHelpText("channels", out string text)); Assert.StartsWith( "@help channels - How to communicate with people in your allegiance or fellowship.", text); } [Fact] public void ChattingGroup_SevenOfEightEntriesAreVerbatim_FifthEntryMarkedUnverified() { Assert.True(RetailCommandHelpTable.TryGetHelpText("chatting", out string text)); Assert.Contains( "@chat - Sets whether or not you receive normal chat.\n", text); Assert.Contains( "@notell - Sets whether or not you receive @tell's.\n", text); Assert.Contains( "@reply - Sends some text to the last person who @tell'd you.\n", text); Assert.Contains( "@retell - Sends some text to the last person you @tell'd.\n", text); Assert.Contains( "@say - Says some text to everyone around you.\n", text); Assert.Contains( "@tell - Sends a private message to another character.\n", text); Assert.Contains( "@afk - Set your away-from-keyboard status.\n", text); Assert.Contains("HelpStupidChannelHack @0x0056f290", text); } // 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) -- "" // 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 .\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(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). Assert.Equal(42, extractedCount); Assert.Equal(4, confirmedNullCount); Assert.Equal(1, unverifiedCount); } private sealed class RecordingCommandBus : ICommandBus { public List Published { get; } = new(); public void Publish(T command) where T : notnull => Published.Add(command); } }