@@ -0,0 +1,26 @@ | |||||
using System.Collections.Generic; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.Commands | |||||
{ | |||||
public struct CommandMatch | |||||
{ | |||||
public CommandInfo Command { get; } | |||||
public string Alias { get; } | |||||
public CommandMatch(CommandInfo command, string alias) | |||||
{ | |||||
Command = command; | |||||
Alias = alias; | |||||
} | |||||
public Task<PreconditionResult> CheckPreconditionsAsync(CommandContext context, IDependencyMap map = null) | |||||
=> Command.CheckPreconditionsAsync(context, map); | |||||
public Task<ParseResult> ParseAsync(CommandContext context, SearchResult searchResult, PreconditionResult? preconditionResult = null) | |||||
=> Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult); | |||||
public Task<ExecuteResult> ExecuteAsync(CommandContext context, IEnumerable<object> argList, IEnumerable<object> paramList, IDependencyMap map) | |||||
=> Command.ExecuteAsync(context, argList, paramList, map); | |||||
public Task<ExecuteResult> ExecuteAsync(CommandContext context, ParseResult parseResult, IDependencyMap map) | |||||
=> Command.ExecuteAsync(context, parseResult, map); | |||||
} | |||||
} |
@@ -21,6 +21,7 @@ namespace Discord.Commands | |||||
private readonly CommandMap _map; | private readonly CommandMap _map; | ||||
internal readonly bool _caseSensitive; | internal readonly bool _caseSensitive; | ||||
internal readonly char _separatorChar; | |||||
internal readonly RunMode _defaultRunMode; | internal readonly RunMode _defaultRunMode; | ||||
public IEnumerable<ModuleInfo> Modules => _moduleDefs.Select(x => x); | public IEnumerable<ModuleInfo> Modules => _moduleDefs.Select(x => x); | ||||
@@ -30,10 +31,14 @@ namespace Discord.Commands | |||||
public CommandService() : this(new CommandServiceConfig()) { } | public CommandService() : this(new CommandServiceConfig()) { } | ||||
public CommandService(CommandServiceConfig config) | public CommandService(CommandServiceConfig config) | ||||
{ | { | ||||
_caseSensitive = config.CaseSensitiveCommands; | |||||
_separatorChar = config.SeparatorChar; | |||||
_defaultRunMode = config.DefaultRunMode; | |||||
_moduleLock = new SemaphoreSlim(1, 1); | _moduleLock = new SemaphoreSlim(1, 1); | ||||
_typedModuleDefs = new ConcurrentDictionary<Type, ModuleInfo>(); | _typedModuleDefs = new ConcurrentDictionary<Type, ModuleInfo>(); | ||||
_moduleDefs = new ConcurrentBag<ModuleInfo>(); | _moduleDefs = new ConcurrentBag<ModuleInfo>(); | ||||
_map = new CommandMap(); | |||||
_map = new CommandMap(this); | |||||
_typeReaders = new ConcurrentDictionary<Type, ConcurrentDictionary<Type, TypeReader>>(); | _typeReaders = new ConcurrentDictionary<Type, ConcurrentDictionary<Type, TypeReader>>(); | ||||
_defaultTypeReaders = new ConcurrentDictionary<Type, TypeReader> | _defaultTypeReaders = new ConcurrentDictionary<Type, TypeReader> | ||||
@@ -57,9 +62,6 @@ namespace Discord.Commands | |||||
}; | }; | ||||
foreach (var type in PrimitiveParsers.SupportedTypes) | foreach (var type in PrimitiveParsers.SupportedTypes) | ||||
_defaultTypeReaders[type] = PrimitiveTypeReader.Create(type); | _defaultTypeReaders[type] = PrimitiveTypeReader.Create(type); | ||||
_caseSensitive = config.CaseSensitiveCommands; | |||||
_defaultRunMode = config.DefaultRunMode; | |||||
} | } | ||||
//Modules | //Modules | ||||
@@ -214,7 +216,7 @@ namespace Discord.Commands | |||||
public SearchResult Search(CommandContext context, string input) | public SearchResult Search(CommandContext context, string input) | ||||
{ | { | ||||
string searchInput = _caseSensitive ? input : input.ToLowerInvariant(); | string searchInput = _caseSensitive ? input : input.ToLowerInvariant(); | ||||
var matches = _map.GetCommands(searchInput).OrderByDescending(x => x.Priority).ToImmutableArray(); | |||||
var matches = _map.GetCommands(searchInput).OrderByDescending(x => x.Command.Priority).ToImmutableArray(); | |||||
if (matches.Length > 0) | if (matches.Length > 0) | ||||
return SearchResult.FromSuccess(input, matches); | return SearchResult.FromSuccess(input, matches); | ||||
@@ -269,7 +271,7 @@ namespace Discord.Commands | |||||
} | } | ||||
} | } | ||||
return await commands[i].Execute(context, parseResult, dependencyMap).ConfigureAwait(false); | |||||
return await commands[i].ExecuteAsync(context, parseResult, dependencyMap).ConfigureAwait(false); | |||||
} | } | ||||
return SearchResult.FromError(CommandError.UnknownCommand, "This input does not match any overload."); | return SearchResult.FromError(CommandError.UnknownCommand, "This input does not match any overload."); | ||||
@@ -4,6 +4,8 @@ | |||||
{ | { | ||||
/// <summary> The default RunMode commands should have, if one is not specified on the Command attribute or builder. </summary> | /// <summary> The default RunMode commands should have, if one is not specified on the Command attribute or builder. </summary> | ||||
public RunMode DefaultRunMode { get; set; } = RunMode.Sync; | public RunMode DefaultRunMode { get; set; } = RunMode.Sync; | ||||
public char SeparatorChar { get; set; } = ' '; | |||||
/// <summary> Should commands be case-sensitive? </summary> | /// <summary> Should commands be case-sensitive? </summary> | ||||
public bool CaseSensitiveCommands { get; set; } = false; | public bool CaseSensitiveCommands { get; set; } = false; | ||||
} | } | ||||
@@ -44,7 +44,12 @@ namespace Discord.Commands | |||||
// both command and module provide aliases | // both command and module provide aliases | ||||
if (module.Aliases.Count > 0 && builder.Aliases.Count > 0) | if (module.Aliases.Count > 0 && builder.Aliases.Count > 0) | ||||
Aliases = module.Aliases.Permutate(builder.Aliases, (first, second) => second != null ? first + " " + second : first).Select(x => service._caseSensitive ? x : x.ToLowerInvariant()).ToImmutableArray(); | |||||
{ | |||||
Aliases = module.Aliases | |||||
.Permutate(builder.Aliases, (first, second) => second != null ? first + service._separatorChar + second : first) | |||||
.Select(x => service._caseSensitive ? x : x.ToLowerInvariant()) | |||||
.ToImmutableArray(); | |||||
} | |||||
// only module provides aliases | // only module provides aliases | ||||
else if (module.Aliases.Count > 0) | else if (module.Aliases.Count > 0) | ||||
Aliases = module.Aliases.Select(x => service._caseSensitive ? x : x.ToLowerInvariant()).ToImmutableArray(); | Aliases = module.Aliases.Select(x => service._caseSensitive ? x : x.ToLowerInvariant()).ToImmutableArray(); | ||||
@@ -84,33 +89,19 @@ namespace Discord.Commands | |||||
return PreconditionResult.FromSuccess(); | return PreconditionResult.FromSuccess(); | ||||
} | } | ||||
public async Task<ParseResult> ParseAsync(CommandContext context, SearchResult searchResult, PreconditionResult? preconditionResult = null) | |||||
public async Task<ParseResult> ParseAsync(CommandContext context, int startIndex, SearchResult searchResult, PreconditionResult? preconditionResult = null) | |||||
{ | { | ||||
if (!searchResult.IsSuccess) | if (!searchResult.IsSuccess) | ||||
return ParseResult.FromError(searchResult); | return ParseResult.FromError(searchResult); | ||||
if (preconditionResult != null && !preconditionResult.Value.IsSuccess) | if (preconditionResult != null && !preconditionResult.Value.IsSuccess) | ||||
return ParseResult.FromError(preconditionResult.Value); | return ParseResult.FromError(preconditionResult.Value); | ||||
string input = searchResult.Text; | |||||
var matchingAliases = Aliases.Where(alias => input.StartsWith(alias)).ToArray(); | |||||
string matchingAlias = null; | |||||
foreach (string alias in matchingAliases) | |||||
{ | |||||
if (alias.Length > matchingAlias.Length) | |||||
matchingAlias = alias; | |||||
} | |||||
if (matchingAlias == null) | |||||
return ParseResult.FromError(CommandError.ParseFailed, "Unable to find matching alias"); | |||||
input = input.Substring(matchingAlias.Length); | |||||
string input = searchResult.Text.Substring(startIndex); | |||||
return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false); | return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false); | ||||
} | } | ||||
public Task<ExecuteResult> Execute(CommandContext context, ParseResult parseResult, IDependencyMap map) | |||||
public Task<ExecuteResult> ExecuteAsync(CommandContext context, ParseResult parseResult, IDependencyMap map) | |||||
{ | { | ||||
if (!parseResult.IsSuccess) | if (!parseResult.IsSuccess) | ||||
return Task.FromResult(ExecuteResult.FromError(parseResult)); | return Task.FromResult(ExecuteResult.FromError(parseResult)); | ||||
@@ -4,36 +4,30 @@ namespace Discord.Commands | |||||
{ | { | ||||
internal class CommandMap | internal class CommandMap | ||||
{ | { | ||||
private readonly CommandService _service; | |||||
private readonly CommandMapNode _root; | private readonly CommandMapNode _root; | ||||
private static readonly string[] _blankAliases = new[] { "" }; | private static readonly string[] _blankAliases = new[] { "" }; | ||||
public CommandMap() | |||||
public CommandMap(CommandService service) | |||||
{ | { | ||||
_service = service; | |||||
_root = new CommandMapNode(""); | _root = new CommandMapNode(""); | ||||
} | } | ||||
public void AddCommand(CommandInfo command) | public void AddCommand(CommandInfo command) | ||||
{ | { | ||||
foreach (string text in GetAliases(command)) | |||||
_root.AddCommand(text, 0, command); | |||||
foreach (string text in command.Aliases) | |||||
_root.AddCommand(_service, text, 0, command); | |||||
} | } | ||||
public void RemoveCommand(CommandInfo command) | public void RemoveCommand(CommandInfo command) | ||||
{ | { | ||||
foreach (string text in GetAliases(command)) | |||||
_root.RemoveCommand(text, 0, command); | |||||
foreach (string text in command.Aliases) | |||||
_root.RemoveCommand(_service, text, 0, command); | |||||
} | } | ||||
public IEnumerable<CommandInfo> GetCommands(string text) | |||||
public IEnumerable<CommandMatch> GetCommands(string text) | |||||
{ | { | ||||
return _root.GetCommands(text, 0); | |||||
} | |||||
private IReadOnlyList<string> GetAliases(CommandInfo command) | |||||
{ | |||||
var aliases = command.Aliases; | |||||
if (aliases.Count == 0) | |||||
return _blankAliases; | |||||
return aliases; | |||||
return _root.GetCommands(_service, text, 0, text != ""); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -7,7 +7,7 @@ namespace Discord.Commands | |||||
{ | { | ||||
internal class CommandMapNode | internal class CommandMapNode | ||||
{ | { | ||||
private static readonly char[] _whitespaceChars = new char[] { ' ', '\r', '\n' }; | |||||
private static readonly char[] _whitespaceChars = new[] { ' ', '\r', '\n' }; | |||||
private readonly ConcurrentDictionary<string, CommandMapNode> _nodes; | private readonly ConcurrentDictionary<string, CommandMapNode> _nodes; | ||||
private readonly string _name; | private readonly string _name; | ||||
@@ -23,9 +23,9 @@ namespace Discord.Commands | |||||
_commands = ImmutableArray.Create<CommandInfo>(); | _commands = ImmutableArray.Create<CommandInfo>(); | ||||
} | } | ||||
public void AddCommand(string text, int index, CommandInfo command) | |||||
public void AddCommand(CommandService service, string text, int index, CommandInfo command) | |||||
{ | { | ||||
int nextSpace = NextWhitespace(text, index); | |||||
int nextSegment = NextSegment(text, index, service._separatorChar); | |||||
string name; | string name; | ||||
lock (_lockObj) | lock (_lockObj) | ||||
@@ -38,19 +38,20 @@ namespace Discord.Commands | |||||
} | } | ||||
else | else | ||||
{ | { | ||||
if (nextSpace == -1) | |||||
if (nextSegment == -1) | |||||
name = text.Substring(index); | name = text.Substring(index); | ||||
else | else | ||||
name = text.Substring(index, nextSpace - index); | |||||
name = text.Substring(index, nextSegment - index); | |||||
var nextNode = _nodes.GetOrAdd(name, x => new CommandMapNode(x)); | |||||
nextNode.AddCommand(nextSpace == -1 ? "" : text, nextSpace + 1, command); | |||||
string fullName = _name == "" ? name : _name + service._separatorChar + name; | |||||
var nextNode = _nodes.GetOrAdd(name, x => new CommandMapNode(fullName)); | |||||
nextNode.AddCommand(service, nextSegment == -1 ? "" : text, nextSegment + 1, command); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
public void RemoveCommand(string text, int index, CommandInfo command) | |||||
public void RemoveCommand(CommandService service, string text, int index, CommandInfo command) | |||||
{ | { | ||||
int nextSpace = NextWhitespace(text, index); | |||||
int nextSegment = NextSegment(text, index, service._separatorChar); | |||||
string name; | string name; | ||||
lock (_lockObj) | lock (_lockObj) | ||||
@@ -59,15 +60,15 @@ namespace Discord.Commands | |||||
_commands = _commands.Remove(command); | _commands = _commands.Remove(command); | ||||
else | else | ||||
{ | { | ||||
if (nextSpace == -1) | |||||
if (nextSegment == -1) | |||||
name = text.Substring(index); | name = text.Substring(index); | ||||
else | else | ||||
name = text.Substring(index, nextSpace - index); | |||||
name = text.Substring(index, nextSegment - index); | |||||
CommandMapNode nextNode; | CommandMapNode nextNode; | ||||
if (_nodes.TryGetValue(name, out nextNode)) | if (_nodes.TryGetValue(name, out nextNode)) | ||||
{ | { | ||||
nextNode.RemoveCommand(nextSpace == -1 ? "" : text, nextSpace + 1, command); | |||||
nextNode.RemoveCommand(service, nextSegment == -1 ? "" : text, nextSegment + 1, command); | |||||
if (nextNode.IsEmpty) | if (nextNode.IsEmpty) | ||||
_nodes.TryRemove(name, out nextNode); | _nodes.TryRemove(name, out nextNode); | ||||
} | } | ||||
@@ -75,39 +76,58 @@ namespace Discord.Commands | |||||
} | } | ||||
} | } | ||||
public IEnumerable<CommandInfo> GetCommands(string text, int index) | |||||
public IEnumerable<CommandMatch> GetCommands(CommandService service, string text, int index, bool visitChildren = true) | |||||
{ | { | ||||
int nextSpace = NextWhitespace(text, index); | |||||
string name; | |||||
var commands = _commands; | var commands = _commands; | ||||
for (int i = 0; i < commands.Length; i++) | for (int i = 0; i < commands.Length; i++) | ||||
yield return _commands[i]; | |||||
yield return new CommandMatch(_commands[i], _name); | |||||
if (text != "") | |||||
if (visitChildren) | |||||
{ | { | ||||
if (nextSpace == -1) | |||||
string name; | |||||
CommandMapNode nextNode; | |||||
//Search for next segment | |||||
int nextSegment = NextSegment(text, index, service._separatorChar); | |||||
if (nextSegment == -1) | |||||
name = text.Substring(index); | name = text.Substring(index); | ||||
else | else | ||||
name = text.Substring(index, nextSpace - index); | |||||
CommandMapNode nextNode; | |||||
name = text.Substring(index, nextSegment - index); | |||||
if (_nodes.TryGetValue(name, out nextNode)) | if (_nodes.TryGetValue(name, out nextNode)) | ||||
{ | { | ||||
foreach (var cmd in nextNode.GetCommands(nextSpace == -1 ? "" : text, nextSpace + 1)) | |||||
foreach (var cmd in nextNode.GetCommands(service, nextSegment == -1 ? "" : text, nextSegment + 1, true)) | |||||
yield return cmd; | yield return cmd; | ||||
} | } | ||||
//Check if this is the last command segment before args | |||||
nextSegment = NextSegment(text, index, _whitespaceChars, service._separatorChar); | |||||
if (nextSegment != -1) | |||||
{ | |||||
name = text.Substring(index, nextSegment - index); | |||||
if (_nodes.TryGetValue(name, out nextNode)) | |||||
{ | |||||
foreach (var cmd in nextNode.GetCommands(service, nextSegment == -1 ? "" : text, nextSegment + 1, false)) | |||||
yield return cmd; | |||||
} | |||||
} | |||||
} | } | ||||
} | } | ||||
private static int NextWhitespace(string text, int startIndex) | |||||
private static int NextSegment(string text, int startIndex, char separator) | |||||
{ | |||||
return text.IndexOf(separator, startIndex); | |||||
} | |||||
private static int NextSegment(string text, int startIndex, char[] separators, char except) | |||||
{ | { | ||||
int lowest = int.MaxValue; | int lowest = int.MaxValue; | ||||
for (int i = 0; i < _whitespaceChars.Length; i++) | |||||
for (int i = 0; i < separators.Length; i++) | |||||
{ | { | ||||
int index = text.IndexOf(_whitespaceChars[i], startIndex); | |||||
if (index != -1 && index < lowest) | |||||
lowest = index; | |||||
if (separators[i] != except) | |||||
{ | |||||
int index = text.IndexOf(separators[i], startIndex); | |||||
if (index != -1 && index < lowest) | |||||
lowest = index; | |||||
} | |||||
} | } | ||||
return (lowest != int.MaxValue) ? lowest : -1; | return (lowest != int.MaxValue) ? lowest : -1; | ||||
} | } | ||||
@@ -7,14 +7,14 @@ namespace Discord.Commands | |||||
public struct SearchResult : IResult | public struct SearchResult : IResult | ||||
{ | { | ||||
public string Text { get; } | public string Text { get; } | ||||
public IReadOnlyList<CommandInfo> Commands { get; } | |||||
public IReadOnlyList<CommandMatch> Commands { get; } | |||||
public CommandError? Error { get; } | public CommandError? Error { get; } | ||||
public string ErrorReason { get; } | public string ErrorReason { get; } | ||||
public bool IsSuccess => !Error.HasValue; | public bool IsSuccess => !Error.HasValue; | ||||
private SearchResult(string text, IReadOnlyList<CommandInfo> commands, CommandError? error, string errorReason) | |||||
private SearchResult(string text, IReadOnlyList<CommandMatch> commands, CommandError? error, string errorReason) | |||||
{ | { | ||||
Text = text; | Text = text; | ||||
Commands = commands; | Commands = commands; | ||||
@@ -22,7 +22,7 @@ namespace Discord.Commands | |||||
ErrorReason = errorReason; | ErrorReason = errorReason; | ||||
} | } | ||||
public static SearchResult FromSuccess(string text, IReadOnlyList<CommandInfo> commands) | |||||
public static SearchResult FromSuccess(string text, IReadOnlyList<CommandMatch> commands) | |||||
=> new SearchResult(text, commands, null, null); | => new SearchResult(text, commands, null, null); | ||||
public static SearchResult FromError(CommandError error, string reason) | public static SearchResult FromError(CommandError error, string reason) | ||||
=> new SearchResult(null, null, error, reason); | => new SearchResult(null, null, error, reason); | ||||