From f95154af234ef01c144eca14d3d56fcf3100e713 Mon Sep 17 00:00:00 2001 From: FiniteReality Date: Mon, 14 Nov 2016 19:26:32 +0000 Subject: [PATCH 1/7] Start work on command builders Right now commands require a module before they can be created; I'm looking to remove this in the future. --- .../Builders/CommandBuilder.cs | 69 ++++++++++++ .../Builders/ModuleBuilder.cs | 102 ++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 src/Discord.Net.Commands/Builders/CommandBuilder.cs create mode 100644 src/Discord.Net.Commands/Builders/ModuleBuilder.cs diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs new file mode 100644 index 000000000..e14743499 --- /dev/null +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -0,0 +1,69 @@ +using System; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace Discord.Commands +{ + public class CommandBuilder + { + private List aliases; + + internal CommandBuilder(ModuleBuilder module, string prefix) + { + aliases = new List(); + + if (prefix != null) + { + aliases.Add(prefix); + Name = prefix; + } + + Module = module; + } + + + public string Name { get; set; } + public string Summary { get; set; } + public string Remarks { get; set; } + public Func Callback { get; set; } + public ModuleBuilder Module { get; } + + public List Aliases => aliases; + + + public CommandBuilder SetName(string name) + { + Name = name; + return this; + } + + public CommandBuilder SetSummary(string summary) + { + Summary = summary; + return this; + } + + public CommandBuilder SetRemarks(string remarks) + { + Remarks = remarks; + return this; + } + + public CommandBuilder SetCallback(Func callback) + { + Callback = callback; + return this; + } + + public CommandBuilder AddAlias(string alias) + { + aliases.Add(alias); + return this; + } + + public ModuleBuilder Done() + { + return Module; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs new file mode 100644 index 000000000..ca4f84633 --- /dev/null +++ b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Discord.Commands +{ + public class ModuleBuilder + { + private List commands; + private List submodules; + private List aliases; + + + public ModuleBuilder() + : this(null, null) + { } + + public ModuleBuilder(string prefix) + : this(null, prefix) + { } + + internal ModuleBuilder(ModuleBuilder parent) + : this(parent, null) + { } + + internal ModuleBuilder(ModuleBuilder parent, string prefix) + { + commands = new List(); + submodules = new List(); + aliases = new List(); + + if (prefix != null) + { + aliases.Add(prefix); + Name = prefix; + } + + ParentModule = parent; + } + + + public string Name { get; set; } + public string Summary { get; set; } + public string Remarks { get; set; } + public ModuleBuilder ParentModule { get; } + + public List Commands => commands; + public List Modules => submodules; + public List Aliases => aliases; + + + public ModuleBuilder SetName(string name) + { + Name = name; + return this; + } + + public ModuleBuilder SetSummary(string summary) + { + Summary = summary; + return this; + } + + public ModuleBuilder SetRemarks(string remarks) + { + Remarks = remarks; + return this; + } + + public ModuleBuilder AddAlias(string alias) + { + aliases.Add(alias); + return this; + } + + public CommandBuilder AddCommand() => AddCommand(null); + public CommandBuilder AddCommand(string name) + { + var builder = new CommandBuilder(this, name); + commands.Add(builder); + + return builder; + } + + public ModuleBuilder AddSubmodule() => AddSubmodule(null); + public ModuleBuilder AddSubmodule(string prefix) + { + var builder = new ModuleBuilder(this, prefix); + submodules.Add(builder); + + return builder; + } + + public ModuleBuilder Done() + { + if (ParentModule == null) + throw new InvalidOperationException("Cannot finish a top-level module!"); + + return ParentModule; + } + } +} From af433c82ccf9d5de28008af1d2f7399124a2ebc1 Mon Sep 17 00:00:00 2001 From: FiniteReality Date: Tue, 15 Nov 2016 20:53:18 +0000 Subject: [PATCH 2/7] Complete builders, start work on using them --- .../Builders/CommandBuilder.cs | 30 +- .../Builders/ModuleBuilder.cs | 21 +- .../Builders/ParameterBuilder.cs | 86 ++++++ src/Discord.Net.Commands/CommandInfo.cs | 284 ------------------ src/Discord.Net.Commands/CommandParser.cs | 2 +- .../Extensions/IEnumerableExtensions.cs | 22 ++ src/Discord.Net.Commands/Info/CommandInfo.cs | 173 +++++++++++ src/Discord.Net.Commands/Info/ModuleInfo.cs | 74 +++++ .../ParameterInfo.cs} | 45 +-- src/Discord.Net.Commands/ModuleInfo.cs | 85 ------ src/Discord.Net.Commands/ReflectionUtils.cs | 2 +- 11 files changed, 416 insertions(+), 408 deletions(-) create mode 100644 src/Discord.Net.Commands/Builders/ParameterBuilder.cs delete mode 100644 src/Discord.Net.Commands/CommandInfo.cs create mode 100644 src/Discord.Net.Commands/Extensions/IEnumerableExtensions.cs create mode 100644 src/Discord.Net.Commands/Info/CommandInfo.cs create mode 100644 src/Discord.Net.Commands/Info/ModuleInfo.cs rename src/Discord.Net.Commands/{CommandParameter.cs => Info/ParameterInfo.cs} (50%) delete mode 100644 src/Discord.Net.Commands/ModuleInfo.cs diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs index e14743499..077787427 100644 --- a/src/Discord.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -2,14 +2,18 @@ using System.Threading.Tasks; using System.Collections.Generic; -namespace Discord.Commands +namespace Discord.Commands.Builders { public class CommandBuilder { + private List preconditions; + private List parameters; private List aliases; internal CommandBuilder(ModuleBuilder module, string prefix) { + preconditions = new List(); + parameters = new List(); aliases = new List(); 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 Callback { get; set; } + public Func Callback { get; set; } public ModuleBuilder Module { get; } + public List Preconditions => preconditions; + public List Parameters => parameters; public List Aliases => aliases; - public CommandBuilder SetName(string name) { Name = name; @@ -49,21 +53,33 @@ namespace Discord.Commands return this; } - public CommandBuilder SetCallback(Func callback) + public CommandBuilder SetCallback(Func 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); } } } \ No newline at end of file diff --git a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs index ca4f84633..0375383a8 100644 --- a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -2,15 +2,15 @@ using System.Collections.Generic; using System.Collections.Immutable; -namespace Discord.Commands +namespace Discord.Commands.Builders { public class ModuleBuilder { private List commands; private List submodules; + private List preconditions; private List aliases; - public ModuleBuilder() : this(null, null) { } @@ -27,6 +27,7 @@ namespace Discord.Commands { commands = new List(); submodules = new List(); + preconditions = new List(); aliases = new List(); 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 Commands => commands; public List Modules => submodules; + public List Preconditions => preconditions; public List 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); } } } diff --git a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs new file mode 100644 index 000000000..e90b2a9bf --- /dev/null +++ b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs @@ -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 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); + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Commands/CommandInfo.cs b/src/Discord.Net.Commands/CommandInfo.cs deleted file mode 100644 index 47aae1ae2..000000000 --- a/src/Discord.Net.Commands/CommandInfo.cs +++ /dev/null @@ -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, object>> _arrayConverters = new ConcurrentDictionary, object>>(); - - private readonly Func _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 Aliases { get; } - public IReadOnlyList Parameters { get; } - public IReadOnlyList 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(); - - aliasesBuilder.Add(Text); - - var aliasesAttr = source.GetCustomAttribute(); - if (aliasesAttr != null) - aliasesBuilder.AddRange(aliasesAttr.Aliases.Select(x => groupPrefix + x)); - - Aliases = aliasesBuilder.ToImmutable(); - - var nameAttr = source.GetCustomAttribute(); - if (nameAttr != null) - Name = nameAttr.Text; - - var summary = source.GetCustomAttribute(); - if (summary != null) - Summary = summary.Text; - - var remarksAttr = source.GetCustomAttribute(); - if (remarksAttr != null) - Remarks = remarksAttr.Text; - - var priorityAttr = source.GetCustomAttribute(); - 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 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 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 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 Execute(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 IReadOnlyList BuildPreconditions(MethodInfo methodInfo) - { - return methodInfo.GetCustomAttributes().ToImmutableArray(); - } - - private IReadOnlyList BuildParameters(MethodInfo methodInfo) - { - var parameters = methodInfo.GetParameters(); - - var paramBuilder = ImmutableArray.CreateBuilder(parameters.Length); - for (int i = 0; i < parameters.Length; i++) - { - var parameter = parameters[i]; - var type = parameter.ParameterType; - - //Detect 'params' - bool isMultiple = parameter.GetCustomAttribute() != 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() != 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()?.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 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 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].ElementType, 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(); - - public override string ToString() => Name; - private string DebuggerDisplay => $"{Module.Name}.{Name} ({Text})"; - } -} diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index 1808b705d..7bdbed955 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -15,7 +15,7 @@ namespace Discord.Commands public static async Task 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; diff --git a/src/Discord.Net.Commands/Extensions/IEnumerableExtensions.cs b/src/Discord.Net.Commands/Extensions/IEnumerableExtensions.cs new file mode 100644 index 000000000..b922dd903 --- /dev/null +++ b/src/Discord.Net.Commands/Extensions/IEnumerableExtensions.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace Discord.Commands +{ + public static class IEnumerableExtensions + { + public static IEnumerable Permutate( + this IEnumerable set, + IEnumerable others, + Func func) + { + foreach (TFirst elem in set) + { + foreach (TSecond elem2 in others) + { + yield return func(elem, elem2); + } + } + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs new file mode 100644 index 000000000..fc1e0421b --- /dev/null +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -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, 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; } + + 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 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 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 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 Execute(CommandContext context, IEnumerable argList, IEnumerable 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 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].ParameterType, 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/ModuleInfo.cs b/src/Discord.Net.Commands/Info/ModuleInfo.cs new file mode 100644 index 000000000..33a6574c4 --- /dev/null +++ b/src/Discord.Net.Commands/Info/ModuleInfo.cs @@ -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 Aliases { get; } + public IEnumerable Commands { get; } + public IReadOnlyList 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 BuildAliases(ModuleBuilder builder) + { + IEnumerable result = null; + + Stack builderStack = new Stack(); + + 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 BuildPreconditions(ModuleBuilder builder) + { + var result = new List(); + + + ModuleBuilder parent = builder; + while (parent.ParentModule != null) + { + result.AddRange(parent.Preconditions); + parent = parent.ParentModule; + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Commands/CommandParameter.cs b/src/Discord.Net.Commands/Info/ParameterInfo.cs similarity index 50% rename from src/Discord.Net.Commands/CommandParameter.cs rename to src/Discord.Net.Commands/Info/ParameterInfo.cs index 1edf42bf1..812fec572 100644 --- a/src/Discord.Net.Commands/CommandParameter.cs +++ b/src/Discord.Net.Commands/Info/ParameterInfo.cs @@ -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 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)" : "")}"; } -} +} \ No newline at end of file diff --git a/src/Discord.Net.Commands/ModuleInfo.cs b/src/Discord.Net.Commands/ModuleInfo.cs deleted file mode 100644 index b7471edb5..000000000 --- a/src/Discord.Net.Commands/ModuleInfo.cs +++ /dev/null @@ -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 _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 Commands { get; } - public IReadOnlyList Preconditions { get; } - - internal ModuleInfo(TypeInfo source, CommandService service) - { - Source = source; - Service = service; - Name = source.Name; - _builder = ReflectionUtils.CreateBuilder(source, Service); - - var groupAttr = source.GetCustomAttribute(); - if (groupAttr != null) - Prefix = groupAttr.Prefix; - else - Prefix = ""; - - var nameAttr = source.GetCustomAttribute(); - if (nameAttr != null) - Name = nameAttr.Text; - - var summaryAttr = source.GetCustomAttribute(); - if (summaryAttr != null) - Summary = summaryAttr.Text; - - var remarksAttr = source.GetCustomAttribute(); - if (remarksAttr != null) - Remarks = remarksAttr.Text; - - List commands = new List(); - SearchClass(source, commands, Prefix); - Commands = commands; - - Preconditions = Source.GetCustomAttributes().ToImmutableArray(); - } - private void SearchClass(TypeInfo parentType, List commands, string groupPrefix) - { - foreach (var method in parentType.DeclaredMethods) - { - var cmdAttr = method.GetCustomAttribute(); - if (cmdAttr != null) - commands.Add(new CommandInfo(method, this, cmdAttr, groupPrefix)); - } - foreach (var type in parentType.DeclaredNestedTypes) - { - var groupAttrib = type.GetCustomAttribute(); - 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; - } -} diff --git a/src/Discord.Net.Commands/ReflectionUtils.cs b/src/Discord.Net.Commands/ReflectionUtils.cs index 052e5fe98..27ea601bf 100644 --- a/src/Discord.Net.Commands/ReflectionUtils.cs +++ b/src/Discord.Net.Commands/ReflectionUtils.cs @@ -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) => { From d25fbf78c061849130a5232ee804844e5df350b1 Mon Sep 17 00:00:00 2001 From: FiniteReality Date: Tue, 15 Nov 2016 21:46:16 +0000 Subject: [PATCH 3/7] Use lambda-style subbuilders --- .../Builders/CommandBuilder.cs | 18 ++++----- .../Builders/ModuleBuilder.cs | 38 ++++++------------- 2 files changed, 18 insertions(+), 38 deletions(-) diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs index 077787427..2ea01e3bb 100644 --- a/src/Discord.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -10,18 +10,12 @@ namespace Discord.Commands.Builders private List parameters; private List aliases; - internal CommandBuilder(ModuleBuilder module, string prefix) + internal CommandBuilder(ModuleBuilder module) { preconditions = new List(); parameters = new List(); aliases = new List(); - if (prefix != null) - { - aliases.Add(prefix); - Name = prefix; - } - Module = module; } @@ -65,15 +59,17 @@ namespace Discord.Commands.Builders return this; } - public CommandBuilder AddParameter(ParameterBuilder parameter) + public CommandBuilder AddParameter(Action createFunc) { - parameters.Add(parameter); + var param = new ParameterBuilder(); + createFunc(param); + parameters.Add(param); return this; } - public CommandBuilder AddAlias(string alias) + public CommandBuilder AddAliases(params string[] newAliases) { - aliases.Add(alias); + aliases.AddRange(newAliases); return this; } diff --git a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs index 0375383a8..dd30b0130 100644 --- a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -12,29 +12,15 @@ namespace Discord.Commands.Builders private List aliases; public ModuleBuilder() - : this(null, null) - { } - - public ModuleBuilder(string prefix) - : this(null, prefix) + : this(null) { } internal ModuleBuilder(ModuleBuilder parent) - : this(parent, null) - { } - - internal ModuleBuilder(ModuleBuilder parent, string prefix) { commands = new List(); submodules = new List(); preconditions = new List(); aliases = new List(); - - if (prefix != null) - { - aliases.Add(prefix); - Name = prefix; - } ParentModule = parent; } @@ -67,9 +53,9 @@ namespace Discord.Commands.Builders return this; } - public ModuleBuilder AddAlias(string alias) + public ModuleBuilder AddAliases(params string[] newAliases) { - aliases.Add(alias); + aliases.AddRange(newAliases); return this; } @@ -79,22 +65,20 @@ namespace Discord.Commands.Builders return this; } - public CommandBuilder AddCommand() => AddCommand(null); - public CommandBuilder AddCommand(string name) + public ModuleBuilder AddCommand(Action createFunc) { - var builder = new CommandBuilder(this, name); + var builder = new CommandBuilder(this); + createFunc(builder); commands.Add(builder); - - return builder; + return this; } - public ModuleBuilder AddSubmodule() => AddSubmodule(null); - public ModuleBuilder AddSubmodule(string prefix) + public ModuleBuilder AddSubmodule(Action createFunc) { - var builder = new ModuleBuilder(this, prefix); + var builder = new ModuleBuilder(this); + createFunc(builder); submodules.Add(builder); - - return builder; + return this; } public ModuleInfo Build(CommandService service) From ab95ced41da08f22c77c426ae730403602027228 Mon Sep 17 00:00:00 2001 From: FiniteReality Date: Tue, 15 Nov 2016 21:57:27 +0000 Subject: [PATCH 4/7] Use default TypeReader if not overriden --- src/Discord.Net.Commands/Builders/ParameterBuilder.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs index e90b2a9bf..8a0a17ab0 100644 --- a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs @@ -80,6 +80,9 @@ namespace Discord.Commands.Builders internal ParameterInfo Build(CommandInfo info, CommandService service) { + if (TypeReader == null) + TypeReader = service.GetTypeReader(ParameterType); + return new ParameterInfo(this, info, service); } } From 6d46347ebcf4cfdb1ecc74cdb43d4d83b2219593 Mon Sep 17 00:00:00 2001 From: FiniteReality Date: Wed, 16 Nov 2016 20:40:28 +0000 Subject: [PATCH 5/7] Finish implementation of command builders --- .../Builders/CommandBuilder.cs | 4 +- .../Builders/ModuleBuilder.cs | 10 +- .../Builders/ParameterBuilder.cs | 1 + src/Discord.Net.Commands/CommandService.cs | 50 ++-- src/Discord.Net.Commands/Info/CommandInfo.cs | 17 +- src/Discord.Net.Commands/Info/ModuleInfo.cs | 13 +- .../Utilities/ModuleClassBuilder.cs | 213 ++++++++++++++++++ .../{ => Utilities}/ReflectionUtils.cs | 0 8 files changed, 255 insertions(+), 53 deletions(-) create mode 100644 src/Discord.Net.Commands/Utilities/ModuleClassBuilder.cs rename src/Discord.Net.Commands/{ => Utilities}/ReflectionUtils.cs (100%) diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs index 2ea01e3bb..cd699ae61 100644 --- a/src/Discord.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -22,7 +22,7 @@ namespace Discord.Commands.Builders public string Name { get; set; } public string Summary { get; set; } public string Remarks { get; set; } - public Func Callback { get; set; } + public Func Callback { get; set; } public ModuleBuilder Module { get; } public List Preconditions => preconditions; @@ -47,7 +47,7 @@ namespace Discord.Commands.Builders return this; } - public CommandBuilder SetCallback(Func callback) + public CommandBuilder SetCallback(Func callback) { Callback = callback; return this; diff --git a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs index dd30b0130..79f818f38 100644 --- a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -12,16 +12,16 @@ namespace Discord.Commands.Builders private List aliases; public ModuleBuilder() - : this(null) - { } - - internal ModuleBuilder(ModuleBuilder parent) { commands = new List(); submodules = new List(); preconditions = new List(); aliases = new List(); - + } + + internal ModuleBuilder(ModuleBuilder parent) + : this() + { ParentModule = parent; } diff --git a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs index 8a0a17ab0..7de26b72f 100644 --- a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs @@ -39,6 +39,7 @@ namespace Discord.Commands.Builders public ParameterBuilder SetDefault(T defaultValue) { + Optional = true; DefaultValue = defaultValue; ParameterType = typeof(T); diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index ef0dba7e7..08cf75ac8 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -11,10 +11,8 @@ namespace Discord.Commands { public class CommandService { - private static readonly TypeInfo _moduleTypeInfo = typeof(ModuleBase).GetTypeInfo(); - private readonly SemaphoreSlim _moduleLock; - private readonly ConcurrentDictionary _moduleDefs; + private readonly ConcurrentDictionary _moduleDefs; private readonly ConcurrentDictionary _typeReaders; private readonly CommandMap _map; @@ -71,16 +69,18 @@ namespace Discord.Commands try { var typeInfo = typeof(T).GetTypeInfo(); - if (!_moduleTypeInfo.IsAssignableFrom(typeInfo)) - throw new ArgumentException($"Modules must inherit ModuleBase."); - - if (typeInfo.IsAbstract) - throw new InvalidOperationException("Modules must not be abstract."); if (_moduleDefs.ContainsKey(typeof(T))) throw new ArgumentException($"This module has already been added."); - return AddModuleInternal(typeInfo); + var module = ModuleClassBuilder.Build(this, typeInfo).First(); + + _moduleDefs[typeof(T)] = module; + + foreach (var cmd in module.Commands) + _map.AddCommand(cmd); + + return module; } finally { @@ -93,43 +93,25 @@ namespace Discord.Commands await _moduleLock.WaitAsync().ConfigureAwait(false); try { - foreach (var type in assembly.ExportedTypes) - { - if (!_moduleDefs.ContainsKey(type)) - { - var typeInfo = type.GetTypeInfo(); - if (_moduleTypeInfo.IsAssignableFrom(typeInfo)) - { - var dontAutoLoad = typeInfo.GetCustomAttribute(); - if (dontAutoLoad == null && !typeInfo.IsAbstract) - moduleDefs.Add(AddModuleInternal(typeInfo)); - } - } - } - return moduleDefs.ToImmutable(); + var types = ModuleClassBuilder.Search(assembly); + return ModuleClassBuilder.Build(types, this).ToImmutableArray(); } finally { _moduleLock.Release(); } } - private ModuleInfo AddModuleInternal(TypeInfo typeInfo) - { - var moduleDef = new ModuleInfo(typeInfo, this); - _moduleDefs[typeInfo.AsType()] = moduleDef; - - foreach (var cmd in moduleDef.Commands) - _map.AddCommand(cmd); - - return moduleDef; - } public async Task RemoveModule(ModuleInfo module) { await _moduleLock.WaitAsync().ConfigureAwait(false); try { - return RemoveModuleInternal(module.Source.BaseType); + var type = _moduleDefs.FirstOrDefault(x => x.Value == module); + if (default(KeyValuePair).Key == type.Key) + throw new KeyNotFoundException($"Could not find the key for the module {module?.Name ?? module?.Aliases?.FirstOrDefault()}"); + + return RemoveModuleInternal(type.Key); } finally { diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index fc1e0421b..31caaf3a8 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -15,7 +15,7 @@ namespace Discord.Commands 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; + private readonly Func _action; public ModuleInfo Module { get; } public string Name { get; } @@ -89,7 +89,7 @@ namespace Discord.Commands return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false); } - public Task Execute(CommandContext context, ParseResult parseResult) + public Task Execute(CommandContext context, ParseResult parseResult, IDependencyMap map) { if (!parseResult.IsSuccess) return Task.FromResult(ExecuteResult.FromError(parseResult)); @@ -110,23 +110,26 @@ namespace Discord.Commands paramList[i] = parseResult.ParamValues[i].Values.First().Value; } - return Execute(context, argList, paramList); + return Execute(context, argList, paramList, map); } - public async Task Execute(CommandContext context, IEnumerable argList, IEnumerable paramList) + public async Task Execute(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).ConfigureAwait(false); + await _action(context, args, map).ConfigureAwait(false); break; case RunMode.Mixed: //Sync until first await statement - var t1 = _action(context, args); + var t1 = _action(context, args, map); break; case RunMode.Async: //Always async - var t2 = Task.Run(() => _action(context, args)); + var t2 = Task.Run(() => _action(context, args, map)); break; } return ExecuteResult.FromSuccess(); diff --git a/src/Discord.Net.Commands/Info/ModuleInfo.cs b/src/Discord.Net.Commands/Info/ModuleInfo.cs index 33a6574c4..6ec0d657b 100644 --- a/src/Discord.Net.Commands/Info/ModuleInfo.cs +++ b/src/Discord.Net.Commands/Info/ModuleInfo.cs @@ -31,14 +31,15 @@ namespace Discord.Commands Preconditions = BuildPreconditions(builder).ToImmutableArray(); } - private static List BuildAliases(ModuleBuilder builder) + private static IEnumerable BuildAliases(ModuleBuilder builder) { IEnumerable result = null; Stack builderStack = new Stack(); + builderStack.Push(builder); - ModuleBuilder parent = builder; - while (parent.ParentModule != null) + ModuleBuilder parent = builder.ParentModule; + while (parent != null) { builderStack.Push(parent); parent = parent.ParentModule; @@ -49,11 +50,13 @@ namespace Discord.Commands 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 + else if (result.Count() > level.Aliases.Count) result = result.Permutate(level.Aliases, (first, second) => first + " " + second); + else + result = level.Aliases.Permutate(result, (second, first) => first + " " + second); } - return result.ToList(); + return result; } private static List BuildPreconditions(ModuleBuilder builder) diff --git a/src/Discord.Net.Commands/Utilities/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Utilities/ModuleClassBuilder.cs new file mode 100644 index 000000000..30ee1e5ba --- /dev/null +++ b/src/Discord.Net.Commands/Utilities/ModuleClassBuilder.cs @@ -0,0 +1,213 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; + +using Discord.Commands.Builders; + +namespace Discord.Commands +{ + internal static class ModuleClassBuilder + { + private static readonly TypeInfo _moduleTypeInfo = typeof(ModuleBase).GetTypeInfo(); + + public static IEnumerable Search(Assembly assembly) + { + foreach (var type in assembly.ExportedTypes) + { + var typeInfo = type.GetTypeInfo(); + if (IsValidModuleDefinition(typeInfo) && + !typeInfo.IsDefined(typeof(DontAutoLoadAttribute))) + { + yield return typeInfo; + } + } + } + + public static IEnumerable Build(CommandService service, params TypeInfo[] validTypes) => Build(validTypes, service); + public static IEnumerable Build(IEnumerable validTypes, CommandService service) + { + if (!validTypes.Any()) + throw new InvalidOperationException("Could not find any valid modules from the given selection"); + + var topLevelGroups = validTypes.Where(x => x.DeclaringType == null); + var subGroups = validTypes.Intersect(topLevelGroups); + + var builtTypes = new List(); + + var result = new List(); + + foreach (var typeInfo in topLevelGroups) + { + // this shouldn't be the case; may be safe to remove? + if (builtTypes.Contains(typeInfo)) + continue; + + builtTypes.Add(typeInfo); + + var module = new ModuleBuilder(); + + BuildModule(module, typeInfo, service); + BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); + + result.Add(module.Build(service)); + } + + return result; + } + + private static void BuildSubTypes(ModuleBuilder builder, IEnumerable subTypes, List builtTypes, CommandService service) + { + foreach (var typeInfo in subTypes) + { + if (builtTypes.Contains(typeInfo)) + continue; + + builtTypes.Add(typeInfo); + + builder.AddSubmodule((module) => { + BuildModule(module, typeInfo, service); + BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); + }); + } + } + + private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, CommandService service) + { + var attributes = typeInfo.GetCustomAttributes(); + + foreach (var attribute in attributes) + { + // TODO: C#7 type switch + if (attribute is NameAttribute) + builder.Name = (attribute as NameAttribute).Text; + 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 GroupAttribute) + builder.AddAliases((attribute as GroupAttribute).Prefix); + else if (attribute is PreconditionAttribute) + builder.AddPrecondition(attribute as PreconditionAttribute); + } + + var validCommands = typeInfo.DeclaredMethods.Where(x => IsValidCommandDefinition(x)); + + foreach (var method in validCommands) + { + builder.AddCommand((command) => { + BuildCommand(command, typeInfo, method, service); + }); + } + } + + private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service) + { + var attributes = method.GetCustomAttributes(); + + foreach (var attribute in attributes) + { + // TODO: C#7 type switch + if (attribute is NameAttribute) + builder.Name = (attribute as NameAttribute).Text; + 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 GroupAttribute) + builder.AddAliases((attribute as GroupAttribute).Prefix); + else if (attribute is PreconditionAttribute) + builder.AddPrecondition(attribute as PreconditionAttribute); + } + + var parameters = method.GetParameters(); + int pos = 0, count = parameters.Length; + foreach (var paramInfo in parameters) + { + builder.AddParameter((parameter) => { + BuildParameter(parameter, paramInfo, pos++, count, service); + }); + } + + var createInstance = ReflectionUtils.CreateBuilder(typeInfo, service); + + builder.Callback = (ctx, args, map) => { + var instance = createInstance(map); + instance.Context = ctx; + try + { + return method.Invoke(instance, args) as Task ?? Task.CompletedTask; + } + finally{ + (instance as IDisposable)?.Dispose(); + } + }; + } + + private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service) + { + var attributes = paramInfo.GetCustomAttributes(); + var paramType = paramInfo.ParameterType; + + builder.Optional = paramInfo.IsOptional; + builder.DefaultValue = paramInfo.HasDefaultValue ? paramInfo.DefaultValue : null; + + foreach (var attribute in attributes) + { + // TODO: C#7 type switch + if (attribute is NameAttribute) + builder.Name = (attribute as NameAttribute).Text; + else if (attribute is SummaryAttribute) + builder.Summary = (attribute as SummaryAttribute).Text; + else if (attribute is ParamArrayAttribute) + { + builder.Multiple = true; + paramType = paramType.GetElementType(); + } + else if (attribute is RemainderAttribute) + { + if (position != count-1) + throw new InvalidOperationException("Remainder parameters must be the last parameter in a command."); + + builder.Remainder = true; + } + } + + var reader = service.GetTypeReader(paramType); + if (reader == null) + { + var paramTypeInfo = paramType.GetTypeInfo(); + if (paramTypeInfo.IsEnum) + { + reader = EnumTypeReader.GetReader(paramType); + service.AddTypeReader(paramType, reader); + } + else + { + throw new InvalidOperationException($"{paramType.FullName} is not supported as a command parameter, are you missing a TypeReader?"); + } + } + + builder.TypeReader = reader; + } + + private static bool IsValidModuleDefinition(TypeInfo typeInfo) + { + return _moduleTypeInfo.IsAssignableFrom(typeInfo) && + !typeInfo.IsAbstract; + } + + private static bool IsValidCommandDefinition(MethodInfo methodInfo) + { + return methodInfo.IsDefined(typeof(CommandAttribute)) && + methodInfo.ReturnType != typeof(Task) && + !methodInfo.IsStatic && + !methodInfo.IsGenericMethod; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Commands/ReflectionUtils.cs b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs similarity index 100% rename from src/Discord.Net.Commands/ReflectionUtils.cs rename to src/Discord.Net.Commands/Utilities/ReflectionUtils.cs From de645548a916f0c538e173c7531a3c250667ef47 Mon Sep 17 00:00:00 2001 From: FiniteReality Date: Fri, 18 Nov 2016 09:14:19 +0000 Subject: [PATCH 6/7] Complete command builders implementation In theory this should just work, more testing is needed though --- .../Builders/CommandBuilder.cs | 37 +++++++ .../Builders/ModuleBuilder.cs | 9 ++ .../Builders/ParameterBuilder.cs | 10 ++ src/Discord.Net.Commands/CommandService.cs | 100 +++++++++++++----- src/Discord.Net.Commands/Info/CommandInfo.cs | 4 + src/Discord.Net.Commands/Info/ModuleInfo.cs | 18 +++- .../Utilities/ModuleClassBuilder.cs | 42 +++++--- 7 files changed, 176 insertions(+), 44 deletions(-) diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs index cd699ae61..921e5df0a 100644 --- a/src/Discord.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using System.Collections.Generic; @@ -22,6 +23,8 @@ namespace Discord.Commands.Builders 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 Func Callback { get; set; } public ModuleBuilder Module { get; } @@ -47,6 +50,18 @@ namespace Discord.Commands.Builders return this; } + public CommandBuilder SetRunMode(RunMode runMode) + { + RunMode = runMode; + return this; + } + + public CommandBuilder SetPriority(int priority) + { + Priority = priority; + return this; + } + public CommandBuilder SetCallback(Func callback) { Callback = callback; @@ -75,6 +90,28 @@ namespace Discord.Commands.Builders internal CommandInfo Build(ModuleInfo info, CommandService service) { + if (aliases.Count == 0) + throw new InvalidOperationException("Commands require at least one alias to be registered"); + + if (Callback == null) + throw new InvalidOperationException("Commands require a callback to be built"); + + if (Name == null) + Name = aliases[0]; + + if (parameters.Count > 0) + { + var lastParam = parameters[parameters.Count - 1]; + + var firstMultipleParam = parameters.FirstOrDefault(x => x.Multiple); + 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.Remainder); + 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 79f818f38..148eedfcf 100644 --- a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -83,6 +83,15 @@ namespace Discord.Commands.Builders public ModuleInfo Build(CommandService service) { + if (aliases.Count == 0) + throw new InvalidOperationException("Modules require at least one alias to be registered"); + + if (commands.Count == 0 && submodules.Count == 0) + throw new InvalidOperationException("Tried to build empty module"); + + if (Name == null) + Name = aliases[0]; + return new ModuleInfo(this, service); } } diff --git a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs index 7de26b72f..a6c8a5c23 100644 --- a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs @@ -81,9 +81,19 @@ namespace Discord.Commands.Builders internal ParameterInfo Build(CommandInfo info, CommandService service) { + // TODO: should we throw when we don't have a name? + if (Name == null) + Name = "[unknown parameter]"; + + if (ParameterType == null) + throw new InvalidOperationException($"Could not build parameter {Name} from command {info.Name} - An invalid parameter type was given"); + if (TypeReader == null) TypeReader = service.GetTypeReader(ParameterType); + if (TypeReader == null) + throw new InvalidOperationException($"Could not build parameter {Name} from command {info.Name} - A valid TypeReader could not be found"); + return new ParameterInfo(this, info, service); } } diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 08cf75ac8..ee18157dc 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -7,22 +7,26 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Discord.Commands.Builders; + namespace Discord.Commands { public class CommandService { private readonly SemaphoreSlim _moduleLock; - private readonly ConcurrentDictionary _moduleDefs; + private readonly ConcurrentDictionary _typedModuleDefs; private readonly ConcurrentDictionary _typeReaders; + private readonly ConcurrentBag _moduleDefs; private readonly CommandMap _map; - public IEnumerable Modules => _moduleDefs.Select(x => x.Value); - public IEnumerable Commands => _moduleDefs.SelectMany(x => x.Value.Commands); + public IEnumerable Modules => _typedModuleDefs.Select(x => x.Value); + public IEnumerable Commands => _typedModuleDefs.SelectMany(x => x.Value.Commands); public CommandService() { _moduleLock = new SemaphoreSlim(1, 1); - _moduleDefs = new ConcurrentDictionary(); + _typedModuleDefs = new ConcurrentDictionary(); + _moduleDefs = new ConcurrentBag(); _map = new CommandMap(); _typeReaders = new ConcurrentDictionary { @@ -63,6 +67,22 @@ namespace Discord.Commands } //Modules + public async Task BuildModule(Action buildFunc) + { + await _moduleLock.WaitAsync().ConfigureAwait(false); + try + { + var builder = new ModuleBuilder(); + buildFunc(builder); + + var module = builder.Build(this); + return LoadModuleInternal(module); + } + finally + { + _moduleLock.Release(); + } + } public async Task AddModule() { await _moduleLock.WaitAsync().ConfigureAwait(false); @@ -70,17 +90,17 @@ namespace Discord.Commands { var typeInfo = typeof(T).GetTypeInfo(); - if (_moduleDefs.ContainsKey(typeof(T))) + if (_typedModuleDefs.ContainsKey(typeof(T))) throw new ArgumentException($"This module has already been added."); - var module = ModuleClassBuilder.Build(this, typeInfo).First(); + var module = ModuleClassBuilder.Build(this, typeInfo).FirstOrDefault(); - _moduleDefs[typeof(T)] = module; + if (module.Value == default(ModuleInfo)) + throw new InvalidOperationException($"Could not build the module {typeof(T).FullName}, did you pass an invalid type?"); - foreach (var cmd in module.Commands) - _map.AddCommand(cmd); - - return module; + _typedModuleDefs[module.Key] = module.Value; + + return LoadModuleInternal(module.Value); } finally { @@ -89,29 +109,44 @@ namespace Discord.Commands } public async Task> AddModules(Assembly assembly) { - var moduleDefs = ImmutableArray.CreateBuilder(); await _moduleLock.WaitAsync().ConfigureAwait(false); try { - var types = ModuleClassBuilder.Search(assembly); - return ModuleClassBuilder.Build(types, this).ToImmutableArray(); + var types = ModuleClassBuilder.Search(assembly).ToArray(); + var moduleDefs = ModuleClassBuilder.Build(types, this); + + foreach (var info in moduleDefs) + { + _typedModuleDefs[info.Key] = info.Value; + LoadModuleInternal(info.Value); + } + + return moduleDefs.Select(x => x.Value).ToImmutableArray(); } finally { _moduleLock.Release(); } } + private ModuleInfo LoadModuleInternal(ModuleInfo module) + { + _moduleDefs.Add(module); + + foreach (var command in module.Commands) + _map.AddCommand(command); + + foreach (var submodule in module.Submodules) + LoadModuleInternal(submodule); + + return module; + } public async Task RemoveModule(ModuleInfo module) { await _moduleLock.WaitAsync().ConfigureAwait(false); try { - var type = _moduleDefs.FirstOrDefault(x => x.Value == module); - if (default(KeyValuePair).Key == type.Key) - throw new KeyNotFoundException($"Could not find the key for the module {module?.Name ?? module?.Aliases?.FirstOrDefault()}"); - - return RemoveModuleInternal(type.Key); + return RemoveModuleInternal(module); } finally { @@ -123,24 +158,33 @@ namespace Discord.Commands await _moduleLock.WaitAsync().ConfigureAwait(false); try { - return RemoveModuleInternal(typeof(T)); + ModuleInfo module; + _typedModuleDefs.TryGetValue(typeof(T), out module); + if (module == default(ModuleInfo)) + return false; + + return RemoveModuleInternal(module); } finally { _moduleLock.Release(); } } - private bool RemoveModuleInternal(Type type) + private bool RemoveModuleInternal(ModuleInfo module) { - ModuleInfo unloadedModule; - if (_moduleDefs.TryRemove(type, out unloadedModule)) + var defsRemove = module; + if (!_moduleDefs.TryTake(out defsRemove)) + return false; + + foreach (var cmd in module.Commands) + _map.RemoveCommand(cmd); + + foreach (var submodule in module.Submodules) { - foreach (var cmd in unloadedModule.Commands) - _map.RemoveCommand(cmd); - return true; + RemoveModuleInternal(submodule); } - else - return false; + + return true; } //Type Readers diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index 31caaf3a8..8ece1cc2c 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -37,10 +37,14 @@ namespace Discord.Commands Summary = builder.Summary; Remarks = builder.Remarks; + RunMode = builder.RunMode; + Priority = builder.Priority; + 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(); + HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].Multiple : false; _action = builder.Callback; } diff --git a/src/Discord.Net.Commands/Info/ModuleInfo.cs b/src/Discord.Net.Commands/Info/ModuleInfo.cs index 6ec0d657b..f874f5540 100644 --- a/src/Discord.Net.Commands/Info/ModuleInfo.cs +++ b/src/Discord.Net.Commands/Info/ModuleInfo.cs @@ -17,6 +17,7 @@ namespace Discord.Commands public IReadOnlyList Aliases { get; } public IEnumerable Commands { get; } public IReadOnlyList Preconditions { get; } + public IReadOnlyList Submodules { get; } internal ModuleInfo(ModuleBuilder builder, CommandService service) { @@ -29,6 +30,8 @@ namespace Discord.Commands Aliases = BuildAliases(builder).ToImmutableArray(); Commands = builder.Commands.Select(x => x.Build(this, service)); Preconditions = BuildPreconditions(builder).ToImmutableArray(); + + Submodules = BuildSubmodules(builder, service).ToImmutableArray(); } private static IEnumerable BuildAliases(ModuleBuilder builder) @@ -59,13 +62,24 @@ namespace Discord.Commands return result; } + private static List BuildSubmodules(ModuleBuilder parent, CommandService service) + { + var result = new List(); + + foreach (var submodule in parent.Modules) + { + result.Add(submodule.Build(service)); + } + + return result; + } + private static List BuildPreconditions(ModuleBuilder builder) { var result = new List(); - ModuleBuilder parent = builder; - while (parent.ParentModule != null) + while (parent != null) { result.AddRange(parent.Preconditions); parent = parent.ParentModule; diff --git a/src/Discord.Net.Commands/Utilities/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Utilities/ModuleClassBuilder.cs index 30ee1e5ba..2280d9246 100644 --- a/src/Discord.Net.Commands/Utilities/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Utilities/ModuleClassBuilder.cs @@ -25,8 +25,8 @@ namespace Discord.Commands } } - public static IEnumerable Build(CommandService service, params TypeInfo[] validTypes) => Build(validTypes, service); - public static IEnumerable Build(IEnumerable validTypes, CommandService service) + public static Dictionary Build(CommandService service, params TypeInfo[] validTypes) => Build(validTypes, service); + public static Dictionary Build(IEnumerable validTypes, CommandService service) { if (!validTypes.Any()) throw new InvalidOperationException("Could not find any valid modules from the given selection"); @@ -36,22 +36,20 @@ namespace Discord.Commands var builtTypes = new List(); - var result = new List(); + var result = new Dictionary(); foreach (var typeInfo in topLevelGroups) { // this shouldn't be the case; may be safe to remove? - if (builtTypes.Contains(typeInfo)) + if (result.ContainsKey(typeInfo.AsType())) continue; - builtTypes.Add(typeInfo); - var module = new ModuleBuilder(); BuildModule(module, typeInfo, service); BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); - result.Add(module.Build(service)); + result[typeInfo.AsType()] = module.Build(service); } return result; @@ -61,15 +59,18 @@ namespace Discord.Commands { foreach (var typeInfo in subTypes) { + if (!IsValidModuleDefinition(typeInfo)) + continue; + if (builtTypes.Contains(typeInfo)) continue; - - builtTypes.Add(typeInfo); builder.AddSubmodule((module) => { BuildModule(module, typeInfo, service); BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); }); + + builtTypes.Add(typeInfo); } } @@ -89,7 +90,10 @@ namespace Discord.Commands else if (attribute is AliasAttribute) builder.AddAliases((attribute as AliasAttribute).Aliases); else if (attribute is GroupAttribute) + { + builder.Name = builder.Name ?? (attribute as GroupAttribute).Prefix; builder.AddAliases((attribute as GroupAttribute).Prefix); + } else if (attribute is PreconditionAttribute) builder.AddPrecondition(attribute as PreconditionAttribute); } @@ -111,8 +115,17 @@ namespace Discord.Commands foreach (var attribute in attributes) { // TODO: C#7 type switch - if (attribute is NameAttribute) + if (attribute is CommandAttribute) + { + 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) @@ -154,15 +167,15 @@ namespace Discord.Commands var attributes = paramInfo.GetCustomAttributes(); var paramType = paramInfo.ParameterType; + builder.Name = paramInfo.Name; + builder.Optional = paramInfo.IsOptional; builder.DefaultValue = paramInfo.HasDefaultValue ? paramInfo.DefaultValue : null; foreach (var attribute in attributes) { // TODO: C#7 type switch - if (attribute is NameAttribute) - builder.Name = (attribute as NameAttribute).Text; - else if (attribute is SummaryAttribute) + if (attribute is SummaryAttribute) builder.Summary = (attribute as SummaryAttribute).Text; else if (attribute is ParamArrayAttribute) { @@ -193,6 +206,7 @@ namespace Discord.Commands } } + builder.ParameterType = paramType; builder.TypeReader = reader; } @@ -205,7 +219,7 @@ namespace Discord.Commands private static bool IsValidCommandDefinition(MethodInfo methodInfo) { return methodInfo.IsDefined(typeof(CommandAttribute)) && - methodInfo.ReturnType != typeof(Task) && + methodInfo.ReturnType == typeof(Task) && !methodInfo.IsStatic && !methodInfo.IsGenericMethod; } From 196cce05afc0a784e7f40be0cfb0f7281a0ac9f7 Mon Sep 17 00:00:00 2001 From: FiniteReality Date: Fri, 18 Nov 2016 09:24:31 +0000 Subject: [PATCH 7/7] Clean up attribute checking and extraneous casts This probably isn't all of them. This is what I get for copy+pasting code. --- src/Discord.Net.Commands/Utilities/ModuleClassBuilder.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Commands/Utilities/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Utilities/ModuleClassBuilder.cs index 2280d9246..85cd9a664 100644 --- a/src/Discord.Net.Commands/Utilities/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Utilities/ModuleClassBuilder.cs @@ -91,8 +91,9 @@ namespace Discord.Commands builder.AddAliases((attribute as AliasAttribute).Aliases); else if (attribute is GroupAttribute) { - builder.Name = builder.Name ?? (attribute as GroupAttribute).Prefix; - builder.AddAliases((attribute as GroupAttribute).Prefix); + var groupAttr = attribute as GroupAttribute; + builder.Name = builder.Name ?? groupAttr.Prefix; + builder.AddAliases(groupAttr.Prefix); } else if (attribute is PreconditionAttribute) builder.AddPrecondition(attribute as PreconditionAttribute); @@ -132,8 +133,6 @@ namespace Discord.Commands builder.Remarks = (attribute as RemarksAttribute).Text; else if (attribute is AliasAttribute) builder.AddAliases((attribute as AliasAttribute).Aliases); - else if (attribute is GroupAttribute) - builder.AddAliases((attribute as GroupAttribute).Prefix); else if (attribute is PreconditionAttribute) builder.AddPrecondition(attribute as PreconditionAttribute); }