| @@ -2,14 +2,18 @@ | |||
| using System.Threading.Tasks; | |||
| using System.Collections.Generic; | |||
| namespace Discord.Commands | |||
| namespace Discord.Commands.Builders | |||
| { | |||
| public class CommandBuilder | |||
| { | |||
| private List<PreconditionAttribute> preconditions; | |||
| private List<ParameterBuilder> parameters; | |||
| private List<string> aliases; | |||
| internal CommandBuilder(ModuleBuilder module, string prefix) | |||
| { | |||
| preconditions = new List<PreconditionAttribute>(); | |||
| parameters = new List<ParameterBuilder>(); | |||
| aliases = new List<string>(); | |||
| if (prefix != null) | |||
| @@ -21,16 +25,16 @@ namespace Discord.Commands | |||
| Module = module; | |||
| } | |||
| public string Name { get; set; } | |||
| public string Summary { get; set; } | |||
| public string Remarks { get; set; } | |||
| public Func<object[], Task> Callback { get; set; } | |||
| public Func<CommandContext, object[], Task> Callback { get; set; } | |||
| public ModuleBuilder Module { get; } | |||
| public List<PreconditionAttribute> Preconditions => preconditions; | |||
| public List<ParameterBuilder> Parameters => parameters; | |||
| public List<string> Aliases => aliases; | |||
| public CommandBuilder SetName(string name) | |||
| { | |||
| Name = name; | |||
| @@ -49,21 +53,33 @@ namespace Discord.Commands | |||
| return this; | |||
| } | |||
| public CommandBuilder SetCallback(Func<object[], Task> callback) | |||
| public CommandBuilder SetCallback(Func<CommandContext, object[], Task> callback) | |||
| { | |||
| Callback = callback; | |||
| return this; | |||
| } | |||
| public CommandBuilder AddPrecondition(PreconditionAttribute precondition) | |||
| { | |||
| preconditions.Add(precondition); | |||
| return this; | |||
| } | |||
| public CommandBuilder AddParameter(ParameterBuilder parameter) | |||
| { | |||
| parameters.Add(parameter); | |||
| return this; | |||
| } | |||
| public CommandBuilder AddAlias(string alias) | |||
| { | |||
| aliases.Add(alias); | |||
| return this; | |||
| } | |||
| public ModuleBuilder Done() | |||
| internal CommandInfo Build(ModuleInfo info, CommandService service) | |||
| { | |||
| return Module; | |||
| return new CommandInfo(this, info, service); | |||
| } | |||
| } | |||
| } | |||
| @@ -2,15 +2,15 @@ | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| namespace Discord.Commands | |||
| namespace Discord.Commands.Builders | |||
| { | |||
| public class ModuleBuilder | |||
| { | |||
| private List<CommandBuilder> commands; | |||
| private List<ModuleBuilder> submodules; | |||
| private List<PreconditionAttribute> preconditions; | |||
| private List<string> aliases; | |||
| public ModuleBuilder() | |||
| : this(null, null) | |||
| { } | |||
| @@ -27,6 +27,7 @@ namespace Discord.Commands | |||
| { | |||
| commands = new List<CommandBuilder>(); | |||
| submodules = new List<ModuleBuilder>(); | |||
| preconditions = new List<PreconditionAttribute>(); | |||
| aliases = new List<string>(); | |||
| if (prefix != null) | |||
| @@ -38,7 +39,6 @@ namespace Discord.Commands | |||
| ParentModule = parent; | |||
| } | |||
| public string Name { get; set; } | |||
| public string Summary { get; set; } | |||
| public string Remarks { get; set; } | |||
| @@ -46,9 +46,9 @@ namespace Discord.Commands | |||
| public List<CommandBuilder> Commands => commands; | |||
| public List<ModuleBuilder> Modules => submodules; | |||
| public List<PreconditionAttribute> Preconditions => preconditions; | |||
| public List<string> Aliases => aliases; | |||
| public ModuleBuilder SetName(string name) | |||
| { | |||
| Name = name; | |||
| @@ -73,6 +73,12 @@ namespace Discord.Commands | |||
| return this; | |||
| } | |||
| public ModuleBuilder AddPrecondition(PreconditionAttribute precondition) | |||
| { | |||
| preconditions.Add(precondition); | |||
| return this; | |||
| } | |||
| public CommandBuilder AddCommand() => AddCommand(null); | |||
| public CommandBuilder AddCommand(string name) | |||
| { | |||
| @@ -91,12 +97,9 @@ namespace Discord.Commands | |||
| return builder; | |||
| } | |||
| public ModuleBuilder Done() | |||
| public ModuleInfo Build(CommandService service) | |||
| { | |||
| if (ParentModule == null) | |||
| throw new InvalidOperationException("Cannot finish a top-level module!"); | |||
| return ParentModule; | |||
| return new ModuleInfo(this, service); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,86 @@ | |||
| using System; | |||
| using System.Threading.Tasks; | |||
| using System.Collections.Generic; | |||
| namespace Discord.Commands.Builders | |||
| { | |||
| public class ParameterBuilder | |||
| { | |||
| public ParameterBuilder() | |||
| { } | |||
| public ParameterBuilder(string name) | |||
| { | |||
| Name = name; | |||
| } | |||
| public string Name { get; set; } | |||
| public string Summary { get; set; } | |||
| public object DefaultValue { get; set; } | |||
| public Type ParameterType { get; set; } | |||
| public TypeReader TypeReader { get; set; } | |||
| public bool Optional { get; set; } | |||
| public bool Remainder { get; set; } | |||
| public bool Multiple { get; set; } | |||
| public ParameterBuilder SetName(string name) | |||
| { | |||
| Name = name; | |||
| return this; | |||
| } | |||
| public ParameterBuilder SetSummary(string summary) | |||
| { | |||
| Summary = summary; | |||
| return this; | |||
| } | |||
| public ParameterBuilder SetDefault<T>(T defaultValue) | |||
| { | |||
| DefaultValue = defaultValue; | |||
| ParameterType = typeof(T); | |||
| if (ParameterType.IsArray) | |||
| ParameterType = ParameterType.GetElementType(); | |||
| return this; | |||
| } | |||
| public ParameterBuilder SetType(Type parameterType) | |||
| { | |||
| ParameterType = parameterType; | |||
| return this; | |||
| } | |||
| public ParameterBuilder SetTypeReader(TypeReader reader) | |||
| { | |||
| TypeReader = reader; | |||
| return this; | |||
| } | |||
| public ParameterBuilder SetOptional(bool isOptional) | |||
| { | |||
| Optional = isOptional; | |||
| return this; | |||
| } | |||
| public ParameterBuilder SetRemainder(bool isRemainder) | |||
| { | |||
| Remainder = isRemainder; | |||
| return this; | |||
| } | |||
| public ParameterBuilder SetMultiple(bool isMultiple) | |||
| { | |||
| Multiple = isMultiple; | |||
| return this; | |||
| } | |||
| internal ParameterInfo Build(CommandInfo info, CommandService service) | |||
| { | |||
| return new ParameterInfo(this, info, service); | |||
| } | |||
| } | |||
| } | |||
| @@ -1,284 +0,0 @@ | |||
| using System; | |||
| using System.Collections.Concurrent; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.Diagnostics; | |||
| using System.Linq; | |||
| using System.Reflection; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Commands | |||
| { | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public class CommandInfo | |||
| { | |||
| private static readonly MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); | |||
| private static readonly ConcurrentDictionary<Type, Func<IEnumerable<object>, object>> _arrayConverters = new ConcurrentDictionary<Type, Func<IEnumerable<object>, object>>(); | |||
| private readonly Func<CommandContext, object[], IDependencyMap, Task> _action; | |||
| public MethodInfo Source { get; } | |||
| public ModuleInfo Module { get; } | |||
| public string Name { get; } | |||
| public string Summary { get; } | |||
| public string Remarks { get; } | |||
| public string Text { get; } | |||
| public int Priority { get; } | |||
| public bool HasVarArgs { get; } | |||
| public RunMode RunMode { get; } | |||
| public IReadOnlyList<string> Aliases { get; } | |||
| public IReadOnlyList<CommandParameter> Parameters { get; } | |||
| public IReadOnlyList<PreconditionAttribute> Preconditions { get; } | |||
| internal CommandInfo(MethodInfo source, ModuleInfo module, CommandAttribute attribute, string groupPrefix) | |||
| { | |||
| try | |||
| { | |||
| Source = source; | |||
| Module = module; | |||
| Name = source.Name; | |||
| if (attribute.Text == null) | |||
| Text = groupPrefix; | |||
| RunMode = attribute.RunMode; | |||
| if (groupPrefix != "") | |||
| groupPrefix += " "; | |||
| if (attribute.Text != null) | |||
| Text = groupPrefix + attribute.Text; | |||
| var aliasesBuilder = ImmutableArray.CreateBuilder<string>(); | |||
| aliasesBuilder.Add(Text); | |||
| var aliasesAttr = source.GetCustomAttribute<AliasAttribute>(); | |||
| if (aliasesAttr != null) | |||
| aliasesBuilder.AddRange(aliasesAttr.Aliases.Select(x => groupPrefix + x)); | |||
| Aliases = aliasesBuilder.ToImmutable(); | |||
| var nameAttr = source.GetCustomAttribute<NameAttribute>(); | |||
| if (nameAttr != null) | |||
| Name = nameAttr.Text; | |||
| var summary = source.GetCustomAttribute<SummaryAttribute>(); | |||
| if (summary != null) | |||
| Summary = summary.Text; | |||
| var remarksAttr = source.GetCustomAttribute<RemarksAttribute>(); | |||
| if (remarksAttr != null) | |||
| Remarks = remarksAttr.Text; | |||
| var priorityAttr = source.GetCustomAttribute<PriorityAttribute>(); | |||
| Priority = priorityAttr?.Priority ?? 0; | |||
| Parameters = BuildParameters(source); | |||
| HasVarArgs = Parameters.Count > 0 ? Parameters[Parameters.Count - 1].IsMultiple : false; | |||
| Preconditions = BuildPreconditions(source); | |||
| _action = BuildAction(source); | |||
| } | |||
| catch (Exception ex) | |||
| { | |||
| throw new Exception($"Failed to build command {source.DeclaringType.FullName}.{source.Name}", ex); | |||
| } | |||
| } | |||
| public async Task<PreconditionResult> CheckPreconditions(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<ParseResult> Parse(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); | |||
| } | |||
| public Task<ExecuteResult> 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 Execute(context, argList, paramList, map); | |||
| } | |||
| public async Task<ExecuteResult> Execute(CommandContext context, IEnumerable<object> argList, IEnumerable<object> 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 IReadOnlyList<PreconditionAttribute> BuildPreconditions(MethodInfo methodInfo) | |||
| { | |||
| return methodInfo.GetCustomAttributes<PreconditionAttribute>().ToImmutableArray(); | |||
| } | |||
| private IReadOnlyList<CommandParameter> BuildParameters(MethodInfo methodInfo) | |||
| { | |||
| var parameters = methodInfo.GetParameters(); | |||
| var paramBuilder = ImmutableArray.CreateBuilder<CommandParameter>(parameters.Length); | |||
| for (int i = 0; i < parameters.Length; i++) | |||
| { | |||
| var parameter = parameters[i]; | |||
| var type = parameter.ParameterType; | |||
| //Detect 'params' | |||
| bool isMultiple = parameter.GetCustomAttribute<ParamArrayAttribute>() != null; | |||
| if (isMultiple) | |||
| type = type.GetElementType(); | |||
| var reader = Module.Service.GetTypeReader(type); | |||
| var typeInfo = type.GetTypeInfo(); | |||
| //Detect enums | |||
| if (reader == null && typeInfo.IsEnum) | |||
| { | |||
| reader = EnumTypeReader.GetReader(type); | |||
| Module.Service.AddTypeReader(type, reader); | |||
| } | |||
| if (reader == null) | |||
| throw new InvalidOperationException($"{type.FullName} is not supported as a command parameter, are you missing a TypeReader?"); | |||
| bool isRemainder = parameter.GetCustomAttribute<RemainderAttribute>() != null; | |||
| if (isRemainder && i != parameters.Length - 1) | |||
| throw new InvalidOperationException("Remainder parameters must be the last parameter in a command."); | |||
| string name = parameter.Name; | |||
| string summary = parameter.GetCustomAttribute<SummaryAttribute>()?.Text; | |||
| bool isOptional = parameter.IsOptional; | |||
| object defaultValue = parameter.HasDefaultValue ? parameter.DefaultValue : null; | |||
| paramBuilder.Add(new CommandParameter(parameters[i], name, summary, type, reader, isOptional, isRemainder, isMultiple, defaultValue)); | |||
| } | |||
| return paramBuilder.ToImmutable(); | |||
| } | |||
| private Func<CommandContext, object[], IDependencyMap, Task> BuildAction(MethodInfo methodInfo) | |||
| { | |||
| if (methodInfo.ReturnType != typeof(Task)) | |||
| throw new InvalidOperationException("Commands must return a non-generic Task."); | |||
| return (context, args, map) => | |||
| { | |||
| var instance = Module.CreateInstance(map); | |||
| instance.Context = context; | |||
| try | |||
| { | |||
| return methodInfo.Invoke(instance, args) as Task ?? Task.CompletedTask; | |||
| } | |||
| finally | |||
| { | |||
| (instance as IDisposable)?.Dispose(); | |||
| } | |||
| }; | |||
| } | |||
| private object[] GenerateArgs(IEnumerable<object> argList, IEnumerable<object> 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].ElementType, t => | |||
| { | |||
| var method = _convertParamsMethod.MakeGenericMethod(t); | |||
| return (Func<IEnumerable<object>, object>)method.CreateDelegate(typeof(Func<IEnumerable<object>, object>)); | |||
| }); | |||
| array[i] = func(paramsList); | |||
| } | |||
| return array; | |||
| } | |||
| private static T[] ConvertParamsList<T>(IEnumerable<object> paramsList) | |||
| => paramsList.Cast<T>().ToArray(); | |||
| public override string ToString() => Name; | |||
| private string DebuggerDisplay => $"{Module.Name}.{Name} ({Text})"; | |||
| } | |||
| } | |||
| @@ -15,7 +15,7 @@ namespace Discord.Commands | |||
| public static async Task<ParseResult> ParseArgs(CommandInfo command, CommandContext context, string input, int startPos) | |||
| { | |||
| CommandParameter curParam = null; | |||
| ParameterInfo curParam = null; | |||
| StringBuilder argBuilder = new StringBuilder(input.Length); | |||
| int endPos = input.Length; | |||
| var curPart = ParserPart.None; | |||
| @@ -0,0 +1,22 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| namespace Discord.Commands | |||
| { | |||
| public static class IEnumerableExtensions | |||
| { | |||
| public static IEnumerable<TResult> Permutate<TFirst, TSecond, TResult>( | |||
| this IEnumerable<TFirst> set, | |||
| IEnumerable<TSecond> others, | |||
| Func<TFirst, TSecond, TResult> func) | |||
| { | |||
| foreach (TFirst elem in set) | |||
| { | |||
| foreach (TSecond elem2 in others) | |||
| { | |||
| yield return func(elem, elem2); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,173 @@ | |||
| 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 Discord.Commands.Builders; | |||
| namespace Discord.Commands | |||
| { | |||
| public class CommandInfo | |||
| { | |||
| private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); | |||
| private static readonly ConcurrentDictionary<Type, Func<IEnumerable<object>, object>> _arrayConverters = new ConcurrentDictionary<Type, Func<IEnumerable<object>, object>>(); | |||
| private readonly Func<CommandContext, object[], Task> _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<string> Aliases { get; } | |||
| public IReadOnlyList<ParameterInfo> Parameters { get; } | |||
| public IReadOnlyList<PreconditionAttribute> Preconditions { get; } | |||
| internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService service) | |||
| { | |||
| Module = module; | |||
| Name = builder.Name; | |||
| Summary = builder.Summary; | |||
| Remarks = builder.Remarks; | |||
| Aliases = module.Aliases.Permutate(builder.Aliases, (first, second) => first + " " + second).ToImmutableArray(); | |||
| Preconditions = builder.Preconditions.ToImmutableArray(); | |||
| Parameters = builder.Parameters.Select(x => x.Build(this, service)).ToImmutableArray(); | |||
| _action = builder.Callback; | |||
| } | |||
| public async Task<PreconditionResult> CheckPreconditions(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<ParseResult> Parse(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); | |||
| } | |||
| public Task<ExecuteResult> Execute(CommandContext context, ParseResult parseResult) | |||
| { | |||
| 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 Execute(context, argList, paramList); | |||
| } | |||
| public async Task<ExecuteResult> Execute(CommandContext context, IEnumerable<object> argList, IEnumerable<object> paramList) | |||
| { | |||
| try | |||
| { | |||
| var args = GenerateArgs(argList, paramList); | |||
| switch (RunMode) | |||
| { | |||
| case RunMode.Sync: //Always sync | |||
| await _action(context, args).ConfigureAwait(false); | |||
| break; | |||
| case RunMode.Mixed: //Sync until first await statement | |||
| var t1 = _action(context, args); | |||
| break; | |||
| case RunMode.Async: //Always async | |||
| var t2 = Task.Run(() => _action(context, args)); | |||
| break; | |||
| } | |||
| return ExecuteResult.FromSuccess(); | |||
| } | |||
| catch (Exception ex) | |||
| { | |||
| return ExecuteResult.FromError(ex); | |||
| } | |||
| } | |||
| private object[] GenerateArgs(IEnumerable<object> argList, IEnumerable<object> 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].ParameterType, t => | |||
| { | |||
| var method = _convertParamsMethod.MakeGenericMethod(t); | |||
| return (Func<IEnumerable<object>, object>)method.CreateDelegate(typeof(Func<IEnumerable<object>, object>)); | |||
| }); | |||
| array[i] = func(paramsList); | |||
| } | |||
| return array; | |||
| } | |||
| private static T[] ConvertParamsList<T>(IEnumerable<object> paramsList) | |||
| => paramsList.Cast<T>().ToArray(); | |||
| } | |||
| } | |||
| @@ -0,0 +1,74 @@ | |||
| using System; | |||
| using System.Linq; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using Discord.Commands.Builders; | |||
| namespace Discord.Commands | |||
| { | |||
| public class ModuleInfo | |||
| { | |||
| public CommandService Service { get; } | |||
| public string Name { get; } | |||
| public string Summary { get; } | |||
| public string Remarks { get; } | |||
| public IReadOnlyList<string> Aliases { get; } | |||
| public IEnumerable<CommandInfo> Commands { get; } | |||
| public IReadOnlyList<PreconditionAttribute> Preconditions { get; } | |||
| internal ModuleInfo(ModuleBuilder builder, CommandService service) | |||
| { | |||
| Service = service; | |||
| Name = builder.Name; | |||
| Summary = builder.Summary; | |||
| Remarks = builder.Remarks; | |||
| Aliases = BuildAliases(builder).ToImmutableArray(); | |||
| Commands = builder.Commands.Select(x => x.Build(this, service)); | |||
| Preconditions = BuildPreconditions(builder).ToImmutableArray(); | |||
| } | |||
| private static List<string> BuildAliases(ModuleBuilder builder) | |||
| { | |||
| IEnumerable<string> result = null; | |||
| Stack<ModuleBuilder> builderStack = new Stack<ModuleBuilder>(); | |||
| ModuleBuilder parent = builder; | |||
| while (parent.ParentModule != null) | |||
| { | |||
| builderStack.Push(parent); | |||
| parent = parent.ParentModule; | |||
| } | |||
| while (builderStack.Count() > 0) | |||
| { | |||
| ModuleBuilder level = builderStack.Pop(); // get the topmost builder | |||
| if (result == null) | |||
| result = level.Aliases.ToList(); // create a shallow copy so we don't overwrite the builder unexpectedly | |||
| else | |||
| result = result.Permutate(level.Aliases, (first, second) => first + " " + second); | |||
| } | |||
| return result.ToList(); | |||
| } | |||
| private static List<PreconditionAttribute> BuildPreconditions(ModuleBuilder builder) | |||
| { | |||
| var result = new List<PreconditionAttribute>(); | |||
| ModuleBuilder parent = builder; | |||
| while (parent.ParentModule != null) | |||
| { | |||
| result.AddRange(parent.Preconditions); | |||
| parent = parent.ParentModule; | |||
| } | |||
| return result; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,37 +1,40 @@ | |||
| using System; | |||
| using System.Diagnostics; | |||
| using System.Reflection; | |||
| using System; | |||
| using System.Linq; | |||
| using System.Threading.Tasks; | |||
| using Discord.Commands.Builders; | |||
| namespace Discord.Commands | |||
| { | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public class CommandParameter | |||
| public class ParameterInfo | |||
| { | |||
| private readonly TypeReader _reader; | |||
| public ParameterInfo Source { get; } | |||
| internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandService service) | |||
| { | |||
| Command = command; | |||
| Name = builder.Name; | |||
| Summary = builder.Summary; | |||
| IsOptional = builder.Optional; | |||
| IsRemainder = builder.Remainder; | |||
| IsMultiple = builder.Multiple; | |||
| ParameterType = builder.ParameterType; | |||
| DefaultValue = builder.DefaultValue; | |||
| _reader = builder.TypeReader; | |||
| } | |||
| public CommandInfo Command { get; } | |||
| public string Name { get; } | |||
| public string Summary { get; } | |||
| public bool IsOptional { get; } | |||
| public bool IsRemainder { get; } | |||
| public bool IsMultiple { get; } | |||
| public Type ElementType { get; } | |||
| public Type ParameterType { get; } | |||
| public object DefaultValue { get; } | |||
| public CommandParameter(ParameterInfo source, string name, string summary, Type type, TypeReader reader, bool isOptional, bool isRemainder, bool isMultiple, object defaultValue) | |||
| { | |||
| Source = source; | |||
| Name = name; | |||
| Summary = summary; | |||
| ElementType = type; | |||
| _reader = reader; | |||
| IsOptional = isOptional; | |||
| IsRemainder = isRemainder; | |||
| IsMultiple = isMultiple; | |||
| DefaultValue = defaultValue; | |||
| } | |||
| public async Task<TypeReaderResult> Parse(CommandContext context, string input) | |||
| { | |||
| return await _reader.Read(context, input).ConfigureAwait(false); | |||
| @@ -40,4 +43,4 @@ namespace Discord.Commands | |||
| public override string ToString() => Name; | |||
| private string DebuggerDisplay => $"{Name}{(IsOptional ? " (Optional)" : "")}{(IsRemainder ? " (Remainder)" : "")}"; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,85 +0,0 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.Diagnostics; | |||
| using System.Reflection; | |||
| namespace Discord.Commands | |||
| { | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public class ModuleInfo | |||
| { | |||
| internal readonly Func<IDependencyMap, ModuleBase> _builder; | |||
| public TypeInfo Source { get; } | |||
| public CommandService Service { get; } | |||
| public string Name { get; } | |||
| public string Prefix { get; } | |||
| public string Summary { get; } | |||
| public string Remarks { get; } | |||
| public IEnumerable<CommandInfo> Commands { get; } | |||
| public IReadOnlyList<PreconditionAttribute> Preconditions { get; } | |||
| internal ModuleInfo(TypeInfo source, CommandService service) | |||
| { | |||
| Source = source; | |||
| Service = service; | |||
| Name = source.Name; | |||
| _builder = ReflectionUtils.CreateBuilder<ModuleBase>(source, Service); | |||
| var groupAttr = source.GetCustomAttribute<GroupAttribute>(); | |||
| if (groupAttr != null) | |||
| Prefix = groupAttr.Prefix; | |||
| else | |||
| Prefix = ""; | |||
| var nameAttr = source.GetCustomAttribute<NameAttribute>(); | |||
| if (nameAttr != null) | |||
| Name = nameAttr.Text; | |||
| var summaryAttr = source.GetCustomAttribute<SummaryAttribute>(); | |||
| if (summaryAttr != null) | |||
| Summary = summaryAttr.Text; | |||
| var remarksAttr = source.GetCustomAttribute<RemarksAttribute>(); | |||
| if (remarksAttr != null) | |||
| Remarks = remarksAttr.Text; | |||
| List<CommandInfo> commands = new List<CommandInfo>(); | |||
| SearchClass(source, commands, Prefix); | |||
| Commands = commands; | |||
| Preconditions = Source.GetCustomAttributes<PreconditionAttribute>().ToImmutableArray(); | |||
| } | |||
| private void SearchClass(TypeInfo parentType, List<CommandInfo> commands, string groupPrefix) | |||
| { | |||
| foreach (var method in parentType.DeclaredMethods) | |||
| { | |||
| var cmdAttr = method.GetCustomAttribute<CommandAttribute>(); | |||
| if (cmdAttr != null) | |||
| commands.Add(new CommandInfo(method, this, cmdAttr, groupPrefix)); | |||
| } | |||
| foreach (var type in parentType.DeclaredNestedTypes) | |||
| { | |||
| var groupAttrib = type.GetCustomAttribute<GroupAttribute>(); | |||
| if (groupAttrib != null) | |||
| { | |||
| string nextGroupPrefix; | |||
| if (groupPrefix != "") | |||
| nextGroupPrefix = groupPrefix + " " + (groupAttrib.Prefix ?? type.Name.ToLowerInvariant()); | |||
| else | |||
| nextGroupPrefix = groupAttrib.Prefix ?? type.Name.ToLowerInvariant(); | |||
| SearchClass(type, commands, nextGroupPrefix); | |||
| } | |||
| } | |||
| } | |||
| internal ModuleBase CreateInstance(IDependencyMap map) | |||
| => _builder(map); | |||
| public override string ToString() => Name; | |||
| private string DebuggerDisplay => Name; | |||
| } | |||
| } | |||
| @@ -18,7 +18,7 @@ namespace Discord.Commands | |||
| throw new InvalidOperationException($"Multiple constructors found for \"{typeInfo.FullName}\""); | |||
| var constructor = constructors[0]; | |||
| ParameterInfo[] parameters = constructor.GetParameters(); | |||
| System.Reflection.ParameterInfo[] parameters = constructor.GetParameters(); | |||
| return (map) => | |||
| { | |||