Skip to content

Commit a4ce2a6

Browse files
committed
Refactored (command-system): stabilize and modularize GM command infrastructure
- Split monolithic command system into focused, maintainable files - Introduced CommandSystemCore with frozen alias lookup - Eliminated shared mutable argument state - Added safe enumeration of distinct command definitions - Retained existing GM workflows and Chaos behavior
1 parent f5daf71 commit a4ce2a6

File tree

13 files changed

+986
-1678
lines changed

13 files changed

+986
-1678
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
using System.Globalization;
2+
3+
namespace Darkages.CommandSystem;
4+
5+
internal static class ArgumentParser
6+
{
7+
public static bool TryParse(
8+
CommandDefinition command,
9+
ReadOnlySpan<char> input,
10+
Token[] tokens,
11+
int argsStart,
12+
int argsCount,
13+
ParsedArgs parsed,
14+
Action<object?, string> onError,
15+
object? ctx)
16+
{
17+
var specs = command.ArgSpecs;
18+
var values = parsed.Values;
19+
20+
// Too many args?
21+
if (argsCount > specs.Length)
22+
{
23+
onError(ctx, $"Too many arguments. Usage: {Usage(command)}");
24+
return false;
25+
}
26+
27+
for (int i = 0; i < specs.Length; i++)
28+
{
29+
var spec = specs[i];
30+
31+
if (i >= argsCount)
32+
{
33+
if (spec.Optional)
34+
{
35+
if (spec.Default is not null)
36+
{
37+
if (!TryCoerce(spec, spec.Default.AsSpan(), out values[i], onError, ctx))
38+
return false;
39+
}
40+
continue;
41+
}
42+
43+
onError(ctx, $"Missing required argument '{spec.Name}'. Usage: {Usage(command)}");
44+
return false;
45+
}
46+
47+
var raw = tokens[argsStart + i].Slice(input);
48+
raw = Tokenizer.Unquote(raw);
49+
50+
if (spec.Validator is not null && !spec.Validator(raw))
51+
{
52+
onError(ctx, $"Argument '{spec.Name}' is invalid.");
53+
return false;
54+
}
55+
56+
if (!TryCoerce(spec, raw, out values[i], onError, ctx))
57+
return false;
58+
}
59+
60+
return true;
61+
}
62+
63+
private static bool TryCoerce(
64+
ArgSpec spec,
65+
ReadOnlySpan<char> raw,
66+
out ArgValue value,
67+
Action<object?, string> onError,
68+
object? ctx)
69+
{
70+
switch (spec.Kind)
71+
{
72+
case ArgKind.String:
73+
value = ArgValue.FromString(raw.ToString());
74+
return true;
75+
76+
case ArgKind.Int32:
77+
if (int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i32))
78+
{
79+
value = ArgValue.FromInt32(i32);
80+
return true;
81+
}
82+
onError(ctx, $"Argument '{spec.Name}' must be an integer.");
83+
value = default;
84+
return false;
85+
86+
case ArgKind.Int64:
87+
if (long.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i64))
88+
{
89+
value = ArgValue.FromInt64(i64);
90+
return true;
91+
}
92+
onError(ctx, $"Argument '{spec.Name}' must be a 64-bit integer.");
93+
value = default;
94+
return false;
95+
96+
case ArgKind.Double:
97+
if (double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var dbl))
98+
{
99+
value = ArgValue.FromDouble(dbl);
100+
return true;
101+
}
102+
onError(ctx, $"Argument '{spec.Name}' must be a number.");
103+
value = default;
104+
return false;
105+
106+
default:
107+
value = ArgValue.FromString(raw.ToString());
108+
return true;
109+
}
110+
}
111+
112+
private static string Usage(CommandDefinition cmd)
113+
{
114+
var alias = cmd.Aliases.Length > 0 ? cmd.Aliases[0] : cmd.Name;
115+
116+
if (cmd.ArgSpecs.Length == 0)
117+
return alias;
118+
119+
var parts = new string[cmd.ArgSpecs.Length];
120+
for (int i = 0; i < cmd.ArgSpecs.Length; i++)
121+
{
122+
var a = cmd.ArgSpecs[i];
123+
parts[i] = a.Optional ? $"[{a.Name}]" : $"<{a.Name}>";
124+
}
125+
126+
return $"{alias} {string.Join(' ', parts)}";
127+
}
128+
}

0 commit comments

Comments
 (0)