feat(chat): port retail client command families

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>
This commit is contained in:
Erik 2026-07-13 14:50:15 +02:00
parent 5a45a7ac7f
commit 9ea579bdd0
47 changed files with 6659 additions and 99 deletions

View file

@ -7,9 +7,9 @@ namespace AcDream.UI.Abstractions.Panels.Chat;
/// This is intentionally separate from chat aliases and ACE server commands.
///
/// <para>
/// First vertical slice: retail registers <c>lifestone</c>, <c>lif</c>, and
/// <c>ls</c> against <c>ClientCommunicationSystem::DoLifestone @ 0x0056FC70</c>.
/// See <c>docs/research/2026-07-13-retail-client-command-routing-pseudocode.md</c>.
/// 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
@ -18,14 +18,16 @@ public static class RetailClientCommandCatalog
ClientCommandId Command,
string Usage,
string HelpText,
Func<string, bool> ValidateArguments);
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);
bool HasValidArguments,
string? InvalidArgumentsText);
private static readonly Definition Lifestone = new(
ClientCommandId.LifestoneRecall,
@ -33,12 +35,212 @@ public static class RetailClientCommandCatalog
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>
@ -60,22 +262,88 @@ public static class RetailClientCommandCatalog
string verb = separator < 0
? trimmed[1..]
: trimmed.Substring(1, separator - 1);
if (!ByVerb.TryGetValue(verb, out Definition? definition))
return false;
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.ValidateArguments(arguments),
definition.InvalidArgumentsText);
return true;
}
/// <summary>Help line generated from the same definition routing uses.</summary>
public static string BuildHelpText() => Lifestone.HelpText;
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)
{