diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs index 9b983fd1f..86f82c26b 100644 --- a/src/Discord.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -7,41 +7,35 @@ namespace Discord.Commands.Builders { public class CommandBuilder { - private readonly List _preconditions; - private readonly List _parameters; private readonly List _aliases; + private readonly List _overloads; public ModuleBuilder Module { get; } - internal Func Callback { get; set; } public string Name { get; set; } public string Summary { get; set; } public string Remarks { get; set; } - public RunMode RunMode { get; set; } - public int Priority { get; set; } - public IReadOnlyList Preconditions => _preconditions; - public IReadOnlyList Parameters => _parameters; public IReadOnlyList Aliases => _aliases; + public IReadOnlyList Overloads => _overloads; //Automatic internal CommandBuilder(ModuleBuilder module) { Module = module; - _preconditions = new List(); - _parameters = new List(); _aliases = new List(); + _overloads = new List(); } //User-defined - internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func callback) + internal CommandBuilder(ModuleBuilder module, string primaryAlias, Action defaultOverloadBuilder) : this(module) { - Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias)); - Discord.Preconditions.NotNull(callback, nameof(callback)); - - Callback = callback; + Preconditions.NotNull(primaryAlias, nameof(primaryAlias)); + Preconditions.NotNull(defaultOverloadBuilder, nameof(defaultOverloadBuilder)); + _aliases.Add(primaryAlias); + AddOverload(defaultOverloadBuilder); } public CommandBuilder WithName(string name) @@ -59,39 +53,17 @@ namespace Discord.Commands.Builders Remarks = remarks; return this; } - public CommandBuilder WithRunMode(RunMode runMode) - { - RunMode = runMode; - return this; - } - public CommandBuilder WithPriority(int priority) - { - Priority = priority; - return this; - } public CommandBuilder AddAliases(params string[] aliases) { _aliases.AddRange(aliases); return this; } - public CommandBuilder AddPrecondition(PreconditionAttribute precondition) + public CommandBuilder AddOverload(Action overloadBuilder) { - _preconditions.Add(precondition); - return this; - } - public CommandBuilder AddParameter(string name, Type type, Action createFunc) - { - var param = new ParameterBuilder(this, name, type); - createFunc(param); - _parameters.Add(param); - return this; - } - internal CommandBuilder AddParameter(Action createFunc) - { - var param = new ParameterBuilder(this); - createFunc(param); - _parameters.Add(param); + var overload = new OverloadBuilder(this); + overloadBuilder(overload); + _overloads.Add(overload); return this; } @@ -101,19 +73,6 @@ namespace Discord.Commands.Builders if (Name == null) Name = _aliases[0]; - if (_parameters.Count > 0) - { - var lastParam = _parameters[_parameters.Count - 1]; - - var firstMultipleParam = _parameters.FirstOrDefault(x => x.IsMultiple); - if ((firstMultipleParam != null) && (firstMultipleParam != lastParam)) - throw new InvalidOperationException("Only the last parameter in a command may have the Multiple flag."); - - var firstRemainderParam = _parameters.FirstOrDefault(x => x.IsRemainder); - if ((firstRemainderParam != null) && (firstRemainderParam != lastParam)) - throw new InvalidOperationException("Only the last parameter in a command may have the Remainder flag."); - } - return new CommandInfo(this, info, service); } } diff --git a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs index 4a4b83497..e1b42ac90 100644 --- a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -68,10 +68,10 @@ namespace Discord.Commands.Builders _preconditions.Add(precondition); return this; } - public ModuleBuilder AddCommand(string primaryAlias, Func callback, Action createFunc) + public ModuleBuilder AddCommand(string primaryAlias, Action defaultOverloadBuilder, Action createFunc = null) { - var builder = new CommandBuilder(this, primaryAlias, callback); - createFunc(builder); + var builder = new CommandBuilder(this, primaryAlias, defaultOverloadBuilder); + createFunc?.Invoke(builder); _commands.Add(builder); return this; } diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index e8dc60de8..2f89edbb0 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -104,15 +104,28 @@ namespace Discord.Commands var validCommands = typeInfo.DeclaredMethods.Where(x => IsValidCommandDefinition(x)); - foreach (var method in validCommands) + var groupedCommands = validCommands.GroupBy(x => x.GetCustomAttribute().Text); + + foreach (var overloads in groupedCommands) { - builder.AddCommand((command) => { - BuildCommand(command, typeInfo, method, service); + builder.AddCommand((command) => + { + foreach (var method in overloads) + { + command.AddOverload((overload) => + { + BuildOverload(overload, typeInfo, method, service); + }); + } + + var defaultOverload = overloads.OrderByDescending(x => x.GetCustomAttribute()?.Priority ?? 0).First(); + + BuildCommand(command, defaultOverload, service); }); } } - private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service) + private static void BuildCommand(CommandBuilder builder, MethodInfo method, CommandService service) { var attributes = method.GetCustomAttributes(); @@ -123,25 +136,36 @@ namespace Discord.Commands { var cmdAttr = attribute as CommandAttribute; builder.AddAliases(cmdAttr.Text); - builder.RunMode = cmdAttr.RunMode; builder.Name = builder.Name ?? cmdAttr.Text; } else if (attribute is NameAttribute) builder.Name = (attribute as NameAttribute).Text; - else if (attribute is PriorityAttribute) - builder.Priority = (attribute as PriorityAttribute).Priority; else if (attribute is SummaryAttribute) builder.Summary = (attribute as SummaryAttribute).Text; else if (attribute is RemarksAttribute) builder.Remarks = (attribute as RemarksAttribute).Text; else if (attribute is AliasAttribute) builder.AddAliases((attribute as AliasAttribute).Aliases); - else if (attribute is PreconditionAttribute) - builder.AddPrecondition(attribute as PreconditionAttribute); } if (builder.Name == null) builder.Name = method.Name; + } + + private static void BuildOverload(OverloadBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service) + { + var attributes = method.GetCustomAttributes(); + + foreach (var attribute in attributes) + { + // TODO: C#7 type switch + if (attribute is CommandAttribute) + builder.RunMode = (attribute as CommandAttribute).RunMode; + else if (attribute is PriorityAttribute) + builder.Priority = (attribute as PriorityAttribute).Priority; + else if (attribute is PreconditionAttribute) + builder.AddPrecondition(attribute as PreconditionAttribute); + } var parameters = method.GetParameters(); int pos = 0, count = parameters.Length; @@ -161,7 +185,8 @@ namespace Discord.Commands { return method.Invoke(instance, args) as Task ?? Task.CompletedTask; } - finally{ + finally + { (instance as IDisposable)?.Dispose(); } }; diff --git a/src/Discord.Net.Commands/Builders/OverloadBuilder.cs b/src/Discord.Net.Commands/Builders/OverloadBuilder.cs new file mode 100644 index 000000000..ef53c3a7d --- /dev/null +++ b/src/Discord.Net.Commands/Builders/OverloadBuilder.cs @@ -0,0 +1,83 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Discord.Commands.Builders +{ + public class OverloadBuilder + { + private readonly List _preconditions; + private readonly List _parameters; + + public CommandBuilder Command { get; } + internal Func Callback { get; set; } + + public RunMode RunMode { get; set; } + public int Priority { get; set; } + + public IReadOnlyList Preconditions => _preconditions; + public IReadOnlyList Parameters => _parameters; + + internal OverloadBuilder(CommandBuilder command) + { + Command = command; + + _preconditions = new List(); + _parameters = new List(); + } + + public OverloadBuilder WithRunMode(RunMode runMode) + { + RunMode = runMode; + return this; + } + + public OverloadBuilder WithPriority(int priority) + { + Priority = priority; + return this; + } + + public OverloadBuilder AddPrecondition(PreconditionAttribute precondition) + { + _preconditions.Add(precondition); + return this; + } + internal OverloadBuilder AddParameter(Action createFunc) + { + var param = new ParameterBuilder(Command); + createFunc(param); + _parameters.Add(param); + return this; + } + public OverloadBuilder AddParameter(string name, Type type, Action createFunc) + { + var param = new ParameterBuilder(Command, name, type); + createFunc(param); + _parameters.Add(param); + return this; + } + + internal OverloadInfo Build(CommandInfo info, CommandService service) + { + Discord.Preconditions.NotNull(Callback, nameof(Callback)); + + if (_parameters.Count > 0) + { + var lastParam = _parameters[_parameters.Count - 1]; + + var firstMultipleParam = _parameters.FirstOrDefault(x => x.IsMultiple); + if ((firstMultipleParam != null) && (firstMultipleParam != lastParam)) + throw new InvalidOperationException("Only the last parameter in a command may have the Multiple flag."); + + var firstRemainderParam = _parameters.FirstOrDefault(x => x.IsRemainder); + if ((firstRemainderParam != null) && (firstRemainderParam != lastParam)) + throw new InvalidOperationException("Only the last parameter in a command may have the Remainder flag."); + } + + return new OverloadInfo(this, info, service); + } + } +} diff --git a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs index d4cf598ec..1c6d1e7d1 100644 --- a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs @@ -87,7 +87,7 @@ namespace Discord.Commands.Builders return this; } - internal ParameterInfo Build(CommandInfo info) + internal ParameterInfo Build(OverloadInfo info) { if (TypeReader == null) throw new InvalidOperationException($"No default TypeReader found, one must be specified"); @@ -95,4 +95,4 @@ namespace Discord.Commands.Builders return new ParameterInfo(this, info, Command.Module.Service); } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index 53ea1330f..2c5d0a0cb 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -13,7 +13,7 @@ namespace Discord.Commands QuotedParameter } - public static async Task ParseArgs(CommandInfo command, CommandContext context, string input, int startPos) + public static async Task ParseArgs(OverloadInfo overload, CommandContext context, string input, int startPos) { ParameterInfo curParam = null; StringBuilder argBuilder = new StringBuilder(input.Length); @@ -66,7 +66,7 @@ namespace Discord.Commands else { if (curParam == null) - curParam = command.Parameters.Count > argList.Count ? command.Parameters[argList.Count] : null; + curParam = overload.Parameters.Count > argList.Count ? overload.Parameters[argList.Count] : null; if (curParam != null && curParam.IsRemainder) { @@ -145,9 +145,9 @@ namespace Discord.Commands return ParseResult.FromError(CommandError.ParseFailed, "A quoted parameter is incomplete"); //Add missing optionals - for (int i = argList.Count; i < command.Parameters.Count; i++) + for (int i = argList.Count; i < overload.Parameters.Count; i++) { - var param = command.Parameters[i]; + var param = overload.Parameters[i]; if (param.IsMultiple) continue; if (!param.IsOptional) @@ -155,7 +155,7 @@ namespace Discord.Commands argList.Add(TypeReaderResult.FromSuccess(param.DefaultValue)); } - return ParseResult.FromSuccess(argList.ToImmutable(), paramList.ToImmutable()); + return ParseResult.FromSuccess(overload, argList.ToImmutable(), paramList.ToImmutable()); } } } diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 9c2440b7c..966f7104b 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -39,8 +39,8 @@ namespace Discord.Commands _defaultTypeReaders = new ConcurrentDictionary { [typeof(bool)] = new SimpleTypeReader(), - [typeof(char)] = new SimpleTypeReader(), - [typeof(string)] = new SimpleTypeReader(), + [typeof(char)] = new SimpleTypeReader(0), + [typeof(string)] = new SimpleTypeReader(0), [typeof(byte)] = new SimpleTypeReader(), [typeof(sbyte)] = new SimpleTypeReader(), [typeof(ushort)] = new SimpleTypeReader(), @@ -228,8 +228,8 @@ namespace Discord.Commands public SearchResult Search(CommandContext context, string input) { string searchInput = _caseSensitive ? input : input.ToLowerInvariant(); - var matches = _map.GetCommands(searchInput).OrderByDescending(x => x.Priority).ToImmutableArray(); - + var matches = _map.GetCommands(input).OrderBy(x => x.Overloads.Average(y => y.Priority)).ToImmutableArray(); + if (matches.Length > 0) return SearchResult.FromSuccess(input, matches); else @@ -249,41 +249,67 @@ namespace Discord.Commands var commands = searchResult.Commands; for (int i = commands.Count - 1; i >= 0; i--) { - var preconditionResult = await commands[i].CheckPreconditionsAsync(context, dependencyMap).ConfigureAwait(false); - if (!preconditionResult.IsSuccess) + var command = commands[i]; + var overloads = command.Overloads.OrderBy(x => x.Priority).ToImmutableArray(); + + PreconditionResult preconditionResult = PreconditionResult.FromSuccess(); + for (int j = command.Overloads.Count - 1; i >= 0; i--) + { + preconditionResult = await overloads[j].CheckPreconditionsAsync(context, dependencyMap).ConfigureAwait(false); + if (!preconditionResult.IsSuccess) + { + if (commands.Count == 1) + return preconditionResult; + else + continue; + } + } + + var rawParseResults = new List(); + foreach (var overload in overloads) { - if (commands.Count == 1) - return preconditionResult; - else - continue; + rawParseResults.Add(await overload.ParseAsync(context, searchResult, preconditionResult).ConfigureAwait(false)); } - var parseResult = await commands[i].ParseAsync(context, searchResult, preconditionResult).ConfigureAwait(false); - if (!parseResult.IsSuccess) + //order by average score + var orderedParseResults = rawParseResults.OrderBy( + x => !x.IsSuccess ? 0 : + ( x.ArgValues.Count > 0 ? x.ArgValues.Average(y => y.Values.Max(z => z.Score)) : 0) + + (x.ParamValues.Count > 0 ? x.ParamValues.Average(y => y.Values.Max(z => z.Score)) : 0)); + + var parseResults = orderedParseResults.ToImmutableArray(); + + for (int j = parseResults.Length - 1; j >= 0; j--) { - if (parseResult.Error == CommandError.MultipleMatches) + var parseResult = parseResults[j]; + var overload = parseResult.Overload; + + if (!parseResult.IsSuccess) { - IReadOnlyList argList, paramList; - switch (multiMatchHandling) + if (parseResult.Error == CommandError.MultipleMatches) { - case MultiMatchHandling.Best: - argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); - paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); - parseResult = ParseResult.FromSuccess(argList, paramList); - break; + IReadOnlyList argList, paramList; + switch (multiMatchHandling) + { + case MultiMatchHandling.Best: + argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); + paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); + parseResult = ParseResult.FromSuccess(parseResult.Overload, argList, paramList); + break; + } } - } - if (!parseResult.IsSuccess) - { - if (commands.Count == 1) - return parseResult; - else - continue; + if (!parseResult.IsSuccess) + { + if (overloads.Length == 1) + return parseResult; + else + continue; + } } - } - return await commands[i].Execute(context, parseResult, dependencyMap).ConfigureAwait(false); + return await overload.Execute(context, parseResult, dependencyMap).ConfigureAwait(false); + } } return SearchResult.FromError(CommandError.UnknownCommand, "This input does not match any overload."); diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index a6ac50005..ada4cb2d5 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -5,43 +5,31 @@ using System.Collections.Immutable; using System.Collections.Concurrent; using System.Threading.Tasks; using System.Reflection; +using System.Diagnostics; using Discord.Commands.Builders; -using System.Diagnostics; namespace Discord.Commands { [DebuggerDisplay("{Name,nq}")] public class CommandInfo { - private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); - private static readonly ConcurrentDictionary, object>> _arrayConverters = new ConcurrentDictionary, object>>(); - - private readonly Func _action; - public ModuleInfo Module { get; } public string Name { get; } public string Summary { get; } public string Remarks { get; } - public int Priority { get; } - public bool HasVarArgs { get; } - public RunMode RunMode { get; } public IReadOnlyList Aliases { get; } - public IReadOnlyList Parameters { get; } - public IReadOnlyList Preconditions { get; } + public IReadOnlyList Overloads { get; } internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService service) { Module = module; - + Name = builder.Name; Summary = builder.Summary; Remarks = builder.Remarks; - RunMode = (builder.RunMode == RunMode.Default ? service._defaultRunMode : builder.RunMode); - Priority = builder.Priority; - // both command and module provide aliases 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(); @@ -55,148 +43,7 @@ namespace Discord.Commands else throw new InvalidOperationException("Cannot build a command without any aliases"); - Preconditions = builder.Preconditions.ToImmutableArray(); - - Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); - HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].IsMultiple : false; - - _action = builder.Callback; - } - - public async Task CheckPreconditionsAsync(CommandContext context, IDependencyMap map = null) - { - if (map == null) - map = DependencyMap.Empty; - - foreach (PreconditionAttribute precondition in Module.Preconditions) - { - var result = await precondition.CheckPermissions(context, this, map).ConfigureAwait(false); - if (!result.IsSuccess) - return result; - } - - foreach (PreconditionAttribute precondition in Preconditions) - { - var result = await precondition.CheckPermissions(context, this, map).ConfigureAwait(false); - if (!result.IsSuccess) - return result; - } - - return PreconditionResult.FromSuccess(); - } - - public async Task ParseAsync(CommandContext context, SearchResult searchResult, PreconditionResult? preconditionResult = null) - { - if (!searchResult.IsSuccess) - return ParseResult.FromError(searchResult); - if (preconditionResult != null && !preconditionResult.Value.IsSuccess) - return ParseResult.FromError(preconditionResult.Value); - - string input = searchResult.Text; - var matchingAliases = Aliases.Where(alias => input.StartsWith(alias)); - - string matchingAlias = ""; - foreach (string alias in matchingAliases) - { - if (alias.Length > matchingAlias.Length) - matchingAlias = alias; - } - - input = input.Substring(matchingAlias.Length); - - return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false); + Overloads = builder.Overloads.Select(x => x.Build(this, service)).ToImmutableArray(); } - - public Task Execute(CommandContext context, ParseResult parseResult, IDependencyMap map) - { - if (!parseResult.IsSuccess) - return Task.FromResult(ExecuteResult.FromError(parseResult)); - - var argList = new object[parseResult.ArgValues.Count]; - for (int i = 0; i < parseResult.ArgValues.Count; i++) - { - if (!parseResult.ArgValues[i].IsSuccess) - return Task.FromResult(ExecuteResult.FromError(parseResult.ArgValues[i])); - argList[i] = parseResult.ArgValues[i].Values.First().Value; - } - - var paramList = new object[parseResult.ParamValues.Count]; - for (int i = 0; i < parseResult.ParamValues.Count; i++) - { - if (!parseResult.ParamValues[i].IsSuccess) - return Task.FromResult(ExecuteResult.FromError(parseResult.ParamValues[i])); - paramList[i] = parseResult.ParamValues[i].Values.First().Value; - } - - return ExecuteAsync(context, argList, paramList, map); - } - public async Task ExecuteAsync(CommandContext context, IEnumerable argList, IEnumerable paramList, IDependencyMap map) - { - if (map == null) - map = DependencyMap.Empty; - - try - { - object[] args = GenerateArgs(argList, paramList); - - foreach (var parameter in Parameters) - { - var result = await parameter.CheckPreconditionsAsync(context, args, map).ConfigureAwait(false); - if (!result.IsSuccess) - return ExecuteResult.FromError(result); - } - - switch (RunMode) - { - case RunMode.Sync: //Always sync - await _action(context, args, map).ConfigureAwait(false); - break; - case RunMode.Mixed: //Sync until first await statement - var t1 = _action(context, args, map); - break; - case RunMode.Async: //Always async - var t2 = Task.Run(() => _action(context, args, map)); - break; - } - return ExecuteResult.FromSuccess(); - } - catch (Exception ex) - { - return ExecuteResult.FromError(ex); - } - } - - private object[] GenerateArgs(IEnumerable argList, IEnumerable paramsList) - { - int argCount = Parameters.Count; - var array = new object[Parameters.Count]; - if (HasVarArgs) - argCount--; - - int i = 0; - foreach (var arg in argList) - { - if (i == argCount) - throw new InvalidOperationException("Command was invoked with too many parameters"); - array[i++] = arg; - } - if (i < argCount) - throw new InvalidOperationException("Command was invoked with too few parameters"); - - if (HasVarArgs) - { - var func = _arrayConverters.GetOrAdd(Parameters[Parameters.Count - 1].Type, t => - { - var method = _convertParamsMethod.MakeGenericMethod(t); - return (Func, object>)method.CreateDelegate(typeof(Func, object>)); - }); - array[i] = func(paramsList); - } - - return array; - } - - private static T[] ConvertParamsList(IEnumerable paramsList) - => paramsList.Cast().ToArray(); } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Commands/Info/OverloadInfo.cs b/src/Discord.Net.Commands/Info/OverloadInfo.cs new file mode 100644 index 000000000..1e56506b6 --- /dev/null +++ b/src/Discord.Net.Commands/Info/OverloadInfo.cs @@ -0,0 +1,175 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using System.Reflection; +using System.Diagnostics; + +using Discord.Commands.Builders; + +namespace Discord.Commands +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class OverloadInfo + { + private static readonly MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); + private static readonly ConcurrentDictionary, object>> _arrayConverters = new ConcurrentDictionary, object>>(); + + private readonly Func _action; + + public CommandInfo Command { get; } + public int Priority { get; } + public bool HasVarArgs { get; } + public RunMode RunMode { get; } + + public IReadOnlyList Parameters { get; } + public IReadOnlyList Preconditions { get; } + + internal OverloadInfo(OverloadBuilder builder, CommandInfo command, CommandService service) + { + Command = command; + + RunMode = (builder.RunMode == RunMode.Default ? service._defaultRunMode : builder.RunMode); + Priority = builder.Priority; + + Preconditions = builder.Preconditions.ToImmutableArray(); + + Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); + HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].IsMultiple : false; + + _action = builder.Callback; + } + + public async Task CheckPreconditionsAsync(CommandContext context, IDependencyMap map = null) + { + if (map == null) + map = DependencyMap.Empty; + + foreach (PreconditionAttribute precondition in Command.Module.Preconditions) + { + var result = await precondition.CheckPermissions(context, Command, map).ConfigureAwait(false); + if (!result.IsSuccess) + return result; + } + + foreach (PreconditionAttribute precondition in Preconditions) + { + var result = await precondition.CheckPermissions(context, Command, map).ConfigureAwait(false); + if (!result.IsSuccess) + return result; + } + + return PreconditionResult.FromSuccess(); + } + + public async Task ParseAsync(CommandContext context, SearchResult searchResult, PreconditionResult? preconditionResult = null) + { + if (!searchResult.IsSuccess) + return ParseResult.FromError(searchResult); + if (preconditionResult != null && !preconditionResult.Value.IsSuccess) + return ParseResult.FromError(preconditionResult.Value); + + string input = searchResult.Text; + var matchingAliases = Command.Aliases.Where(alias => input.StartsWith(alias)); + + string matchingAlias = ""; + foreach (string alias in matchingAliases) + { + if (alias.Length > matchingAlias.Length) + matchingAlias = alias; + } + + input = input.Substring(matchingAlias.Length); + + return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false); + } + + public Task Execute(CommandContext context, ParseResult parseResult, IDependencyMap map) + { + if (!parseResult.IsSuccess) + return Task.FromResult(ExecuteResult.FromError(parseResult)); + + var argList = new object[parseResult.ArgValues.Count]; + for (int i = 0; i < parseResult.ArgValues.Count; i++) + { + if (!parseResult.ArgValues[i].IsSuccess) + return Task.FromResult(ExecuteResult.FromError(parseResult.ArgValues[i])); + argList[i] = parseResult.ArgValues[i].Values.First().Value; + } + + var paramList = new object[parseResult.ParamValues.Count]; + for (int i = 0; i < parseResult.ParamValues.Count; i++) + { + if (!parseResult.ParamValues[i].IsSuccess) + return Task.FromResult(ExecuteResult.FromError(parseResult.ParamValues[i])); + paramList[i] = parseResult.ParamValues[i].Values.First().Value; + } + + return ExecuteAsync(context, argList, paramList, map); + } + public async Task ExecuteAsync(CommandContext context, IEnumerable argList, IEnumerable paramList, IDependencyMap map) + { + if (map == null) + map = DependencyMap.Empty; + + try + { + var args = GenerateArgs(argList, paramList); + switch (RunMode) + { + case RunMode.Sync: //Always sync + await _action(context, args, map).ConfigureAwait(false); + break; + case RunMode.Mixed: //Sync until first await statement + var t1 = _action(context, args, map); + break; + case RunMode.Async: //Always async + var t2 = Task.Run(() => _action(context, args, map)); + break; + } + return ExecuteResult.FromSuccess(); + } + catch (Exception ex) + { + return ExecuteResult.FromError(ex); + } + } + + private object[] GenerateArgs(IEnumerable argList, IEnumerable paramsList) + { + int argCount = Parameters.Count; + var array = new object[Parameters.Count]; + if (HasVarArgs) + argCount--; + + int i = 0; + foreach (var arg in argList) + { + if (i == argCount) + throw new InvalidOperationException("Command was invoked with too many parameters"); + array[i++] = arg; + } + if (i < argCount) + throw new InvalidOperationException("Command was invoked with too few parameters"); + + if (HasVarArgs) + { + var func = _arrayConverters.GetOrAdd(Parameters[Parameters.Count - 1].Type, t => + { + var method = _convertParamsMethod.MakeGenericMethod(t); + return (Func, object>)method.CreateDelegate(typeof(Func, object>)); + }); + array[i] = func(paramsList); + } + + return array; + } + + private string DebuggerDisplay => $"{Command.Name} ({Priority}, {RunMode})"; + + private static T[] ConvertParamsList(IEnumerable paramsList) + => paramsList.Cast().ToArray(); + } +} diff --git a/src/Discord.Net.Commands/Info/ParameterInfo.cs b/src/Discord.Net.Commands/Info/ParameterInfo.cs index f8a97647a..152556ec3 100644 --- a/src/Discord.Net.Commands/Info/ParameterInfo.cs +++ b/src/Discord.Net.Commands/Info/ParameterInfo.cs @@ -3,16 +3,19 @@ using System.Linq; using System.Collections.Generic; using System.Collections.Immutable; using System.Threading.Tasks; +using System.Diagnostics; using Discord.Commands.Builders; namespace Discord.Commands { + [DebuggerDisplay("{DebuggerDisplay,nq}")] public class ParameterInfo { private readonly TypeReader _reader; + private readonly OverloadInfo _overload; - public CommandInfo Command { get; } + public CommandInfo Command => _overload.Command; public string Name { get; } public string Summary { get; } public bool IsOptional { get; } @@ -23,9 +26,9 @@ namespace Discord.Commands public IReadOnlyList Preconditions { get; } - internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandService service) + internal ParameterInfo(ParameterBuilder builder, OverloadInfo overload, CommandService service) { - Command = command; + _overload = overload; Name = builder.Name; Summary = builder.Summary; @@ -69,4 +72,4 @@ namespace Discord.Commands public override string ToString() => Name; private string DebuggerDisplay => $"{Name}{(IsOptional ? " (Optional)" : "")}{(IsRemainder ? " (Remainder)" : "")}"; } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Commands/Readers/SimpleTypeReader.cs b/src/Discord.Net.Commands/Readers/SimpleTypeReader.cs index ad939e59d..abef230e2 100644 --- a/src/Discord.Net.Commands/Readers/SimpleTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/SimpleTypeReader.cs @@ -6,16 +6,19 @@ namespace Discord.Commands { private readonly TryParseDelegate _tryParse; - public SimpleTypeReader() + private readonly float _score; + + public SimpleTypeReader(float score = 1) { _tryParse = PrimitiveParsers.Get(); + _score = score; } public override Task Read(CommandContext context, string input) { T value; if (_tryParse(input, out value)) - return Task.FromResult(TypeReaderResult.FromSuccess(value)); + return Task.FromResult(TypeReaderResult.FromSuccess(new TypeReaderValue(value, _score))); return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Failed to parse {typeof(T).Name}")); } } diff --git a/src/Discord.Net.Commands/Results/ParseResult.cs b/src/Discord.Net.Commands/Results/ParseResult.cs index 0f024cb44..00f15feeb 100644 --- a/src/Discord.Net.Commands/Results/ParseResult.cs +++ b/src/Discord.Net.Commands/Results/ParseResult.cs @@ -9,34 +9,37 @@ namespace Discord.Commands public IReadOnlyList ArgValues { get; } public IReadOnlyList ParamValues { get; } + public OverloadInfo Overload { get; } + public CommandError? Error { get; } public string ErrorReason { get; } public bool IsSuccess => !Error.HasValue; - private ParseResult(IReadOnlyList argValues, IReadOnlyList paramValue, CommandError? error, string errorReason) + private ParseResult(OverloadInfo overload, IReadOnlyList argValues, IReadOnlyList paramValue, CommandError? error, string errorReason) { + Overload = overload; ArgValues = argValues; ParamValues = paramValue; Error = error; ErrorReason = errorReason; } - public static ParseResult FromSuccess(IReadOnlyList argValues, IReadOnlyList paramValues) + public static ParseResult FromSuccess(OverloadInfo overload, IReadOnlyList argValues, IReadOnlyList paramValues) { for (int i = 0; i < argValues.Count; i++) { if (argValues[i].Values.Count > 1) - return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found."); + return new ParseResult(overload, argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found."); } for (int i = 0; i < paramValues.Count; i++) { if (paramValues[i].Values.Count > 1) - return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found."); + return new ParseResult(overload, argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found."); } - return new ParseResult(argValues, paramValues, null, null); + return new ParseResult(overload, argValues, paramValues, null, null); } - public static ParseResult FromSuccess(IReadOnlyList argValues, IReadOnlyList paramValues) + public static ParseResult FromSuccess(OverloadInfo overload, IReadOnlyList argValues, IReadOnlyList paramValues) { var argList = new TypeReaderResult[argValues.Count]; for (int i = 0; i < argValues.Count; i++) @@ -48,13 +51,13 @@ namespace Discord.Commands for (int i = 0; i < paramValues.Count; i++) paramList[i] = TypeReaderResult.FromSuccess(paramValues[i]); } - return new ParseResult(argList, paramList, null, null); + return new ParseResult(overload, argList, paramList, null, null); } public static ParseResult FromError(CommandError error, string reason) - => new ParseResult(null, null, error, reason); + => new ParseResult(null, null, null, error, reason); public static ParseResult FromError(IResult result) - => new ParseResult(null, null, result.Error, result.ErrorReason); + => new ParseResult(null, null, null, result.Error, result.ErrorReason); public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; private string DebuggerDisplay => IsSuccess ? $"Success ({ArgValues.Count}{(ParamValues.Count > 0 ? $" +{ParamValues.Count} Values" : "")})" : $"{Error}: {ErrorReason}";