feat(chat): route retail lifestone commands
Separate retail client actions, ACE server commands, and ordinary chat at the shared router. Port lifestone/lif/ls from the named retail registry through a typed App controller to game action 0x0063, keep unknown verbs on ACE Talk, and cover both UI backends plus exact outbound bytes. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
5090aa7217
commit
5a45a7ac7f
19 changed files with 604 additions and 78 deletions
|
|
@ -0,0 +1,90 @@
|
|||
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>
|
||||
/// 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>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class RetailClientCommandCatalog
|
||||
{
|
||||
private sealed record Definition(
|
||||
ClientCommandId Command,
|
||||
string Usage,
|
||||
string HelpText,
|
||||
Func<string, bool> ValidateArguments);
|
||||
|
||||
/// <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);
|
||||
|
||||
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 FrozenDictionary<string, Definition> ByVerb =
|
||||
new Dictionary<string, Definition>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["lifestone"] = Lifestone,
|
||||
["lif"] = Lifestone,
|
||||
["ls"] = Lifestone,
|
||||
}.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);
|
||||
if (!ByVerb.TryGetValue(verb, out Definition? definition))
|
||||
return false;
|
||||
|
||||
string arguments = separator < 0
|
||||
? string.Empty
|
||||
: trimmed[(separator + 1)..].Trim();
|
||||
match = new Match(
|
||||
definition.Command,
|
||||
arguments,
|
||||
definition.Usage,
|
||||
definition.ValidateArguments(arguments));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Help line generated from the same definition routing uses.</summary>
|
||||
public static string BuildHelpText() => Lifestone.HelpText;
|
||||
|
||||
private static int IndexOfWhitespace(string value)
|
||||
{
|
||||
for (int i = 1; i < value.Length; i++)
|
||||
{
|
||||
if (char.IsWhiteSpace(value[i]))
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue