Expand the typed client-command boundary across travel, character queries, local UI and layout controls, AFK and consent, emotes, friends, squelch and filters, and fill-components. Preserve retail packet layouts and queue ownership, import the confirmation dialog, and keep authoritative social state in Core. Co-Authored-By: Codex <codex@openai.com>
358 lines
13 KiB
C#
358 lines
13 KiB
C#
using System.Collections.Frozen;
|
|
|
|
namespace AcDream.UI.Abstractions.Panels.Chat;
|
|
|
|
/// <summary>
|
|
/// Immutable catalog of commands the named retail client executes locally.
|
|
/// This is intentionally separate from chat aliases and ACE server commands.
|
|
///
|
|
/// <para>
|
|
/// Aliases and ownership come from the named-retail command table. Packet
|
|
/// and local-state behavior is recorded in the command-family pseudocode
|
|
/// notes under <c>docs/research/</c>.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class RetailClientCommandCatalog
|
|
{
|
|
private sealed record Definition(
|
|
ClientCommandId Command,
|
|
string Usage,
|
|
string HelpText,
|
|
Func<string, bool> ValidateArguments,
|
|
string? InvalidArgumentsText = null);
|
|
|
|
/// <summary>A resolved retail command plus its raw, trimmed argument text.</summary>
|
|
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 <message>]",
|
|
"/afk [on|off|msg <message>] - Sets your away-from-keyboard status.");
|
|
|
|
private static readonly Definition Consent = AnyArguments(
|
|
ClientCommandId.Consent,
|
|
"/consent <on|off|who|clear|remove <name>>",
|
|
"/consent - Manages corpse-looting consent.");
|
|
|
|
private static readonly Definition Emote = AnyArguments(
|
|
ClientCommandId.Emote,
|
|
"/emote <text>",
|
|
"/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 <name>",
|
|
"/friends_add <name> - Adds a character to your friends list.");
|
|
|
|
private static readonly Definition FriendsRemove = AnyArguments(
|
|
ClientCommandId.FriendsRemove,
|
|
"/friends_remove <name|-all>",
|
|
"/friends_remove <name|-all> - Removes friends from your list.");
|
|
|
|
private static readonly Definition Squelch = AnyArguments(
|
|
ClientCommandId.Squelch,
|
|
"/squelch [options] <name>",
|
|
"/squelch - Ignores messages from a player or account.");
|
|
|
|
private static readonly Definition Unsquelch = AnyArguments(
|
|
ClientCommandId.Unsquelch,
|
|
"/unsquelch [options] <name>",
|
|
"/unsquelch - Stops ignoring messages from a player or account.");
|
|
|
|
private static readonly Definition Filter = AnyArguments(
|
|
ClientCommandId.Filter,
|
|
"/filter -<message type>",
|
|
"/filter -<message type> - Globally hides a message category.");
|
|
|
|
private static readonly Definition Unfilter = AnyArguments(
|
|
ClientCommandId.Unfilter,
|
|
"/unfilter -<message type>",
|
|
"/unfilter -<message type> - 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<string, Definition> ByVerb =
|
|
new Dictionary<string, Definition>(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);
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Help line generated from the same definition routing uses.</summary>
|
|
public static string BuildHelpText() => string.Join("\n ",
|
|
Lifestone.HelpText,
|
|
Marketplace.HelpText,
|
|
PkArena.HelpText,
|
|
PkLiteArena.HelpText,
|
|
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;
|
|
}
|
|
}
|