| @@ -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); | ||||