using System.Collections.Frozen; namespace AcDream.UI.Abstractions.Panels.Chat; /// /// Immutable catalog of commands the named retail client executes locally. /// This is intentionally separate from chat aliases and ACE server commands. /// /// /// Aliases and ownership come from the named-retail command table. Packet /// and local-state behavior is recorded in the command-family pseudocode /// notes under docs/research/. /// /// public static class RetailClientCommandCatalog { private sealed record Definition( ClientCommandId Command, string Usage, string HelpText, Func ValidateArguments, string? InvalidArgumentsText = null); /// A resolved retail command plus its raw, trimmed argument text. public readonly record struct Match( ClientCommandId Command, string Arguments, string Usage, bool HasValidArguments, string? InvalidArgumentsText); private static readonly Definition Lifestone = new( ClientCommandId.LifestoneRecall, Usage: "/lifestone", HelpText: "/lifestone (/lif, /ls) - Returns you to the last lifestone you used without killing you.", ValidateArguments: static arguments => arguments.Length == 0); private static readonly Definition Marketplace = NoArguments( ClientCommandId.MarketplaceRecall, "/marketplace", "/marketplace (/mar, /mp) - Teleports you to the Marketplace of Dereth."); private static readonly Definition PkArena = NoArguments( ClientCommandId.PkArenaRecall, "/pkarena", "/pkarena (/pka) - Teleports a Player Killer to the PK Arena."); private static readonly Definition PkLiteArena = NoArguments( ClientCommandId.PkLiteArenaRecall, "/pklarena", "/pklarena (/pla) - Teleports a PKLite player to the PKLite Arena."); private static readonly Definition HouseRecall = NoArguments( ClientCommandId.HouseRecall, "/house recall", "/house recall (/hor, /hr) - Teleports you to your house."); private static readonly Definition MansionRecall = NoArguments( ClientCommandId.MansionRecall, "/house mansion_recall", "/house mansion_recall (/hom, /hoa) - Teleports you to your allegiance mansion."); private static readonly Definition QueryAge = NoArguments( ClientCommandId.QueryAge, "/age", "/age - Displays how long your character has been played."); private static readonly Definition QueryBirth = NoArguments( ClientCommandId.QueryBirth, "/birth", "/birth - Displays when your character was created."); private static readonly Definition FrameRate = NoArguments( ClientCommandId.ToggleFrameRate, "/framerate", "/framerate - Toggles the framerate display."); private static readonly Definition LockUi = NoArguments( ClientCommandId.ToggleUiLock, "/lockui", "/lockui - Toggles whether the interface can be moved or resized."); private static readonly Definition Version = NoArguments( ClientCommandId.ShowVersion, "/version", "/version - Displays the client version."); private static readonly Definition Location = NoArguments( ClientCommandId.ShowLocation, "/loc", "/loc - Displays your current position."); private static readonly Definition Corpse = new( ClientCommandId.ShowLastCorpseLocation, Usage: "/corpse", HelpText: "/corpse (/cor) - Displays the location of your last outdoor death.", // DoCorpse @ 0x00578220 ignores its argument count. ValidateArguments: static _ => true); private static readonly Definition Die = new( ClientCommandId.Die, Usage: "/die", HelpText: "/die - Kills your character after confirmation.", ValidateArguments: static arguments => arguments.Length == 0, InvalidArgumentsText: "Please see @help die for more information on how to use this command."); private static readonly Definition Clear = AnyArguments( ClientCommandId.ClearChat, "/clear [all]", "/clear [all] - Clears the current chat window, or every chat window."); private static readonly Definition SaveUi = AnyArguments( ClientCommandId.SaveUi, "/saveui [filename]", "/saveui [filename] - Saves the current interface layout."); private static readonly Definition LoadUi = AnyArguments( ClientCommandId.LoadUi, "/loadui [filename]", "/loadui [filename] - Loads a saved interface layout."); private static readonly Definition SaveAutoUi = AnyArguments( ClientCommandId.SaveAutoUi, "/saveautoui", "/saveautoui - Saves the automatic character-and-resolution interface layout."); private static readonly Definition LoadAutoUi = AnyArguments( ClientCommandId.LoadAutoUi, "/loadautoui", "/loadautoui - Loads the automatic character-and-resolution interface layout."); private static readonly Definition Away = AnyArguments( ClientCommandId.Away, "/afk [on|off|msg ]", "/afk [on|off|msg ] - Sets your away-from-keyboard status."); private static readonly Definition Consent = AnyArguments( ClientCommandId.Consent, "/consent >", "/consent - Manages corpse-looting consent."); private static readonly Definition Emote = AnyArguments( ClientCommandId.Emote, "/emote ", "/emote (/e, /em, /me) - Performs a text emote."); private static readonly Definition Emotes = NoArguments( ClientCommandId.ListEmotes, "/emotes", "/emotes - Lists all standard emotes."); private static readonly Definition Friends = AnyArguments( ClientCommandId.Friends, "/friends [add|remove|online|old]", "/friends - Helps you manage your friends list."); private static readonly Definition FriendsAdd = AnyArguments( ClientCommandId.FriendsAdd, "/friends_add ", "/friends_add - Adds a character to your friends list."); private static readonly Definition FriendsRemove = AnyArguments( ClientCommandId.FriendsRemove, "/friends_remove ", "/friends_remove - Removes friends from your list."); private static readonly Definition Squelch = AnyArguments( ClientCommandId.Squelch, "/squelch [options] ", "/squelch - Ignores messages from a player or account."); private static readonly Definition Unsquelch = AnyArguments( ClientCommandId.Unsquelch, "/unsquelch [options] ", "/unsquelch - Stops ignoring messages from a player or account."); private static readonly Definition Filter = AnyArguments( ClientCommandId.Filter, "/filter -", "/filter - - Globally hides a message category."); private static readonly Definition Unfilter = AnyArguments( ClientCommandId.Unfilter, "/unfilter -", "/unfilter - - Shows a globally hidden message category."); private static readonly Definition MessageTypes = NoArguments( ClientCommandId.ListMessageTypes, "/messagetypes", "/messagetypes - 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."); private static readonly FrozenDictionary ByVerb = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["lifestone"] = Lifestone, ["lif"] = Lifestone, ["ls"] = Lifestone, ["marketplace"] = Marketplace, ["mar"] = Marketplace, ["mp"] = Marketplace, ["pkarena"] = PkArena, ["pka"] = PkArena, ["pklarena"] = PkLiteArena, ["pla"] = PkLiteArena, ["hor"] = HouseRecall, ["hr"] = HouseRecall, ["hom"] = MansionRecall, ["hoa"] = MansionRecall, ["age"] = QueryAge, ["birth"] = QueryBirth, ["framerate"] = FrameRate, ["lockui"] = LockUi, ["version"] = Version, ["loc"] = Location, ["corpse"] = Corpse, ["cor"] = Corpse, ["die"] = Die, ["clear"] = Clear, ["saveui"] = SaveUi, ["loadui"] = LoadUi, ["saveautoui"] = SaveAutoUi, ["loadautoui"] = LoadAutoUi, ["afk"] = Away, ["consent"] = Consent, ["e"] = Emote, ["em"] = Emote, ["emote"] = Emote, ["me"] = Emote, ["emotes"] = Emotes, ["friends"] = Friends, ["friends_add"] = FriendsAdd, ["friends_remove"] = FriendsRemove, ["squelch"] = Squelch, ["unsquelch"] = Unsquelch, ["filter"] = Filter, ["unfilter"] = Unfilter, ["messagetypes"] = MessageTypes, ["fillcomps"] = FillComponents, }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); /// /// Resolve a complete chat-bar line. Returns false when its verb is not /// client-owned; callers can then try chat aliases or the server-command /// path. Both retail command prefixes are accepted. /// public static bool TryMatch(string input, out Match match) { match = default; if (string.IsNullOrWhiteSpace(input)) return false; string trimmed = input.Trim(); if (trimmed.Length < 2 || trimmed[0] is not ('/' or '@')) return false; int separator = IndexOfWhitespace(trimmed); string verb = separator < 0 ? trimmed[1..] : trimmed.Substring(1, separator - 1); string arguments = separator < 0 ? string.Empty : trimmed[(separator + 1)..].Trim(); Definition? definition; if (verb.Equals("house", 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; } else if (!ByVerb.TryGetValue(verb, out definition)) { return false; } match = new Match( definition.Command, arguments, definition.Usage, definition.ValidateArguments(arguments), definition.InvalidArgumentsText); return true; } /// Help line generated from the same definition routing uses. public static string BuildHelpText() => string.Join("\n ", Lifestone.HelpText, Marketplace.HelpText, PkArena.HelpText, PkLiteArena.HelpText, HouseRecall.HelpText, MansionRecall.HelpText, QueryAge.HelpText, QueryBirth.HelpText, FrameRate.HelpText, LockUi.HelpText, Version.HelpText, Location.HelpText, Corpse.HelpText, Die.HelpText, Clear.HelpText, SaveUi.HelpText, LoadUi.HelpText, SaveAutoUi.HelpText, LoadAutoUi.HelpText, Away.HelpText, Consent.HelpText, Emote.HelpText, Emotes.HelpText, Friends.HelpText, Squelch.HelpText, Unsquelch.HelpText, Filter.HelpText, Unfilter.HelpText, MessageTypes.HelpText, FillComponents.HelpText); private static Definition NoArguments( ClientCommandId command, string usage, string helpText) => new(command, usage, helpText, static arguments => arguments.Length == 0); private static Definition AnyArguments( ClientCommandId command, string usage, string helpText) => new(command, usage, helpText, static _ => true); private static int IndexOfWhitespace(string value) { for (int i = 1; i < value.Length; i++) { if (char.IsWhiteSpace(value[i])) return i; } return -1; } }