Skip to content

Commit b157f5d

Browse files
committed
feat(DevConsole): implement enhanced autocomplete functionality
- Introduced a new system for dev-console autocomplete enhancements, allowing for improved command argument matching and display. - Added multiple classes for managing autocomplete bindings, context, and enhancements, including DevConsoleAutocomplete, DevConsoleAutocompleteBinding, and DevConsoleAutocompleteRegistry. - Implemented functionality for localized title matching and display, enabling more user-friendly autocomplete suggestions. - Registered new patches to apply these enhancements during dev-console command execution, improving overall usability and flexibility. - Created a catalog for mapping model entry IDs to localized titles, enhancing the autocomplete experience with contextual information.
1 parent a32527c commit b157f5d

14 files changed

Lines changed: 809 additions & 57 deletions
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using MegaCrit.Sts2.Core.DevConsole;
2+
using MegaCrit.Sts2.Core.DevConsole.ConsoleCommands;
3+
4+
namespace STS2RitsuLib.Diagnostics.DevConsole
5+
{
6+
/// <summary>
7+
/// Public entry point for registering and applying dev-console autocomplete enhancements.
8+
/// </summary>
9+
public static class DevConsoleAutocomplete
10+
{
11+
/// <summary>
12+
/// Registers a command-argument autocomplete binding.
13+
/// </summary>
14+
public static void Register(DevConsoleAutocompleteBinding binding)
15+
{
16+
DevConsoleAutocompleteRegistry.Register(binding);
17+
}
18+
19+
/// <summary>
20+
/// Registers enhancements for a fixed argument index on a command.
21+
/// </summary>
22+
public static void Register(
23+
string commandName,
24+
int argumentIndex,
25+
DevConsoleAutocompleteEnhancements enhancements,
26+
Func<DevConsoleAutocompleteContext, bool>? appliesWhen = null)
27+
{
28+
DevConsoleAutocompleteRegistry.Register(commandName, argumentIndex, enhancements, appliesWhen);
29+
}
30+
31+
/// <summary>
32+
/// Registers enhancements selected by <paramref name="appliesWhen" />.
33+
/// </summary>
34+
public static void Register(
35+
string commandName,
36+
DevConsoleAutocompleteEnhancements enhancements,
37+
Func<DevConsoleAutocompleteContext, bool> appliesWhen)
38+
{
39+
DevConsoleAutocompleteRegistry.Register(commandName, enhancements, appliesWhen);
40+
}
41+
42+
/// <summary>
43+
/// Resolves enhancements for a completion call.
44+
/// </summary>
45+
public static DevConsoleAutocompleteEnhancements Resolve(
46+
AbstractConsoleCmd command,
47+
string[] completedArgs,
48+
int argumentIndex)
49+
{
50+
return DevConsoleAutocompleteRegistry.Resolve(command, completedArgs, argumentIndex);
51+
}
52+
53+
/// <summary>
54+
/// Builds the match predicate chain for manual <c>CompleteArgument</c> usage in mod commands.
55+
/// </summary>
56+
public static Func<string, string, bool>? BuildMatchPredicate(
57+
AbstractConsoleCmd command,
58+
string[] completedArgs,
59+
Func<string, string, bool>? inner = null)
60+
{
61+
return DevConsoleAutocompleteEnhancer.BuildMatchPredicate(
62+
Resolve(command, completedArgs, completedArgs.Length),
63+
inner);
64+
}
65+
66+
/// <summary>
67+
/// Applies result enhancements for manual <c>CompleteArgument</c> usage in mod commands.
68+
/// </summary>
69+
public static void ApplyToResult(
70+
AbstractConsoleCmd command,
71+
string[] completedArgs,
72+
ref CompletionResult result)
73+
{
74+
DevConsoleAutocompleteEnhancer.ApplyToResult(
75+
ref result,
76+
Resolve(command, completedArgs, result.ArgumentIndex));
77+
}
78+
}
79+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace STS2RitsuLib.Diagnostics.DevConsole
2+
{
3+
/// <summary>
4+
/// Binds autocomplete enhancements to a command argument slot.
5+
/// </summary>
6+
public sealed class DevConsoleAutocompleteBinding
7+
{
8+
/// <summary>
9+
/// Dev-console command name (for example <c>card</c>).
10+
/// </summary>
11+
public required string CommandName { get; init; }
12+
13+
/// <summary>
14+
/// When set, only this argument index receives the enhancements.
15+
/// </summary>
16+
public int? ArgumentIndex { get; init; }
17+
18+
/// <summary>
19+
/// Optional extra guard based on completed arguments and command state.
20+
/// </summary>
21+
public Func<DevConsoleAutocompleteContext, bool>? AppliesWhen { get; init; }
22+
23+
/// <summary>
24+
/// Enhancements applied when this binding matches.
25+
/// </summary>
26+
public DevConsoleAutocompleteEnhancements Enhancements { get; init; }
27+
}
28+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using MegaCrit.Sts2.Core.DevConsole.ConsoleCommands;
2+
3+
namespace STS2RitsuLib.Diagnostics.DevConsole
4+
{
5+
/// <summary>
6+
/// Identifies a dev-console <see cref="AbstractConsoleCmd.CompleteArgument" /> invocation.
7+
/// </summary>
8+
public sealed class DevConsoleAutocompleteContext
9+
{
10+
/// <summary>
11+
/// Creates a context for the active completion call.
12+
/// </summary>
13+
public DevConsoleAutocompleteContext(
14+
AbstractConsoleCmd command,
15+
string[] completedArgs,
16+
int argumentIndex)
17+
{
18+
Command = command ?? throw new ArgumentNullException(nameof(command));
19+
CompletedArgs = completedArgs ?? throw new ArgumentNullException(nameof(completedArgs));
20+
ArgumentIndex = argumentIndex;
21+
}
22+
23+
/// <summary>
24+
/// Console command producing completions.
25+
/// </summary>
26+
public AbstractConsoleCmd Command { get; }
27+
28+
/// <summary>
29+
/// Arguments already present before the token being completed.
30+
/// </summary>
31+
public IReadOnlyList<string> CompletedArgs { get; }
32+
33+
/// <summary>
34+
/// Zero-based index of the argument being completed.
35+
/// </summary>
36+
public int ArgumentIndex { get; }
37+
38+
/// <summary>
39+
/// Dev-console command name.
40+
/// </summary>
41+
public string CommandName => Command.CmdName;
42+
}
43+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
namespace STS2RitsuLib.Diagnostics.DevConsole
2+
{
3+
/// <summary>
4+
/// Built-in dev-console autocomplete bindings for vanilla model-entry-id arguments.
5+
/// </summary>
6+
internal static class DevConsoleAutocompleteDefaults
7+
{
8+
public static void Register()
9+
{
10+
RegisterFirstModelEntryId("card");
11+
RegisterFirstModelEntryId("potion");
12+
RegisterFirstModelEntryId("event");
13+
RegisterFirstModelEntryId("ancient");
14+
RegisterFirstModelEntryId("fight");
15+
RegisterFirstModelEntryId("afflict");
16+
RegisterFirstModelEntryId("enchant");
17+
RegisterFirstModelEntryId("remove_card");
18+
19+
DevConsoleAutocompleteRegistry.Register(
20+
"relic",
21+
DevConsoleAutocompleteEnhancements.RitsuLibModEntryId,
22+
IsDirectRelicIdArgument);
23+
24+
DevConsoleAutocompleteRegistry.Register(
25+
"relic",
26+
DevConsoleAutocompleteEnhancements.RitsuLibModEntryId,
27+
IsRelicIdAfterSubcommand);
28+
}
29+
30+
private static void RegisterFirstModelEntryId(string commandName)
31+
{
32+
DevConsoleAutocompleteRegistry.Register(
33+
commandName,
34+
DevConsoleAutocompleteEnhancements.RitsuLibModEntryId,
35+
IsFirstModelEntryIdArgument);
36+
}
37+
38+
private static bool IsFirstModelEntryIdArgument(DevConsoleAutocompleteContext context)
39+
{
40+
return context is { ArgumentIndex: 0, CompletedArgs.Count: 0 };
41+
}
42+
43+
private static bool IsDirectRelicIdArgument(DevConsoleAutocompleteContext context)
44+
{
45+
return context is { ArgumentIndex: 0, CompletedArgs.Count: 0 };
46+
}
47+
48+
private static bool IsRelicIdAfterSubcommand(DevConsoleAutocompleteContext context)
49+
{
50+
if (context.ArgumentIndex != 1 || context.CompletedArgs.Count != 1)
51+
return false;
52+
53+
var subcommand = context.CompletedArgs[0];
54+
return subcommand.Equals("add", StringComparison.OrdinalIgnoreCase) ||
55+
subcommand.Equals("remove", StringComparison.OrdinalIgnoreCase);
56+
}
57+
}
58+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
namespace STS2RitsuLib.Diagnostics.DevConsole
2+
{
3+
/// <summary>
4+
/// Formats dev-console autocomplete candidates with optional localized suffix labels.
5+
/// </summary>
6+
public static class DevConsoleAutocompleteDisplay
7+
{
8+
internal const string SuffixOpener = " (";
9+
10+
/// <summary>
11+
/// Appends <c> (localized-title)</c> to <paramref name="entryId" /> when a localized title exists.
12+
/// </summary>
13+
public static string FormatCandidate(string entryId)
14+
{
15+
ArgumentException.ThrowIfNullOrWhiteSpace(entryId);
16+
17+
var title = DevConsoleModelIdAutocompleteCatalog.TryGetLocalizedTitle(entryId);
18+
return string.IsNullOrWhiteSpace(title)
19+
? entryId
20+
: $"{entryId}{SuffixOpener}{SanitizeSuffix(title)})";
21+
}
22+
23+
/// <summary>
24+
/// Strips a trailing localized suffix from a decorated autocomplete candidate.
25+
/// </summary>
26+
public static string StripLocalizedSuffix(string candidate)
27+
{
28+
if (string.IsNullOrWhiteSpace(candidate))
29+
return candidate;
30+
31+
var suffixStart = candidate.LastIndexOf(SuffixOpener, StringComparison.Ordinal);
32+
if (suffixStart < 0 || !candidate.EndsWith(')'))
33+
return candidate;
34+
35+
return candidate[..suffixStart];
36+
}
37+
38+
internal static string SanitizeSuffix(string title)
39+
{
40+
return title.Replace(')', '\uFF09').Trim();
41+
}
42+
43+
internal static string ComputeCommonPrefix(IReadOnlyList<string> entryIds, string commandPrefix)
44+
{
45+
switch (entryIds.Count)
46+
{
47+
case 0:
48+
return string.Empty;
49+
case 1:
50+
return commandPrefix + entryIds[0] + " ";
51+
}
52+
53+
var minLength = entryIds.Min(static id => id.Length);
54+
var first = entryIds[0];
55+
var sharedLength = 0;
56+
57+
for (var i = 0; i < minLength; i++)
58+
{
59+
var ch = first[i];
60+
if (entryIds.Any(id => char.ToLowerInvariant(id[i]) != char.ToLowerInvariant(ch)))
61+
break;
62+
63+
sharedLength = i + 1;
64+
}
65+
66+
return sharedLength > 0
67+
? commandPrefix + first[..sharedLength]
68+
: string.Empty;
69+
}
70+
}
71+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
namespace STS2RitsuLib.Diagnostics.DevConsole
2+
{
3+
/// <summary>
4+
/// Dev-console autocomplete behaviors that can be bound per command argument.
5+
/// </summary>
6+
[Flags]
7+
public enum DevConsoleAutocompleteEnhancements
8+
{
9+
/// <summary>
10+
/// No enhancements.
11+
/// </summary>
12+
None = 0,
13+
14+
/// <summary>
15+
/// Allows matching candidates by localized title text, not only entry id prefix.
16+
/// </summary>
17+
LocalizedTitleMatch = 1 << 0,
18+
19+
/// <summary>
20+
/// Appends <c> (localized-title)</c> to displayed candidates and keeps
21+
/// <see cref="MegaCrit.Sts2.Core.DevConsole.CompletionResult.CommonPrefix" /> canonical.
22+
/// </summary>
23+
LocalizedDisplayLabels = 1 << 1,
24+
25+
/// <summary>
26+
/// Enables tail shorthand matching for ritsulib-registered mod entry ids when no custom matcher is supplied.
27+
/// </summary>
28+
RitsuLibOwnedIdShorthandMatch = 1 << 2,
29+
30+
/// <summary>
31+
/// Removes duplicate candidates while preserving order.
32+
/// </summary>
33+
DeduplicateCandidates = 1 << 3,
34+
35+
/// <summary>
36+
/// Localized title matching and display labels for model entry ids.
37+
/// </summary>
38+
ModelEntryId = LocalizedTitleMatch | LocalizedDisplayLabels | DeduplicateCandidates,
39+
40+
/// <summary>
41+
/// <see cref="ModelEntryId" /> plus ritsulib-owned id shorthand matching.
42+
/// </summary>
43+
RitsuLibModEntryId = ModelEntryId | RitsuLibOwnedIdShorthandMatch,
44+
}
45+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using MegaCrit.Sts2.Core.DevConsole;
2+
3+
namespace STS2RitsuLib.Diagnostics.DevConsole
4+
{
5+
/// <summary>
6+
/// Applies registered dev-console autocomplete enhancements to matchers and results.
7+
/// </summary>
8+
public static class DevConsoleAutocompleteEnhancer
9+
{
10+
/// <summary>
11+
/// Builds a match predicate chain for <paramref name="enhancements" />.
12+
/// </summary>
13+
public static Func<string, string, bool>? BuildMatchPredicate(
14+
DevConsoleAutocompleteEnhancements enhancements,
15+
Func<string, string, bool>? inner = null)
16+
{
17+
if (enhancements == DevConsoleAutocompleteEnhancements.None)
18+
return inner;
19+
20+
var predicate = inner;
21+
22+
if (enhancements.HasFlag(DevConsoleAutocompleteEnhancements.RitsuLibOwnedIdShorthandMatch) &&
23+
predicate == null)
24+
predicate = DevConsoleAutocompleteOwnedIdMatch.Match;
25+
26+
if (enhancements.HasFlag(DevConsoleAutocompleteEnhancements.LocalizedTitleMatch))
27+
predicate = DevConsoleAutocompleteMatchExtensions.WithLocalizedModelTitleMatch(predicate);
28+
29+
return predicate;
30+
}
31+
32+
/// <summary>
33+
/// Applies result-side enhancements such as localized labels and de-duplication.
34+
/// </summary>
35+
public static void ApplyToResult(
36+
ref CompletionResult result,
37+
DevConsoleAutocompleteEnhancements enhancements)
38+
{
39+
if (enhancements == DevConsoleAutocompleteEnhancements.None || result.Candidates.Count == 0)
40+
return;
41+
42+
if (enhancements.HasFlag(DevConsoleAutocompleteEnhancements.LocalizedDisplayLabels))
43+
DevConsoleAutocompleteMatchExtensions.ApplyLocalizedDisplayLabels(ref result);
44+
45+
if (enhancements.HasFlag(DevConsoleAutocompleteEnhancements.DeduplicateCandidates))
46+
result.Candidates = result.Candidates
47+
.Distinct(StringComparer.OrdinalIgnoreCase)
48+
.ToList();
49+
}
50+
}
51+
}

0 commit comments

Comments
 (0)