feat(chat): Campaign CH slice CH4 — command registry completion
Brings acdream's / and @ command parsing to parity with the complete
retail registry (130 registered verbs + 22 unregistered GetChannelID
fallback tags = 152 client-parsed verbs), per
docs/research/2026-08-09-chat-retail-command-registry.md.
Parser semantics (retail OnChatCommand/DoCommand):
- : and ; rewrite to "@emote <rest>" before dispatch.
- Verb trailing-comma trim ("@f, hi" == "@f hi") applied at every
verb-lookup site in the catalog and the parser.
- @tell/aliases split the target on the FIRST COMMA, not the first
whitespace token, so multi-word names work ("@tell Aunt Agatha, hi").
- The 22 unregistered GM/faction channel tags (admin, sentinel,
celestialhand, ...) now broadcast for real via a new
RetailChannelTagTable + SendRawChannelCmd bypass, reusing the existing
BuildChatChannel wire builder.
Binding corrections:
- /g, /group, /party -> Fellowship (0x800), not General.
- /rp -> reply alias (retail's own help text confirms "@r or @rp"), not
Roleplay; /role (an acdream invention) deleted.
- /allegiance, /all -> the allegiance management command
(RetailClientCommandCatalog), not a channel verb.
- /house no longer swallows unrecognized subcommands with a local usage
error; they now correctly fall through to ACE.
- @mr/@pr pinned as permanently non-executable (retail registers them
with a null function pointer).
New verbs with real local execution: endurance, speaker, title (silent,
AP-182), chat, notell, join, leave, permit, hslist, index, clist, on,
off, alh/ah (+ "@allegiance hometown"/"ho"), "@allegiance info",
"@house abandon"; a missing-alias sweep across pkl/hou/message_types/
msgtypes/msg_types/rt/send/whisper/w/vassal/covassal/co-vassals/c/
fellows/group/party/guild/gu/cg/ct/clfg/crp/soc/o; the non-retail
inventions gen/cv/lookingforgroup/tr/role/h are deleted. New Core.Net
wire builders (IndexChannels, ListChannels, AddChannel, RemoveChannel,
RecallAllegianceHometown, AllegianceInfoRequest, ListAvailableHouses,
AddPlayerPermission, RemovePlayerPermission, AbandonHouse) are all
parameterless or single-field payloads cross-checked against ACE's
GameAction readers, not guessed.
Deferred (filed as #360/#361/#362, register rows TS-68/TS-69/TS-70):
the ~22 remaining allegiance/house subcommands + standalone @motd
(largest single item, needs its own slice per the doc), the three
still-inert pure-local commands (day/log/render), and the inbound
GameEvent responses for the new outbound requests. All correctly fall
through to ACE server-passthrough rather than being silently swallowed
or faking success.
RetailCommandRegistryConformanceTests pins the complete 152-verb
registry against production: every verb resolves through exactly one
production surface if Implemented, through none if HelpOnly/
ServerPassthrough, and two reverse-direction tests fail the build if
RetailClientCommandCatalog or ChatInputParser ever claims a verb
outside this registry again. Final tally: 138 Implemented / 5
ServerPassthrough / 9 HelpOnly = 152.
Release suite: 12,190 passed / 4 skipped / 0 failed (up from CH3's
11,964/4/0).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
9247d5d5b5
commit
090825e703
23 changed files with 2069 additions and 97 deletions
|
|
@ -41,4 +41,38 @@ public enum ClientCommandId
|
|||
Unfilter,
|
||||
ListMessageTypes,
|
||||
FillComponents,
|
||||
|
||||
// Campaign CH slice CH4 (2026-08-09): command-registry completion.
|
||||
/// <summary>@endurance — fixed help paragraph about the Endurance attribute.</summary>
|
||||
Endurance,
|
||||
/// <summary>@speaker — fixed deprecation notice ("see @allegiance officer").</summary>
|
||||
Speaker,
|
||||
/// <summary>@title <text> — sets the popup chat window's title (local state only; no title-bar chrome yet, AP-182).</summary>
|
||||
SetChatTitle,
|
||||
/// <summary>@chat on|off — global Speech squelch toggle (message type 2).</summary>
|
||||
ChatToggle,
|
||||
/// <summary>@notell on|off — global Tell squelch toggle (message type 3).</summary>
|
||||
NoTellToggle,
|
||||
/// <summary>@join <channel tag> — sets the matching PlayerModule::Hear*Chat option on.</summary>
|
||||
JoinChannel,
|
||||
/// <summary>@leave <channel tag> — clears the matching PlayerModule::Hear*Chat option.</summary>
|
||||
LeaveChannel,
|
||||
/// <summary>@permit add|remove <name> — corpse-looting permission management.</summary>
|
||||
Permit,
|
||||
/// <summary>@hslist <type> / "@house available" — list houses available for purchase.</summary>
|
||||
HouseAvailableList,
|
||||
/// <summary>@index — request the channel index (admin/advocate/PSR only server-side).</summary>
|
||||
IndexChannels,
|
||||
/// <summary>@clist <channel> — request the member list of a channel.</summary>
|
||||
ListChannel,
|
||||
/// <summary>@on <channel> — join a GM/faction channel.</summary>
|
||||
OnChannel,
|
||||
/// <summary>@off <channel> — leave a GM/faction channel.</summary>
|
||||
OffChannel,
|
||||
/// <summary>@alh / @ah / "@allegiance hometown" / "@allegiance ho" — recall to the allegiance bindstone.</summary>
|
||||
AllegianceHometown,
|
||||
/// <summary>"@allegiance info [name]" — request allegiance member info.</summary>
|
||||
AllegianceInfo,
|
||||
/// <summary>"@house abandon" — abandon the character's house.</summary>
|
||||
HouseAbandon,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@ public enum SubmitOutcome { Empty, ClientHandled, UnknownCommand, Sent, Dropped
|
|||
/// and retained retail chat window route through here.
|
||||
///
|
||||
/// <para>
|
||||
/// Flow: retail client-command catalog, local presentation command,
|
||||
/// degenerate-prefix guard, explicit server command, then chat parse. Unknown
|
||||
/// Flow: emote-prefix rewrite, retail client-command catalog, local
|
||||
/// presentation command, degenerate-prefix guard, the retail
|
||||
/// <c>ChannelSystem::GetChannelID</c> fallback (unregistered GM/faction
|
||||
/// channel tags), explicit server command, then chat parse. Unknown
|
||||
/// slash/at verbs publish <see cref="SendServerCommandCmd"/> in canonical
|
||||
/// <c>@</c> form; the App host sends those through Talk, the only wire path
|
||||
/// ACE parses commands on. Prefix text with no letter verb is refused locally
|
||||
|
|
@ -32,6 +34,18 @@ public static class ChatCommandRouter
|
|||
if (trimmed.Length == 0)
|
||||
return SubmitOutcome.Empty;
|
||||
|
||||
// Retail OnChatCommand @ 0x0058144D, cases 0x0B/0x0C (':'/';'):
|
||||
// the first character is replaced with a space and the whole line
|
||||
// is prefixed with the literal "@emote" before dispatch —
|
||||
// ":waves" == ";waves" == "@emote waves". Campaign CH slice CH4
|
||||
// (2026-08-09). Both prefix characters are handled IDENTICALLY per
|
||||
// the command-registry doc (a single sentence covers both cases);
|
||||
// this is not a guess.
|
||||
if (trimmed[0] is ':' or ';')
|
||||
{
|
||||
trimmed = "@emote " + trimmed[1..];
|
||||
}
|
||||
|
||||
if (RetailClientCommandCatalog.TryMatch(trimmed, out var clientCommand))
|
||||
{
|
||||
if (!clientCommand.HasValidArguments)
|
||||
|
|
@ -59,6 +73,23 @@ public static class ChatCommandRouter
|
|||
return SubmitOutcome.UnknownCommand;
|
||||
}
|
||||
|
||||
// Campaign CH slice CH4 (2026-08-09), doc §1/§2.3: retail's
|
||||
// DoCommand falls through to DoChannelCommand @ 0x005774A0 for any
|
||||
// verb absent from the registered-command hash table. That
|
||||
// function tries ChannelSystem::GetChannelID on the verb and, on a
|
||||
// hit, broadcasts to the channel — this is how the 22 unregistered
|
||||
// GM/faction tags (@admin, @sentinel, @celestialhand, ...) work in
|
||||
// retail despite never being "registered". Verbs ChatInputParser
|
||||
// already knows (the registered legacy/Turbine channels) are
|
||||
// excluded here so they keep riding their normal
|
||||
// self-echo/Turbine-gated SendChatCmd path below — GetChannelID
|
||||
// would resolve them too, but retail never reaches this fallback
|
||||
// for a REGISTERED verb (it's intercepted by the main hash table
|
||||
// first).
|
||||
SubmitOutcome? fallbackOutcome = TryDispatchChannelFallback(trimmed, vm, bus);
|
||||
if (fallbackOutcome is { } outcome)
|
||||
return outcome;
|
||||
|
||||
if (TryBuildServerCommand(trimmed, out string serverCommand))
|
||||
{
|
||||
bus.Publish(new SendServerCommandCmd(serverCommand));
|
||||
|
|
@ -76,6 +107,39 @@ public static class ChatCommandRouter
|
|||
return SubmitOutcome.Dropped;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns null when the verb is not one of the 22 unregistered
|
||||
/// fallback channel tags (caller continues its own dispatch chain);
|
||||
/// otherwise returns the outcome to return immediately.
|
||||
/// </summary>
|
||||
private static SubmitOutcome? TryDispatchChannelFallback(string trimmed, ChatVM vm, ICommandBus bus)
|
||||
{
|
||||
if (trimmed[0] is not ('/' or '@'))
|
||||
return null;
|
||||
|
||||
string verb = ChatInputParser.GetVerbToken(trimmed);
|
||||
string normalizedChatVerb = "/" + verb[1..].TrimEnd(',');
|
||||
if (ChatInputParser.IsKnownVerb(normalizedChatVerb))
|
||||
return null; // registered verb — handled by the normal channel path.
|
||||
|
||||
string tag = normalizedChatVerb[1..];
|
||||
if (!RetailChannelTagTable.TryResolve(tag, out uint channelId))
|
||||
return null;
|
||||
|
||||
int separator = trimmed.IndexOfAny([' ', '\t']);
|
||||
string text = separator < 0 ? string.Empty : trimmed[(separator + 1)..].Trim();
|
||||
if (text.Length == 0)
|
||||
{
|
||||
// Retail's DoChannelCommand: "You must specify the text you
|
||||
// wish to say." — a real local error, not a passthrough.
|
||||
vm.ShowSystemMessage("You must specify the text you wish to say!");
|
||||
return SubmitOutcome.ClientHandled;
|
||||
}
|
||||
|
||||
bus.Publish(new SendRawChannelCmd(channelId, text));
|
||||
return SubmitOutcome.Sent;
|
||||
}
|
||||
|
||||
private static bool TryBuildServerCommand(string trimmed, out string command)
|
||||
{
|
||||
command = string.Empty;
|
||||
|
|
@ -93,15 +157,40 @@ public static class ChatCommandRouter
|
|||
|
||||
private static bool TryHandleLocalPresentationCommand(string trimmed, ChatVM vm)
|
||||
{
|
||||
if (EqAny(trimmed, "/help", "/?", "/h", "@help", "@?", "@h"))
|
||||
if (EqAny(trimmed, "/help", "/?", "@help", "@?"))
|
||||
{
|
||||
vm.ShowSystemMessage(BuildHelpText());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Campaign CH slice CH4 (2026-08-09), Tier 2 item 15: "/help <verb>"
|
||||
// / "@help <verb>" — retail's DoHelp @ 0x0057F9E0 looks the verb up
|
||||
// and calls its registered help callback. Non-retail "/h" alias
|
||||
// deleted per the command-registry doc §4's removal list.
|
||||
if (StartsWithAny(trimmed, "/help ", "@help ", "/? ", "@? "))
|
||||
{
|
||||
string verb = trimmed[(trimmed.IndexOf(' ') + 1)..].Trim();
|
||||
vm.ShowSystemMessage(BuildVerbHelpText(verb));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string BuildVerbHelpText(string verb)
|
||||
{
|
||||
if (verb.Length == 0)
|
||||
return BuildHelpText();
|
||||
|
||||
string normalized = verb.TrimStart('/', '@');
|
||||
if (RetailClientCommandCatalog.TryGetHelpText(normalized, out string catalogText))
|
||||
return catalogText;
|
||||
if (RetailCommandHelpTable.TryGetHelpText(normalized, out string tableText))
|
||||
return tableText;
|
||||
|
||||
return $"No help available for '{verb}'.";
|
||||
}
|
||||
|
||||
private static bool EqAny(string value, params string[] options)
|
||||
{
|
||||
for (int i = 0; i < options.Length; i++)
|
||||
|
|
@ -113,13 +202,24 @@ public static class ChatCommandRouter
|
|||
return false;
|
||||
}
|
||||
|
||||
private static bool StartsWithAny(string value, params string[] options)
|
||||
{
|
||||
for (int i = 0; i < options.Length; i++)
|
||||
{
|
||||
if (value.StartsWith(options[i], StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string BuildHelpText() =>
|
||||
"Note: / and @ are equivalent prefixes.\n" +
|
||||
"Chat: /say (default), /tell <name>, /reply, /retell\n" +
|
||||
"Channels: /general /trade /fellowship /allegiance\n" +
|
||||
$"{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 (this) /clear /framerate /loc\n" +
|
||||
"Client: /help [command] (this) /clear /framerate /loc\n" +
|
||||
$" {RetailClientCommandCatalog.BuildHelpText()}\n" +
|
||||
"Server: type @acehelp or @acecommands for ACE's full list.";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,29 +39,61 @@ public static class ChatInputParser
|
|||
// Alias tables. Order matters only for error messages — verb
|
||||
// matching is exact-token, not prefix.
|
||||
private static readonly string[] SayAliases = { "/say", "/s" };
|
||||
private static readonly string[] TellAliases = { "/tell", "/t" };
|
||||
private static readonly string[] ReplyAliases = { "/reply", "/r" };
|
||||
// Campaign CH slice CH4 (2026-08-09): retail's DoTell @0x00577E40
|
||||
// registers "tell" under FOUR verb strings — tell/t/send/whisper/w —
|
||||
// per the command-registry doc §2.2.
|
||||
private static readonly string[] TellAliases = { "/tell", "/t", "/send", "/whisper", "/w" };
|
||||
// Campaign CH slice CH4 (2026-08-09): retail's DoReply @0x00577910
|
||||
// registers "reply" under THREE verb strings — reply/r/rp — confirmed
|
||||
// by retail's own help text ("You may also use @r or @rp",
|
||||
// acclient_2013_pseudo_c.txt:1030742). "/rp" moved here from the
|
||||
// Roleplay channel table below, where it was a Tier-1 correctness bug
|
||||
// (a private reply becoming a global Roleplay broadcast).
|
||||
private static readonly string[] ReplyAliases = { "/reply", "/r", "/rp" };
|
||||
// Phase J Tier 2: /retell <msg> — resend to last person YOU tell'd.
|
||||
// Mirrors retail's @retell. Distinct from /reply which targets the
|
||||
// last person who tell'd US.
|
||||
private static readonly string[] RetellAliases = { "/retell" };
|
||||
// last person who tell'd US. Campaign CH slice CH4 added the "/rt"
|
||||
// alias — retail's DoReTell registers both "retell" and "rt".
|
||||
private static readonly string[] RetellAliases = { "/retell", "/rt" };
|
||||
|
||||
// Channel aliases. Each maps a single verb token to a channel kind.
|
||||
// The same list drives both the verb test and the prefix-strip.
|
||||
// Long-form aliases mirror retail muscle memory (e.g. "/allegiance"
|
||||
// for "/a", "/patron" for "/p"). Phase J added the long forms after
|
||||
// a 2026-04-25 live session showed "/patron hello" falling through
|
||||
// as plain Say with the literal "/patron " prefix.
|
||||
// Long-form aliases mirror retail muscle memory (e.g. "/patron" for
|
||||
// "/p"). Phase J added the long forms after a 2026-04-25 live session
|
||||
// showed "/patron hello" falling through as plain Say with the literal
|
||||
// "/patron " prefix.
|
||||
//
|
||||
// Campaign CH slice CH4 (2026-08-09) reconciled this table against the
|
||||
// retail command-registry doc §2.3/§2.4 (Tier 1 fixes #1-3, alias
|
||||
// sweep #16):
|
||||
// - "/g" moves from General to FELLOWSHIP (retail: g/group/party/
|
||||
// fellow/fellows/fellowship all bind Fellowship, 0x800). Sending
|
||||
// fellowship chatter to General was a live correctness bug.
|
||||
// - "/allegiance" is DELETED — retail's allegiance/all is the
|
||||
// allegiance MANAGEMENT COMMAND (RetailClientCommandCatalog),
|
||||
// not a channel verb. The channel-send verbs are a/ab/guild/gu.
|
||||
// - "/gen", "/cv", "/lookingforgroup", "/tr", "/role" are DELETED —
|
||||
// none are registered retail verbs (doc §4 "candidates for
|
||||
// removal").
|
||||
// - Missing retail aliases added: guild, gu (Allegiance Turbine);
|
||||
// co-vassals, covassal, c (CoVassals); vassal (Vassals); cg
|
||||
// (General Turbine); ct (Trade Turbine); crp (Roleplay Turbine);
|
||||
// clfg (LFG Turbine); soc (Society Turbine); o (Olthoi Turbine);
|
||||
// fellows, group, party (Fellowship).
|
||||
private static readonly (string Verb, ChatChannelKind Channel)[] ChannelVerbs =
|
||||
{
|
||||
("/g", ChatChannelKind.General),
|
||||
("/general", ChatChannelKind.General),
|
||||
("/gen", ChatChannelKind.General),
|
||||
("/cg", ChatChannelKind.General),
|
||||
("/f", ChatChannelKind.Fellowship),
|
||||
("/fellow", ChatChannelKind.Fellowship),
|
||||
("/fellows", ChatChannelKind.Fellowship),
|
||||
("/fellowship", ChatChannelKind.Fellowship),
|
||||
("/g", ChatChannelKind.Fellowship),
|
||||
("/group", ChatChannelKind.Fellowship),
|
||||
("/party", ChatChannelKind.Fellowship),
|
||||
("/a", ChatChannelKind.Allegiance),
|
||||
("/allegiance", ChatChannelKind.Allegiance),
|
||||
("/guild", ChatChannelKind.Allegiance),
|
||||
("/gu", ChatChannelKind.Allegiance),
|
||||
// CH3 (2026-08-09): retail's @ab — DoAllegianceBroadcast, the
|
||||
// legacy 0x02000000 monarch/speaker broadcast — confirmed against
|
||||
// the retail command registry (§2.3/§2.5). "/allegiancebroadcast"
|
||||
|
|
@ -74,18 +106,22 @@ public static class ChatInputParser
|
|||
("/p", ChatChannelKind.Patron),
|
||||
("/patron", ChatChannelKind.Patron),
|
||||
("/v", ChatChannelKind.Vassals),
|
||||
("/vassal", ChatChannelKind.Vassals),
|
||||
("/vassals", ChatChannelKind.Vassals),
|
||||
("/cv", ChatChannelKind.CoVassals),
|
||||
("/c", ChatChannelKind.CoVassals),
|
||||
("/covassal", ChatChannelKind.CoVassals),
|
||||
("/covassals", ChatChannelKind.CoVassals),
|
||||
("/co-vassals", ChatChannelKind.CoVassals),
|
||||
("/lfg", ChatChannelKind.Lfg),
|
||||
("/lookingforgroup", ChatChannelKind.Lfg),
|
||||
("/clfg", ChatChannelKind.Lfg),
|
||||
("/trade", ChatChannelKind.Trade),
|
||||
("/tr", ChatChannelKind.Trade),
|
||||
("/role", ChatChannelKind.Roleplay),
|
||||
("/rp", ChatChannelKind.Roleplay),
|
||||
("/ct", ChatChannelKind.Trade),
|
||||
("/crp", ChatChannelKind.Roleplay),
|
||||
("/roleplay", ChatChannelKind.Roleplay),
|
||||
("/society", ChatChannelKind.Society),
|
||||
("/soc", ChatChannelKind.Society),
|
||||
("/olthoi", ChatChannelKind.Olthoi),
|
||||
("/o", ChatChannelKind.Olthoi),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -123,7 +159,7 @@ public static class ChatInputParser
|
|||
{
|
||||
string substituted = "/" + trimmed.Substring(1);
|
||||
string verb = ExtractVerb(substituted);
|
||||
if (AllKnownVerbs.Contains(verb))
|
||||
if (IsKnownVerb(verb))
|
||||
{
|
||||
return Parse(substituted, defaultChannel, lastTellSender, lastOutgoingTellTarget);
|
||||
}
|
||||
|
|
@ -193,10 +229,17 @@ public static class ChatInputParser
|
|||
// ── helpers ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Match holtburger's <c>parse_targeted_chat_command</c>: split on
|
||||
/// first whitespace into verb / rest, check verb against aliases,
|
||||
/// then split rest into target / message. Returns false if either
|
||||
/// the verb is wrong or target / message is empty.
|
||||
/// Match holtburger's <c>parse_targeted_chat_command</c>, corrected for
|
||||
/// retail's ACTUAL <c>@tell</c> shape (Campaign CH slice CH4,
|
||||
/// 2026-08-09): split on first whitespace into verb / rest, check verb
|
||||
/// against aliases, then split rest on the FIRST COMMA into target /
|
||||
/// message — retail's <c>DoTell @ 0x00577E40</c> requires a comma after
|
||||
/// the name ("you must put a comma after the character's name",
|
||||
/// acclient_2013_pseudo_c.txt:1030771) precisely because names can be
|
||||
/// multiple words ("@tell Aunt Agatha, hello" addresses "Aunt Agatha").
|
||||
/// Splitting on the first WHITESPACE (the old behavior) truncated
|
||||
/// multi-word names to their first token. Returns false if either the
|
||||
/// verb is wrong, there's no comma, or target / message is empty.
|
||||
/// </summary>
|
||||
private static bool TryParseTargeted(string command, string[] aliases, out string target, out string message)
|
||||
{
|
||||
|
|
@ -206,25 +249,35 @@ public static class ChatInputParser
|
|||
int firstWs = IndexOfWhitespace(command);
|
||||
if (firstWs < 0) return false;
|
||||
|
||||
var verb = command.Substring(0, firstWs);
|
||||
var verb = TrimVerbComma(command.Substring(0, firstWs));
|
||||
if (!ContainsExact(aliases, verb)) return false;
|
||||
|
||||
var rest = command.Substring(firstWs + 1).TrimStart();
|
||||
if (rest.Length == 0) return false;
|
||||
|
||||
int targetEnd = IndexOfWhitespace(rest);
|
||||
if (targetEnd < 0) return false; // target only, no message
|
||||
int commaIndex = rest.IndexOf(',');
|
||||
if (commaIndex < 0)
|
||||
{
|
||||
// No comma at all: retail's help text is explicit that one is
|
||||
// required. Fall back to the pre-CH4 whitespace split so a
|
||||
// single-word target typed without a comma ("/t Bestie hi")
|
||||
// still works — this is strictly more permissive than retail,
|
||||
// not less, and every existing single-word-target test still
|
||||
// passes. Pre-existing Phase I fix: strip trailing punctuation
|
||||
// other than comma too (":", ".", "!", "?", ";") for the same
|
||||
// "retail muscle memory" reason, now that comma itself is
|
||||
// handled by the branch above.
|
||||
int targetEnd = IndexOfWhitespace(rest);
|
||||
if (targetEnd < 0) return false; // target only, no message
|
||||
target = rest.Substring(0, targetEnd).TrimEnd(',', ';', ':', '.', '!', '?');
|
||||
message = rest.Substring(targetEnd + 1).TrimStart();
|
||||
}
|
||||
else
|
||||
{
|
||||
target = rest.Substring(0, commaIndex).TrimEnd();
|
||||
message = rest.Substring(commaIndex + 1).TrimStart();
|
||||
}
|
||||
|
||||
target = rest.Substring(0, targetEnd);
|
||||
message = rest.Substring(targetEnd + 1).TrimStart();
|
||||
// Phase I (post-launch fix): retail muscle memory is
|
||||
// "/t Name, message" — comma is the separator. Our split-on-
|
||||
// whitespace pulls "Name," (with trailing comma) as the target,
|
||||
// which then 0x052B-fails on the server lookup. Strip a
|
||||
// trailing punctuation from the target so both forms work:
|
||||
// "/t Caith hi" -> target="Caith"
|
||||
// "/t Caith, hi" -> target="Caith"
|
||||
target = target.TrimEnd(',', ';', ':', '.', '!', '?');
|
||||
if (target.Length == 0 || message.Length == 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -240,7 +293,7 @@ public static class ChatInputParser
|
|||
int firstWs = IndexOfWhitespace(command);
|
||||
if (firstWs < 0) return false;
|
||||
|
||||
var verb = command.Substring(0, firstWs);
|
||||
var verb = TrimVerbComma(command.Substring(0, firstWs));
|
||||
if (!ContainsExact(aliases, verb)) return false;
|
||||
|
||||
message = command.Substring(firstWs + 1).TrimStart();
|
||||
|
|
@ -253,8 +306,9 @@ public static class ChatInputParser
|
|||
/// </summary>
|
||||
private static bool IsBareVerb(string command, string[] aliases)
|
||||
{
|
||||
string trimmedVerb = TrimVerbComma(command);
|
||||
foreach (var alias in aliases)
|
||||
if (command == alias) return true;
|
||||
if (trimmedVerb == alias) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -268,7 +322,7 @@ public static class ChatInputParser
|
|||
{
|
||||
int firstWs = IndexOfWhitespace(command);
|
||||
if (firstWs < 0) return false;
|
||||
var verb = command.Substring(0, firstWs);
|
||||
var verb = TrimVerbComma(command.Substring(0, firstWs));
|
||||
if (!ContainsExact(aliases, verb)) return false;
|
||||
|
||||
var rest = command.Substring(firstWs + 1).TrimStart();
|
||||
|
|
@ -328,9 +382,27 @@ public static class ChatInputParser
|
|||
/// need to distinguish "unknown slash command" from "known
|
||||
/// verb with bad arguments" without reproducing the alias
|
||||
/// tables. <c>@</c>-prefixed verbs need to be normalized to
|
||||
/// <c>/</c> before passing.
|
||||
/// <c>/</c> before passing. Trims a trailing comma first — retail's
|
||||
/// <c>DoCommand @ 0x0057E2E0</c> right-trims <c>','</c> off the verb
|
||||
/// token before ANY lookup (Campaign CH slice CH4, 2026-08-09), so
|
||||
/// <c>"/f,"</c> is recognized exactly like <c>"/f"</c>.
|
||||
/// </summary>
|
||||
public static bool IsKnownVerb(string verb) => AllKnownVerbs.Contains(verb);
|
||||
public static bool IsKnownVerb(string verb) => AllKnownVerbs.Contains(TrimVerbComma(verb));
|
||||
|
||||
/// <summary>
|
||||
/// Every chat-alias / channel verb this parser recognizes (with
|
||||
/// leading <c>/</c>). Used by the CH4 conformance test to enforce the
|
||||
/// ownership rule in both directions.
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<string> KnownVerbs => AllKnownVerbs;
|
||||
|
||||
/// <summary>
|
||||
/// Right-trim a trailing <c>','</c> from a verb token — retail's
|
||||
/// <c>DoCommand</c> trim-char set (<c>0x0079452C</c>, right-trim only)
|
||||
/// applied before every verb-hash-table lookup. <c>"@f, hi"</c> ≡
|
||||
/// <c>"@f hi"</c>.
|
||||
/// </summary>
|
||||
private static string TrimVerbComma(string verb) => verb.TrimEnd(',');
|
||||
|
||||
/// <summary>
|
||||
/// Pull the first whitespace-separated token (the command verb)
|
||||
|
|
|
|||
115
src/AcDream.UI.Abstractions/Panels/Chat/RetailChannelTagTable.cs
Normal file
115
src/AcDream.UI.Abstractions/Panels/Chat/RetailChannelTagTable.cs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
using System.Collections.Frozen;
|
||||
|
||||
namespace AcDream.UI.Abstractions.Panels.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Retail's <c>ChannelSystem::GetChannelID @ 0x005CF1F0</c> — every legacy
|
||||
/// <c>ChatChannel</c> bitflag tag, both the ones with a registered send verb
|
||||
/// (fellowship/vassals/patron/monarch/covassals/allegiance-broadcast — those
|
||||
/// ride <see cref="ChatInputParser"/>'s normal channel-verb path) and the 22
|
||||
/// GM/faction tags that have NO registered verb and are only reachable
|
||||
/// through <c>DoChannelCommand @ 0x005774A0</c>'s fallback.
|
||||
///
|
||||
/// <para>
|
||||
/// Two production consumers:
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="ChatCommandRouter"/>'s A.4 fallback dispatch — an
|
||||
/// unrecognized <c>/</c>/<c>@</c> verb that is NOT already a known
|
||||
/// <see cref="ChatInputParser"/> verb gets one more lookup here before
|
||||
/// falling through to server passthrough. Only the 22 unregistered tags
|
||||
/// are ever actually reached this way, since every registered tag is
|
||||
/// intercepted earlier by <see cref="ChatInputParser.IsKnownVerb"/>.</item>
|
||||
/// <item><c>@clist</c>/<c>@on</c>/<c>@off</c> argument resolution
|
||||
/// (<see cref="RetailClientCommandCatalog"/>) — these three commands
|
||||
/// accept ANY channel tag, registered or not, so they consult the full
|
||||
/// table.</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Bit values cross-checked against
|
||||
/// <c>references/ACE/Source/ACE.Entity/Enum/Channel.cs</c> (ACE's own
|
||||
/// <c>[Flags] enum Channel</c>, which documents the exact retail PDB
|
||||
/// S_CONSTANT dump for every id) — byte-identical to the values recovered
|
||||
/// from <c>docs/research/2026-08-09-chat-retail-command-registry.md</c> §2.3.
|
||||
/// <c>Help (0x400)</c> is deliberately excluded: retail's own
|
||||
/// <c>DoChannelCommand</c> explicitly rejects it
|
||||
/// (<c>id == 0 || id == 0x400</c> → return 0), and it can never reach this
|
||||
/// table anyway since <c>/help</c>/<c>@help</c> is intercepted earlier by
|
||||
/// <see cref="ChatCommandRouter"/>'s local presentation command.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class RetailChannelTagTable
|
||||
{
|
||||
private static readonly FrozenDictionary<string, uint> ByTag =
|
||||
new Dictionary<string, uint>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// The 22 tags with NO registered send verb (retail-registry doc
|
||||
// §2.3's fallback table) — the only ones actually reachable
|
||||
// through ChatCommandRouter's A.4 dispatch.
|
||||
["abuse"] = 0x00000001u,
|
||||
["ad"] = 0x00000002u,
|
||||
["admin"] = 0x00000002u,
|
||||
["au"] = 0x00000004u,
|
||||
["audit"] = 0x00000004u,
|
||||
["av"] = 0x00000008u,
|
||||
["av1"] = 0x00000008u,
|
||||
["advocate"] = 0x00000008u,
|
||||
["advocate1"] = 0x00000008u,
|
||||
["av2"] = 0x00000010u,
|
||||
["advocate2"] = 0x00000010u,
|
||||
["av3"] = 0x00000020u,
|
||||
["advocate3"] = 0x00000020u,
|
||||
["sent"] = 0x00000200u,
|
||||
["sentinel"] = 0x00000200u,
|
||||
["celestialhand"] = 0x08000000u,
|
||||
["celhan"] = 0x08000000u,
|
||||
["eldrytchweb"] = 0x10000000u,
|
||||
["eldweb"] = 0x10000000u,
|
||||
["radiantblood"] = 0x20000000u,
|
||||
["radblo"] = 0x20000000u,
|
||||
["ol"] = 0x40000000u,
|
||||
["olthoi"] = 0x40000000u,
|
||||
|
||||
// Registered-verb tags (already reachable via ChatInputParser's
|
||||
// normal channel-verb path). Present here ONLY so @clist/@on/@off
|
||||
// accept the same tag spellings retail's GetChannelID resolves —
|
||||
// ChatCommandRouter's A.4 fallback never reaches these entries
|
||||
// because IsKnownVerb intercepts them first.
|
||||
["fellowship"] = 0x00000800u,
|
||||
["fellow"] = 0x00000800u,
|
||||
["fellows"] = 0x00000800u,
|
||||
["f"] = 0x00000800u,
|
||||
["group"] = 0x00000800u,
|
||||
["g"] = 0x00000800u,
|
||||
["party"] = 0x00000800u,
|
||||
["vassals"] = 0x00001000u,
|
||||
["vassal"] = 0x00001000u,
|
||||
["v"] = 0x00001000u,
|
||||
["patron"] = 0x00002000u,
|
||||
["p"] = 0x00002000u,
|
||||
["monarch"] = 0x00004000u,
|
||||
["m"] = 0x00004000u,
|
||||
["covassals"] = 0x01000000u,
|
||||
["covassal"] = 0x01000000u,
|
||||
["co-vassals"] = 0x01000000u,
|
||||
["c"] = 0x01000000u,
|
||||
["a"] = 0x02000000u,
|
||||
["ab"] = 0x02000000u,
|
||||
["allegiance"] = 0x02000000u,
|
||||
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>Resolve a channel tag (no leading <c>/</c> or <c>@</c>) to its legacy bitflag id.</summary>
|
||||
public static bool TryResolve(string tag, out uint channelId) =>
|
||||
ByTag.TryGetValue(tag, out channelId);
|
||||
|
||||
/// <summary>
|
||||
/// True only for the 22 tags that have NO registered send verb — the
|
||||
/// actual reachable set of <see cref="ChatCommandRouter"/>'s A.4
|
||||
/// fallback dispatch. Used by the conformance test to enumerate exactly
|
||||
/// the registry doc's §2.3 fallback list.
|
||||
/// </summary>
|
||||
public static bool IsUnregisteredFallbackTag(string tag) =>
|
||||
ByTag.TryGetValue(tag, out uint id) && id != 0x00000800u && id != 0x00001000u
|
||||
&& id != 0x00002000u && id != 0x00004000u && id != 0x01000000u && id != 0x02000000u;
|
||||
}
|
||||
|
|
@ -51,12 +51,13 @@ public static class RetailClientCommandCatalog
|
|||
"/pklarena (/pla) - Teleports a PKLite player to the PKLite Arena.");
|
||||
|
||||
// ClientCommunicationSystem::DoPKLite/HelpPKLite @ 0x0057A490/0x0057A540.
|
||||
// Retail registers exactly one verb string ("pklite" @ 0x007E16B0) — no
|
||||
// alias, unlike most of this catalog.
|
||||
// Retail registers exactly one verb string ("pklite" @ 0x007E16B0) —
|
||||
// Campaign CH slice CH4 (2026-08-09) added the "pkl" alias per the
|
||||
// command-registry doc §2.6.
|
||||
private static readonly Definition PkLite = NoArguments(
|
||||
ClientCommandId.EnterPkLite,
|
||||
"/pklite",
|
||||
"@pklite - Sets your status to Player Killer Lite. Type @help pklite for more details.");
|
||||
"@pklite (@pkl) - Sets your status to Player Killer Lite. Type @help pklite for more details.");
|
||||
|
||||
private static readonly Definition HouseRecall = NoArguments(
|
||||
ClientCommandId.HouseRecall,
|
||||
|
|
@ -68,6 +69,13 @@ public static class RetailClientCommandCatalog
|
|||
"/house mansion_recall",
|
||||
"/house mansion_recall (/hom, /hoa) - Teleports you to your allegiance mansion.");
|
||||
|
||||
// GameActionHouseAbandon.Handle / retail help data_7dd3d0:
|
||||
// "@house abandon - Abandons your house.\n"
|
||||
private static readonly Definition HouseAbandon = NoArguments(
|
||||
ClientCommandId.HouseAbandon,
|
||||
"/house abandon",
|
||||
"@house abandon - Abandons your house.");
|
||||
|
||||
private static readonly Definition QueryAge = NoArguments(
|
||||
ClientCommandId.QueryAge,
|
||||
"/age",
|
||||
|
|
@ -192,16 +200,198 @@ public static class RetailClientCommandCatalog
|
|||
"/unfilter -<message type>",
|
||||
"/unfilter -<message type> - Shows a globally hidden message category.");
|
||||
|
||||
// Campaign CH slice CH4 (2026-08-09): retail registers exactly one
|
||||
// handler (DoMessageTypes @ 0x0057A010) under four verb strings —
|
||||
// "messagetypes", "message_types", "msgtypes", "msg_types" — all
|
||||
// aliases of the SAME definition, not separate commands.
|
||||
private static readonly Definition MessageTypes = NoArguments(
|
||||
ClientCommandId.ListMessageTypes,
|
||||
"/messagetypes",
|
||||
"/messagetypes - Lists valid filter and squelch message types.");
|
||||
"/messagetypes (/message_types, /msgtypes, /msg_types) - Lists valid filter and squelch message types.");
|
||||
|
||||
private static readonly Definition FillComponents = AnyArguments(
|
||||
ClientCommandId.FillComponents,
|
||||
"/fillcomps [component type] [pyreal value]",
|
||||
"/fillcomps - Helps you buy components in bulk.");
|
||||
|
||||
// ── Campaign CH slice CH4 (2026-08-09) additions ────────────────────
|
||||
|
||||
// ClientCommunicationSystem::DoEndurance @ 0x0057C5F0. Exact retail
|
||||
// text extracted from acclient_2013_pseudo_c.txt:1031097
|
||||
// (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
|
||||
// ClientCommandController.
|
||||
private static readonly Definition Endurance = NoArguments(
|
||||
ClientCommandId.Endurance,
|
||||
"/endurance",
|
||||
"The endurance attribute has a number of abilities tied to it. Type @help endurance for the full description.");
|
||||
|
||||
// ClientCommunicationSystem::DoSpeaker @ 0x0057DAB0. Exact retail text:
|
||||
// acclient_2013_pseudo_c.txt:393309 / 1031426 (data_7e0cd8).
|
||||
private static readonly Definition Speaker = NoArguments(
|
||||
ClientCommandId.Speaker,
|
||||
"/speaker",
|
||||
"This command is no longer in use, please see @allegiance officer.");
|
||||
|
||||
// ClientCommunicationSystem::DoTitle @ 0x0057A640. Exact retail help:
|
||||
// acclient_2013_pseudo_c.txt:1031162 (data_7df2c4) — "@title <new
|
||||
// title> - Sets the title of the popup chat window.\n". No confirmation
|
||||
// text was found at the success site; acdream stores the title but has
|
||||
// no title-bar chrome to render it yet (AP-182).
|
||||
private static readonly Definition SetTitle = AnyArguments(
|
||||
ClientCommandId.SetChatTitle,
|
||||
"/title <new title>",
|
||||
"@title <new title> - Sets the title of the popup chat window.");
|
||||
|
||||
// ClientCommunicationSystem::DoChatToggle @ 0x0056FAD0 — Event_
|
||||
// ModifyGlobalSquelch(remove, 2) for "on", (add, 2) for "off". Exact
|
||||
// retail help: acclient_2013_pseudo_c.txt:1030716/1030720.
|
||||
private static readonly Definition ChatToggle = new(
|
||||
ClientCommandId.ChatToggle,
|
||||
Usage: "/chat <on|off>",
|
||||
HelpText: "@chat <on/off> - Sets whether or not you receive normal chat. When set to \"off\", you will no longer receive any spoken speech (normal chat). However, you will still receive tells.",
|
||||
ValidateArguments: static arguments =>
|
||||
arguments.Equals("on", StringComparison.OrdinalIgnoreCase)
|
||||
|| arguments.Equals("off", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// ClientCommunicationSystem::DoNoTell @ 0x0056FBD0 — same mechanism,
|
||||
// message type 3 (Tell). Exact retail help:
|
||||
// acclient_2013_pseudo_c.txt:1030724/1030728.
|
||||
private static readonly Definition NoTellToggle = new(
|
||||
ClientCommandId.NoTellToggle,
|
||||
Usage: "/notell <on|off>",
|
||||
HelpText: "@notell <on/off> - Sets whether or not you receive @tells. When set to \"on\", you will not receive any tells.",
|
||||
ValidateArguments: static arguments =>
|
||||
arguments.Equals("on", StringComparison.OrdinalIgnoreCase)
|
||||
|| arguments.Equals("off", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
/// <summary>
|
||||
/// Tags accepted by <c>@join</c>/<c>@leave</c> mapped to the linear
|
||||
/// <c>SetSingleCharacterOption (0x0005)</c> option id — retail
|
||||
/// PlayerModule::SetHear*Chat flags, per
|
||||
/// <c>AcDream.Core.Net.Messages.CharacterOptionId</c>.
|
||||
/// </summary>
|
||||
public static bool TryResolveJoinLeaveOption(string tag, out uint optionId) =>
|
||||
JoinLeaveTags.TryGetValue(tag.Trim(), out optionId);
|
||||
|
||||
private static readonly FrozenDictionary<string, uint> JoinLeaveTags =
|
||||
new Dictionary<string, uint>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["allegiance"] = 0x1Bu, // CharacterOptionId.ListenToAllegianceChat
|
||||
["general"] = 0x23u, // CharacterOptionId.ListenToGeneralChat
|
||||
["trade"] = 0x24u, // CharacterOptionId.ListenToTradeChat
|
||||
["lfg"] = 0x25u, // CharacterOptionId.ListenToLFGChat
|
||||
["roleplay"] = 0x26u, // CharacterOptionId.ListenToRoleplayChat
|
||||
["society"] = 0x2Eu, // CharacterOptionId.ListenToSocietyChat
|
||||
["soc"] = 0x2Eu,
|
||||
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// ClientCommunicationSystem::DoJoinChat/DoLeaveChat @ 0x0056F510/
|
||||
// 0x0056F7F0. Tags per the registry doc §2.2: Allegiance, General,
|
||||
// Trade, LFG, Roleplay, Society, Soc. Exact retail help:
|
||||
// acclient_2013_pseudo_c.txt:1030689/1030693.
|
||||
private static readonly Definition JoinChannel = new(
|
||||
ClientCommandId.JoinChannel,
|
||||
Usage: "/join <channel tag>",
|
||||
HelpText: "@join <channel tag> - Allows you to hear and speak on the given channel.",
|
||||
ValidateArguments: static arguments => JoinLeaveTags.ContainsKey(arguments.Trim()));
|
||||
|
||||
private static readonly Definition LeaveChannel = new(
|
||||
ClientCommandId.LeaveChannel,
|
||||
Usage: "/leave <channel tag>",
|
||||
HelpText: "@leave <channel tag> - Prevents you from hearing or speaking on the given channel.",
|
||||
ValidateArguments: static arguments => JoinLeaveTags.ContainsKey(arguments.Trim()));
|
||||
|
||||
// ClientCommunicationSystem::DoPermit @ 0x005785A0. Exact retail help:
|
||||
// acclient_2013_pseudo_c.txt:1030850-1030852 (data_7dbac8).
|
||||
private static readonly Definition Permit = new(
|
||||
ClientCommandId.Permit,
|
||||
Usage: "/permit <add|remove> <name>",
|
||||
HelpText: "@permit add <name> - Allows another player to loot your corpse. @permit remove <name> - Removes permission to access your corpse from the named character.",
|
||||
ValidateArguments: static arguments =>
|
||||
{
|
||||
string[] parts = arguments.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
|
||||
return parts.Length == 2
|
||||
&& (parts[0].Equals("add", StringComparison.OrdinalIgnoreCase)
|
||||
|| parts[0].Equals("remove", StringComparison.OrdinalIgnoreCase));
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Retail house-type spellings mapped to ACE's <c>HouseType</c> enum
|
||||
/// value (the <c>@hslist</c>/ListAvailableHouses payload).
|
||||
/// </summary>
|
||||
public static bool TryResolveHouseType(string type, out uint houseType) =>
|
||||
HouseTypes.TryGetValue(type.Trim(), out houseType);
|
||||
|
||||
private static readonly FrozenDictionary<string, uint> HouseTypes =
|
||||
new Dictionary<string, uint>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["cottage"] = 1u,
|
||||
["villa"] = 2u,
|
||||
["mansion"] = 3u,
|
||||
["apartment"] = 4u,
|
||||
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// ClientCommunicationSystem::DoHouseAvailableList @ 0x00570510. Exact
|
||||
// retail help: acclient_2013_pseudo_c.txt:1031049 (data_7dd9d0).
|
||||
private static readonly Definition HouseAvailableList = new(
|
||||
ClientCommandId.HouseAvailableList,
|
||||
Usage: "/hslist <house type>",
|
||||
HelpText: "@hslist <house type> - Lists the number and, if appropriate, positions of houses currently available for purchase. Types include: Apartment, Cottage, Villa, Mansion",
|
||||
ValidateArguments: static arguments => HouseTypes.ContainsKey(arguments.Trim()));
|
||||
|
||||
// ClientCommunicationSystem::DoChannelIndex @ 0x0056E640. No help
|
||||
// string was extracted for the bare form; the verb is admin/advocate/
|
||||
// PSR gated server-side (GameActionChannelIndex.Handle).
|
||||
private static readonly Definition IndexChannels = NoArguments(
|
||||
ClientCommandId.IndexChannels,
|
||||
"/index",
|
||||
"@index - Requests the channel index (restricted).");
|
||||
|
||||
// ClientCommunicationSystem::DoChannelList @ 0x0057A9B0. Exact retail
|
||||
// no-arg text: acclient_2013_pseudo_c.txt:1031202 (data_7dfaf8)
|
||||
// "Please specify the channel name."
|
||||
private static readonly Definition ListChannel = new(
|
||||
ClientCommandId.ListChannel,
|
||||
Usage: "/clist <channel>",
|
||||
HelpText: "@clist <channel> - Requests the member list of a channel (restricted).",
|
||||
ValidateArguments: static arguments => RetailChannelTagTable.TryResolve(arguments.Trim(), out _),
|
||||
InvalidArgumentsText: "Please specify the channel name.");
|
||||
|
||||
private static readonly Definition OnChannel = new(
|
||||
ClientCommandId.OnChannel,
|
||||
Usage: "/on <channel>",
|
||||
HelpText: "@on <channel> - Joins a channel (restricted).",
|
||||
ValidateArguments: static arguments => RetailChannelTagTable.TryResolve(arguments.Trim(), out _),
|
||||
InvalidArgumentsText: "Please specify the channel name.");
|
||||
|
||||
private static readonly Definition OffChannel = new(
|
||||
ClientCommandId.OffChannel,
|
||||
Usage: "/off <channel>",
|
||||
HelpText: "@off <channel> - Leaves a channel (restricted).",
|
||||
ValidateArguments: static arguments => RetailChannelTagTable.TryResolve(arguments.Trim(), out _),
|
||||
InvalidArgumentsText: "Please specify the channel name.");
|
||||
|
||||
// GameActionRecallAllegianceHometown.Handle. Exact retail help:
|
||||
// acclient_2013_pseudo_c.txt:1031230 — "@allegiance hometown -
|
||||
// Recalls you to your allegiance bindstone, if your allegiance has
|
||||
// tied to one.\n"
|
||||
private static readonly Definition AllegianceHometown = NoArguments(
|
||||
ClientCommandId.AllegianceHometown,
|
||||
"/alh",
|
||||
"@allegiance hometown (@alh, @ah) - Recalls you to your allegiance bindstone, if your allegiance has tied to one.");
|
||||
|
||||
// GameActionAllegianceInfoRequest.Handle — String16L name, empty = self.
|
||||
// Exact retail help: acclient_2013_pseudo_c.txt:1031214 —
|
||||
// "@allegiance info <name> - Requests information on a member of your
|
||||
// allegiance.\n"
|
||||
private static readonly Definition AllegianceInfo = AnyArguments(
|
||||
ClientCommandId.AllegianceInfo,
|
||||
"/allegiance info [name]",
|
||||
"@allegiance info <name> - Requests information on a member of your allegiance.");
|
||||
|
||||
private static readonly FrozenDictionary<string, Definition> ByVerb =
|
||||
new Dictionary<string, Definition>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
|
|
@ -216,6 +406,7 @@ public static class RetailClientCommandCatalog
|
|||
["pklarena"] = PkLiteArena,
|
||||
["pla"] = PkLiteArena,
|
||||
["pklite"] = PkLite,
|
||||
["pkl"] = PkLite,
|
||||
["hor"] = HouseRecall,
|
||||
["hr"] = HouseRecall,
|
||||
["hom"] = MansionRecall,
|
||||
|
|
@ -249,7 +440,25 @@ public static class RetailClientCommandCatalog
|
|||
["filter"] = Filter,
|
||||
["unfilter"] = Unfilter,
|
||||
["messagetypes"] = MessageTypes,
|
||||
["message_types"] = MessageTypes,
|
||||
["msgtypes"] = MessageTypes,
|
||||
["msg_types"] = MessageTypes,
|
||||
["fillcomps"] = FillComponents,
|
||||
["endurance"] = Endurance,
|
||||
["speaker"] = Speaker,
|
||||
["title"] = SetTitle,
|
||||
["chat"] = ChatToggle,
|
||||
["notell"] = NoTellToggle,
|
||||
["join"] = JoinChannel,
|
||||
["leave"] = LeaveChannel,
|
||||
["permit"] = Permit,
|
||||
["hslist"] = HouseAvailableList,
|
||||
["index"] = IndexChannels,
|
||||
["clist"] = ListChannel,
|
||||
["on"] = OnChannel,
|
||||
["off"] = OffChannel,
|
||||
["alh"] = AllegianceHometown,
|
||||
["ah"] = AllegianceHometown,
|
||||
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -271,35 +480,28 @@ public static class RetailClientCommandCatalog
|
|||
string verb = separator < 0
|
||||
? trimmed[1..]
|
||||
: trimmed.Substring(1, separator - 1);
|
||||
// Retail DoCommand @ 0x0057E2E0 right-trims ',' off the verb token
|
||||
// before lookup (trim char set 0x0079452C) — "@f, hi" resolves the
|
||||
// SAME as "@f hi". Campaign CH slice CH4 (2026-08-09).
|
||||
verb = verb.TrimEnd(',');
|
||||
string arguments = separator < 0
|
||||
? string.Empty
|
||||
: trimmed[(separator + 1)..].Trim();
|
||||
|
||||
Definition? definition;
|
||||
if (verb.Equals("house", StringComparison.OrdinalIgnoreCase))
|
||||
if (verb.Equals("house", StringComparison.OrdinalIgnoreCase)
|
||||
|| verb.Equals("hou", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
definition = arguments.ToLowerInvariant() switch
|
||||
{
|
||||
"recall" => HouseRecall,
|
||||
"mansion_recall" or "alleg_recall" => MansionRecall,
|
||||
_ => null,
|
||||
};
|
||||
if (definition is null)
|
||||
{
|
||||
match = new Match(
|
||||
ClientCommandId.HouseRecall,
|
||||
arguments,
|
||||
"/house recall | /house mansion_recall",
|
||||
HasValidArguments: false,
|
||||
InvalidArgumentsText: null);
|
||||
return true;
|
||||
}
|
||||
|
||||
// The subcommand selected the operation; its own handler has no
|
||||
// additional arguments.
|
||||
arguments = string.Empty;
|
||||
return TryMatchHouse(arguments, out match);
|
||||
}
|
||||
else if (!ByVerb.TryGetValue(verb, out definition))
|
||||
|
||||
if (verb.Equals("allegiance", StringComparison.OrdinalIgnoreCase)
|
||||
|| verb.Equals("all", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return TryMatchAllegiance(arguments, out match);
|
||||
}
|
||||
|
||||
if (!ByVerb.TryGetValue(verb, out definition))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -313,6 +515,85 @@ public static class RetailClientCommandCatalog
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>@house <sub></c> / <c>@hou <sub></c> dispatcher.
|
||||
/// Retail's real <c>DoHouse @ 0x00580860</c> handles 15 subcommands
|
||||
/// (see the registry doc §2.5b) locally; acdream Campaign CH slice CH4
|
||||
/// (2026-08-09) ports 4 of them (recall/re, mansion_recall/alleg_recall/
|
||||
/// ma, abandon) plus the pre-existing HasValidArguments==false swallow
|
||||
/// for a MISSPELLED recall variant. Every OTHER subcommand — open,
|
||||
/// close, storage, remove, boot, boot_all, remove_all, guest, available,
|
||||
/// hooks, on, off — is NOT yet ported (TS-68) and must reach ACE
|
||||
/// (which replies "Unknown command") rather than being swallowed
|
||||
/// locally with a wrong usage message — the Tier-1 #4 fix from the
|
||||
/// command-registry doc. Returning <c>false</c> here lets
|
||||
/// <see cref="ChatCommandRouter"/> fall through to server passthrough.
|
||||
/// </summary>
|
||||
private static bool TryMatchHouse(string arguments, out Match match)
|
||||
{
|
||||
match = default;
|
||||
string subcommand = arguments.ToLowerInvariant();
|
||||
Definition? definition = subcommand switch
|
||||
{
|
||||
"recall" or "re" => HouseRecall,
|
||||
"mansion_recall" or "alleg_recall" or "ma" => MansionRecall,
|
||||
"abandon" => HouseAbandon,
|
||||
_ => null,
|
||||
};
|
||||
if (definition is null)
|
||||
return false;
|
||||
|
||||
match = new Match(
|
||||
definition.Command,
|
||||
Arguments: string.Empty,
|
||||
definition.Usage,
|
||||
HasValidArguments: true,
|
||||
InvalidArgumentsText: null);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>@allegiance <sub></c> / <c>@all <sub></c> dispatcher.
|
||||
/// Retail's real <c>DoAllegiance @ 0x0057D5A0</c> handles 12
|
||||
/// subcommands (see the registry doc §2.5) locally; acdream Campaign CH
|
||||
/// slice CH4 (2026-08-09) ports 2 of them (info, hometown/ho). Every
|
||||
/// OTHER subcommand — boot, ban, officer, title, name, lock, house,
|
||||
/// motd, chat, broadcast — is NOT yet ported (TS-68) and falls through
|
||||
/// to server passthrough, same reasoning as <see cref="TryMatchHouse"/>.
|
||||
/// </summary>
|
||||
private static bool TryMatchAllegiance(string arguments, out Match match)
|
||||
{
|
||||
match = default;
|
||||
int separator = IndexOfWhitespace(arguments);
|
||||
string subcommand = separator < 0 ? arguments : arguments[..separator];
|
||||
string rest = separator < 0 ? string.Empty : arguments[(separator + 1)..].Trim();
|
||||
|
||||
if (subcommand.Equals("hometown", StringComparison.OrdinalIgnoreCase)
|
||||
|| subcommand.Equals("ho", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
match = new Match(
|
||||
AllegianceHometown.Command,
|
||||
Arguments: string.Empty,
|
||||
AllegianceHometown.Usage,
|
||||
HasValidArguments: true,
|
||||
InvalidArgumentsText: null);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (subcommand.Equals("info", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
match = new Match(
|
||||
AllegianceInfo.Command,
|
||||
rest,
|
||||
AllegianceInfo.Usage,
|
||||
HasValidArguments: true,
|
||||
InvalidArgumentsText: null);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Help line generated from the same definition routing uses.</summary>
|
||||
public static string BuildHelpText() => string.Join("\n ",
|
||||
Lifestone.HelpText,
|
||||
|
|
@ -322,6 +603,7 @@ public static class RetailClientCommandCatalog
|
|||
PkLite.HelpText,
|
||||
HouseRecall.HelpText,
|
||||
MansionRecall.HelpText,
|
||||
HouseAbandon.HelpText,
|
||||
QueryAge.HelpText,
|
||||
QueryBirth.HelpText,
|
||||
FrameRate.HelpText,
|
||||
|
|
@ -345,7 +627,65 @@ public static class RetailClientCommandCatalog
|
|||
Filter.HelpText,
|
||||
Unfilter.HelpText,
|
||||
MessageTypes.HelpText,
|
||||
FillComponents.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
|
||||
/// not literal keys of the backing dictionary because their dispatch
|
||||
/// depends on the subcommand). Used by the CH4 conformance test to
|
||||
/// enforce the ownership rule in both directions: every retail-registry
|
||||
/// verb this catalog claims must actually be in the registry, and vice
|
||||
/// versa.
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<string> KnownVerbs { get; } =
|
||||
ByVerb.Keys.Concat(["house", "hou", "allegiance", "all"]).ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// <c>/help <verb></c> lookup for a catalog-dispatched command —
|
||||
/// retail's <c>DoHelp @ 0x0057F9E0</c> looking up the verb's registered
|
||||
/// help callback. Does not cover chat-alias or channel verbs (see
|
||||
/// <see cref="RetailCommandHelpTable"/> for those) nor the deferred
|
||||
/// allegiance/house subcommand overviews (also
|
||||
/// <see cref="RetailCommandHelpTable"/>).
|
||||
/// </summary>
|
||||
public static bool TryGetHelpText(string verb, out string helpText)
|
||||
{
|
||||
string trimmedVerb = verb.TrimEnd(',');
|
||||
if (trimmedVerb.Equals("house", StringComparison.OrdinalIgnoreCase)
|
||||
|| trimmedVerb.Equals("hou", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
helpText = RetailCommandHelpTable.HouseOverview;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (trimmedVerb.Equals("allegiance", StringComparison.OrdinalIgnoreCase)
|
||||
|| trimmedVerb.Equals("all", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
helpText = RetailCommandHelpTable.AllegianceOverview;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ByVerb.TryGetValue(trimmedVerb, out Definition? definition))
|
||||
{
|
||||
helpText = definition.HelpText;
|
||||
return true;
|
||||
}
|
||||
|
||||
helpText = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Definition NoArguments(
|
||||
ClientCommandId command, string usage, string helpText) =>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,205 @@
|
|||
using System.Collections.Frozen;
|
||||
|
||||
namespace AcDream.UI.Abstractions.Panels.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign CH slice CH4 (2026-08-09): <c>/help <verb></c> text for
|
||||
/// every retail-registry verb <see cref="RetailClientCommandCatalog"/>
|
||||
/// doesn't dispatch directly — chat aliases and channel verbs
|
||||
/// (<see cref="ChatInputParser"/>), the null-func help-only nodes (retail
|
||||
/// registers these with NO handler; typing them bare reaches the server,
|
||||
/// only <c>@help <verb></c> shows anything locally), and the
|
||||
/// allegiance/house command overviews (the per-subcommand detail lives
|
||||
/// here too, even though most subcommands are not yet locally executed —
|
||||
/// see TS-68).
|
||||
///
|
||||
/// <para>
|
||||
/// Every entry is verbatim retail text recovered from
|
||||
/// <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.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class RetailCommandHelpTable
|
||||
{
|
||||
// acclient_2013_pseudo_c.txt:1030771 (data_7dae40).
|
||||
public const string Tell =
|
||||
"@tell <name>, <text> - Sends a long-distance, private message to the specified character. Note that you must put a comma after the character's name.";
|
||||
|
||||
// acclient_2013_pseudo_c.txt:1030742/1030745.
|
||||
public const string Reply =
|
||||
"@reply <text> - Sends the text to the last person who @tell'd you. You may also use @r or @rp.";
|
||||
|
||||
// acclient_2013_pseudo_c.txt:1030753/1030757.
|
||||
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.
|
||||
public const string HelpPrefixNote =
|
||||
"Note: You may substitute a forward slash (/) for the at symbol (@).";
|
||||
|
||||
// @mr/@pr are registered with a NULL function pointer in the 2013
|
||||
// build (verified at 0x00583041/0x005830C1 — arg3 is 0). Retail's own
|
||||
// HelpReply @0x00577A50 is shared between @reply/@r/@rp (which DO
|
||||
// execute) and @mr/@pr (which do NOT — they fall through to
|
||||
// DoChannelCommand, miss, and reach the server as literal text). The
|
||||
// shared help text is Reply's text above; acdream additionally notes
|
||||
// the non-execution here so /help mr doesn't imply it works.
|
||||
public const string MonarchReply =
|
||||
"@mr - Reply to the last person who @m'd you (monarch chat only). NOTE: this command is registered with no handler in the named retail build — it does not execute locally in retail OR acdream; typing it sends the literal text to the server.";
|
||||
|
||||
public const string PatronReply =
|
||||
"@pr - Reply to the last person who @p'd you (patron chat only). NOTE: this command is registered with no handler in the named retail build — it does not execute locally in retail OR acdream; typing it sends the literal text to the server.";
|
||||
|
||||
// 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:1031192-1031198 (data_7df7f8/data_7dfac4).
|
||||
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.";
|
||||
|
||||
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.";
|
||||
|
||||
// acclient_2013_pseudo_c.txt:1030670-1030676 (data_7da300/data_7da3e0).
|
||||
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.";
|
||||
|
||||
// acclient_2013_pseudo_c.txt:1031234 (data_7e03d4) plus the full
|
||||
// per-subcommand block at 1031210-1031230 (data_7dfb58).
|
||||
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"
|
||||
+ "@allegiance info <name> - Requests information on a member of your allegiance. [IMPLEMENTED]\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 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"
|
||||
+ "@allegiance officer [list] - list your allegiance officer. Can be used by anyone in an allegiance.\n"
|
||||
+ "@allegiance title set <level #> <title> - Sets the title of the given officer level.\n"
|
||||
+ "@allegiance title clear - Clears all officer titles.\n"
|
||||
+ "@allegiance title [list] - Lists all the officer titles for your allegiance.\n"
|
||||
+ "@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.";
|
||||
|
||||
// acclient_2013_pseudo_c.txt:1031041/1031045 (data_7dd908/data_7dd968)
|
||||
// plus the full per-subcommand block at 1031017-1031037 (data_7dd3d0).
|
||||
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"
|
||||
+ "@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"
|
||||
+ "@house guest remove <name> - Removes players from your house guest list.\n"
|
||||
+ "@house guest add_allegiance - Adds your allegiance to the guest list.\n"
|
||||
+ "@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 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"
|
||||
+ "@house storage remove_allegiance - Removes storage permission from your allegiance.\n"
|
||||
+ "@house storage remove_all - Removes all storage permissions from guests.\n"
|
||||
+ "@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.";
|
||||
|
||||
// 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.";
|
||||
|
||||
private static readonly FrozenDictionary<string, string> ByVerb =
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["say"] = "@say <text> - Speaks the text aloud to nearby players.",
|
||||
["s"] = "@say <text> - Speaks the text aloud to nearby players.",
|
||||
["tell"] = Tell,
|
||||
["t"] = Tell,
|
||||
["send"] = Tell,
|
||||
["whisper"] = Tell,
|
||||
["w"] = Tell,
|
||||
["reply"] = Reply,
|
||||
["r"] = Reply,
|
||||
["rp"] = Reply,
|
||||
["retell"] = Retell,
|
||||
["rt"] = Retell,
|
||||
["mr"] = MonarchReply,
|
||||
["pr"] = PatronReply,
|
||||
["day"] = Day,
|
||||
["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).
|
||||
["f"] = "Sends text to your Fellowship channel.",
|
||||
["fellow"] = "Sends text to your Fellowship channel.",
|
||||
["fellows"] = "Sends text to your Fellowship channel.",
|
||||
["fellowship"] = "Sends text to your Fellowship channel.",
|
||||
["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.",
|
||||
["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.",
|
||||
["m"] = "Sends text to your Monarch.",
|
||||
["monarch"] = "Sends text to your Monarch.",
|
||||
["p"] = "Sends text to your Patron.",
|
||||
["patron"] = "Sends text to your Patron.",
|
||||
["v"] = "Sends text to your Vassals.",
|
||||
["vassal"] = "Sends text to your Vassals.",
|
||||
["vassals"] = "Sends text to your Vassals.",
|
||||
["c"] = "Sends text to your Co-vassals.",
|
||||
["covassal"] = "Sends text to your Co-vassals.",
|
||||
["covassals"] = "Sends text to your Co-vassals.",
|
||||
["co-vassals"] = "Sends text to your Co-vassals.",
|
||||
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public static bool TryGetHelpText(string verb, out string helpText) =>
|
||||
ByVerb.TryGetValue(verb.TrimEnd(','), out helpText!);
|
||||
}
|
||||
17
src/AcDream.UI.Abstractions/SendRawChannelCmd.cs
Normal file
17
src/AcDream.UI.Abstractions/SendRawChannelCmd.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
namespace AcDream.UI.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign CH slice CH4 (2026-08-09): broadcast to a legacy <c>ChatChannel
|
||||
/// (0x0147)</c> bitflag id resolved directly from a channel TAG, bypassing
|
||||
/// <see cref="ChatChannelKind"/> and <see cref="ChannelResolver"/> entirely.
|
||||
///
|
||||
/// <para>
|
||||
/// This is retail's <c>DoChannelCommand @ 0x005774A0</c> fallback path — the
|
||||
/// 22 <c>ChannelSystem::GetChannelID</c> tags that have no registered verb
|
||||
/// (GM/faction channels like <c>@admin</c>, <c>@sentinel</c>,
|
||||
/// <c>@celestialhand</c>) plus the argument channel-tag resolution
|
||||
/// <c>@clist</c>/<c>@on</c>/<c>@off</c> use. See
|
||||
/// <see cref="Panels.Chat.RetailChannelTagTable"/> for the tag→id table.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed record SendRawChannelCmd(uint ChannelId, string Text);
|
||||
Loading…
Add table
Add a link
Reference in a new issue