committags/2.05b047bf02bAuthor: Joe4evr <jii.geugten@gmail.com> Date: Fri Feb 2 22:22:00 2018 +0100 [feature/OnModuleAdded] Quickstart fixes (#946) * Quickstart: fix minor derp * Other overdue fixes commitbd3e9eee94Author: Christopher F <computerizedtaco@gmail.com> Date: Sat Jan 27 16:51:18 2018 -0500 Resort usings in ModuleBase commit8042767579Author: Christopher F <computerizedtaco@gmail.com> Date: Sat Jan 27 16:41:39 2018 -0500 Clean up removed owned IServiceProvider commit30066cb102Author: Christopher F <computerizedtaco@gmail.com> Date: Sat Jan 27 16:37:22 2018 -0500 Remove redundant try-catch around OnModuleBuilding invocation If this exception is going to be rethrown, there's no reason to include a try-catch. commit60c7c31d44Author: Christopher F <computerizedtaco@gmail.com> Date: Sat Jan 27 16:36:27 2018 -0500 Include the ModuleBuilder in OnModuleBuilding This allows modules hooking into OnModuleBuilding method to mutate theirselves at runtime. commitb6a9ff5786Author: Joe4evr <jii.geugten@gmail.com> Date: Mon Jan 22 13:17:14 2018 +0100 #DERP commitf623d19c68Author: Joe4evr <jii.geugten@gmail.com> Date: Mon Jan 22 13:15:31 2018 +0100 Resolution for #937 because it's literally 4 lines of code commit8272c9675bAuthor: Joe4evr <jii.geugten@gmail.com> Date: Mon Jan 22 11:39:28 2018 +0100 Re-adjust quickstart commite30b907135Author: Joe4evr <jii.geugten@gmail.com> Date: Mon Jan 22 11:35:08 2018 +0100 Undo experimental changes, request IServiceProvider instance everywhere instead commitad7e0a46c8Author: Joe4evr <jii.geugten@gmail.com> Date: Fri Jan 19 03:40:27 2018 +0100 Fix quickstart leftover from previous draft commite3349ef3d4Author: Joe4evr <jii.geugten@gmail.com> Date: Fri Jan 19 03:33:46 2018 +0100 Doc comment on items commit81bd9111faAuthor: Joe4evr <jii.geugten@gmail.com> Date: Fri Jan 19 03:16:44 2018 +0100 Add comment about the ServiceProviderFactory in the quickstart commit72b5e6c8a1Author: Joe4evr <jii.geugten@gmail.com> Date: Fri Jan 19 03:10:40 2018 +0100 Remove superfluous comments, provide simpler alternative for setting the ServiceProvider. commit74b17b0e04Author: Joe4evr <jii.geugten@gmail.com> Date: Tue Jan 16 18:06:28 2018 +0100 Experimental change for feedback commit7b100e99bbAuthor: Joe4evr <jii.geugten@gmail.com> Date: Mon Jan 15 23:34:06 2018 +0100 * Make the service provider parameters required * Adjust quickstart guide to reflect changes commit7f1b792946Author: Joe4evr <jii.geugten@gmail.com> Date: Mon Jan 15 20:04:37 2018 +0100 I..... missed one. commit031b289d80Author: Joe4evr <jii.geugten@gmail.com> Date: Mon Jan 15 20:02:20 2018 +0100 Rename method to more intuitive 'OnModuleBuilding' commit9a166ef1d0Author: Joe4evr <jii.geugten@gmail.com> Date: Mon Jan 15 19:09:10 2018 +0100 Add callback method for when a module class has been added to the CommandService.
| @@ -19,10 +19,10 @@ class Program | |||||
| private readonly DiscordSocketClient _client; | private readonly DiscordSocketClient _client; | ||||
| // Keep the CommandService and IServiceCollection around for use with commands. | |||||
| // Keep the CommandService and DI container around for use with commands. | |||||
| // These two types require you install the Discord.Net.Commands package. | // These two types require you install the Discord.Net.Commands package. | ||||
| private readonly IServiceCollection _map = new ServiceCollection(); | |||||
| private readonly CommandService _commands = new CommandService(); | |||||
| private readonly CommandService _commands; | |||||
| private readonly IServiceProvider _services; | |||||
| private Program() | private Program() | ||||
| { | { | ||||
| @@ -41,14 +41,45 @@ class Program | |||||
| // add the `using` at the top, and uncomment this line: | // add the `using` at the top, and uncomment this line: | ||||
| //WebSocketProvider = WS4NetProvider.Instance | //WebSocketProvider = WS4NetProvider.Instance | ||||
| }); | }); | ||||
| _commands = new CommandService(new CommandServiceConfig | |||||
| { | |||||
| // Again, log level: | |||||
| LogLevel = LogSeverity.Info, | |||||
| // There's a few more properties you can set, | |||||
| // for example, case-insensitive commands. | |||||
| CaseSensitiveCommands = false, | |||||
| }); | |||||
| // Subscribe the logging handler to both the client and the CommandService. | // Subscribe the logging handler to both the client and the CommandService. | ||||
| _client.Log += Logger; | |||||
| _commands.Log += Logger; | |||||
| _client.Log += Log; | |||||
| _commands.Log += Log; | |||||
| // Setup your DI container. | |||||
| _services = ConfigureServices(), | |||||
| } | |||||
| // If any services require the client, or the CommandService, or something else you keep on hand, | |||||
| // pass them as parameters into this method as needed. | |||||
| // If this method is getting pretty long, you can seperate it out into another file using partials. | |||||
| private static IServiceProvider ConfigureServices() | |||||
| { | |||||
| var map = new ServiceCollection() | |||||
| // Repeat this for all the service classes | |||||
| // and other dependencies that your commands might need. | |||||
| .AddSingleton(new SomeServiceClass()); | |||||
| // When all your required services are in the collection, build the container. | |||||
| // Tip: There's an overload taking in a 'validateScopes' bool to make sure | |||||
| // you haven't made any mistakes in your dependency graph. | |||||
| return map.BuildServiceProvider(); | |||||
| } | } | ||||
| // Example of a logging handler. This can be re-used by addons | // Example of a logging handler. This can be re-used by addons | ||||
| // that ask for a Func<LogMessage, Task>. | // that ask for a Func<LogMessage, Task>. | ||||
| private static Task Logger(LogMessage message) | |||||
| private static Task Log(LogMessage message) | |||||
| { | { | ||||
| switch (message.Severity) | switch (message.Severity) | ||||
| { | { | ||||
| @@ -92,24 +123,15 @@ class Program | |||||
| await Task.Delay(Timeout.Infinite); | await Task.Delay(Timeout.Infinite); | ||||
| } | } | ||||
| private IServiceProvider _services; | |||||
| private async Task InitCommands() | private async Task InitCommands() | ||||
| { | { | ||||
| // Repeat this for all the service classes | |||||
| // and other dependencies that your commands might need. | |||||
| _map.AddSingleton(new SomeServiceClass()); | |||||
| // When all your required services are in the collection, build the container. | |||||
| // Tip: There's an overload taking in a 'validateScopes' bool to make sure | |||||
| // you haven't made any mistakes in your dependency graph. | |||||
| _services = _map.BuildServiceProvider(); | |||||
| // Either search the program and add all Module classes that can be found. | // Either search the program and add all Module classes that can be found. | ||||
| // Module classes MUST be marked 'public' or they will be ignored. | // Module classes MUST be marked 'public' or they will be ignored. | ||||
| await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); | |||||
| // You also need to pass your 'IServiceProvider' instance now, | |||||
| // so make sure that's done before you get here. | |||||
| await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); | |||||
| // Or add Modules manually if you prefer to be a little more explicit: | // Or add Modules manually if you prefer to be a little more explicit: | ||||
| await _commands.AddModuleAsync<SomeModule>(); | |||||
| await _commands.AddModuleAsync<SomeModule>(_services); | |||||
| // Note that the first one is 'Modules' (plural) and the second is 'Module' (singular). | // Note that the first one is 'Modules' (plural) and the second is 'Module' (singular). | ||||
| // Subscribe a handler to see if a message invokes a command. | // Subscribe a handler to see if a message invokes a command. | ||||
| @@ -123,8 +145,6 @@ class Program | |||||
| if (msg == null) return; | if (msg == null) return; | ||||
| // We don't want the bot to respond to itself or other bots. | // We don't want the bot to respond to itself or other bots. | ||||
| // NOTE: Selfbots should invert this first check and remove the second | |||||
| // as they should ONLY be allowed to respond to messages from the same account. | |||||
| if (msg.Author.Id == _client.CurrentUser.Id || msg.Author.IsBot) return; | if (msg.Author.Id == _client.CurrentUser.Id || msg.Author.IsBot) return; | ||||
| // Create a number to track where the prefix ends and the command begins | // Create a number to track where the prefix ends and the command begins | ||||
| @@ -140,10 +160,12 @@ class Program | |||||
| // Execute the command. (result does not indicate a return value, | // Execute the command. (result does not indicate a return value, | ||||
| // rather an object stating if the command executed successfully). | // rather an object stating if the command executed successfully). | ||||
| var result = await _commands.ExecuteAsync(context, pos, _services); | |||||
| var result = await _commands.ExecuteAsync(context, pos); | |||||
| // Uncomment the following lines if you want the bot | // Uncomment the following lines if you want the bot | ||||
| // to send a message if it failed (not advised for most situations). | |||||
| // to send a message if it failed. | |||||
| // This does not catch errors from commands with 'RunMode.Async', | |||||
| // subscribe a handler for '_commands.CommandExecuted' to see those. | |||||
| //if (!result.IsSuccess && result.Error != CommandError.UnknownCommand) | //if (!result.IsSuccess && result.Error != CommandError.UnknownCommand) | ||||
| // await msg.Channel.SendMessageAsync(result.ErrorReason); | // await msg.Channel.SendMessageAsync(result.ErrorReason); | ||||
| } | } | ||||
| @@ -1,5 +1,6 @@ | |||||
| using System; | |||||
| using System; | |||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Reflection; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Microsoft.Extensions.DependencyInjection; | using Microsoft.Extensions.DependencyInjection; | ||||
| @@ -18,6 +19,7 @@ namespace Discord.Commands.Builders | |||||
| public string Name { get; set; } | public string Name { get; set; } | ||||
| public string Summary { get; set; } | public string Summary { get; set; } | ||||
| public string Remarks { get; set; } | public string Remarks { get; set; } | ||||
| public string Group { get; set; } | |||||
| public IReadOnlyList<CommandBuilder> Commands => _commands; | public IReadOnlyList<CommandBuilder> Commands => _commands; | ||||
| public IReadOnlyList<ModuleBuilder> Modules => _submodules; | public IReadOnlyList<ModuleBuilder> Modules => _submodules; | ||||
| @@ -25,6 +27,8 @@ namespace Discord.Commands.Builders | |||||
| public IReadOnlyList<Attribute> Attributes => _attributes; | public IReadOnlyList<Attribute> Attributes => _attributes; | ||||
| public IReadOnlyList<string> Aliases => _aliases; | public IReadOnlyList<string> Aliases => _aliases; | ||||
| internal TypeInfo TypeInfo { get; set; } | |||||
| //Automatic | //Automatic | ||||
| internal ModuleBuilder(CommandService service, ModuleBuilder parent) | internal ModuleBuilder(CommandService service, ModuleBuilder parent) | ||||
| { | { | ||||
| @@ -111,17 +115,23 @@ namespace Discord.Commands.Builders | |||||
| return this; | return this; | ||||
| } | } | ||||
| private ModuleInfo BuildImpl(CommandService service, ModuleInfo parent = null) | |||||
| private ModuleInfo BuildImpl(CommandService service, IServiceProvider services, ModuleInfo parent = null) | |||||
| { | { | ||||
| //Default name to first alias | //Default name to first alias | ||||
| if (Name == null) | if (Name == null) | ||||
| Name = _aliases[0]; | Name = _aliases[0]; | ||||
| return new ModuleInfo(this, service, parent); | |||||
| if (TypeInfo != null) | |||||
| { | |||||
| var moduleInstance = ReflectionUtils.CreateObject<IModuleBase>(TypeInfo, service, services); | |||||
| moduleInstance.OnModuleBuilding(service, this); | |||||
| } | |||||
| return new ModuleInfo(this, service, services, parent); | |||||
| } | } | ||||
| public ModuleInfo Build(CommandService service) => BuildImpl(service); | |||||
| public ModuleInfo Build(CommandService service, IServiceProvider services) => BuildImpl(service, services); | |||||
| internal ModuleInfo Build(CommandService service, ModuleInfo parent) => BuildImpl(service, parent); | |||||
| internal ModuleInfo Build(CommandService service, IServiceProvider services, ModuleInfo parent) => BuildImpl(service, services, parent); | |||||
| } | } | ||||
| } | } | ||||
| @@ -42,8 +42,8 @@ namespace Discord.Commands | |||||
| } | } | ||||
| public static Task<Dictionary<Type, ModuleInfo>> BuildAsync(CommandService service, params TypeInfo[] validTypes) => BuildAsync(validTypes, service); | |||||
| public static async Task<Dictionary<Type, ModuleInfo>> BuildAsync(IEnumerable<TypeInfo> validTypes, CommandService service) | |||||
| public static Task<Dictionary<Type, ModuleInfo>> BuildAsync(CommandService service, IServiceProvider services, params TypeInfo[] validTypes) => BuildAsync(validTypes, service, services); | |||||
| public static async Task<Dictionary<Type, ModuleInfo>> BuildAsync(IEnumerable<TypeInfo> validTypes, CommandService service, IServiceProvider services) | |||||
| { | { | ||||
| /*if (!validTypes.Any()) | /*if (!validTypes.Any()) | ||||
| throw new InvalidOperationException("Could not find any valid modules from the given selection");*/ | throw new InvalidOperationException("Could not find any valid modules from the given selection");*/ | ||||
| @@ -63,11 +63,11 @@ namespace Discord.Commands | |||||
| var module = new ModuleBuilder(service, null); | var module = new ModuleBuilder(service, null); | ||||
| BuildModule(module, typeInfo, service); | |||||
| BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); | |||||
| BuildModule(module, typeInfo, service, services); | |||||
| BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service, services); | |||||
| builtTypes.Add(typeInfo); | builtTypes.Add(typeInfo); | ||||
| result[typeInfo.AsType()] = module.Build(service); | |||||
| result[typeInfo.AsType()] = module.Build(service, services); | |||||
| } | } | ||||
| await service._cmdLogger.DebugAsync($"Successfully built {builtTypes.Count} modules.").ConfigureAwait(false); | await service._cmdLogger.DebugAsync($"Successfully built {builtTypes.Count} modules.").ConfigureAwait(false); | ||||
| @@ -75,7 +75,7 @@ namespace Discord.Commands | |||||
| return result; | return result; | ||||
| } | } | ||||
| private static void BuildSubTypes(ModuleBuilder builder, IEnumerable<TypeInfo> subTypes, List<TypeInfo> builtTypes, CommandService service) | |||||
| private static void BuildSubTypes(ModuleBuilder builder, IEnumerable<TypeInfo> subTypes, List<TypeInfo> builtTypes, CommandService service, IServiceProvider services) | |||||
| { | { | ||||
| foreach (var typeInfo in subTypes) | foreach (var typeInfo in subTypes) | ||||
| { | { | ||||
| @@ -87,17 +87,18 @@ namespace Discord.Commands | |||||
| builder.AddModule((module) => | builder.AddModule((module) => | ||||
| { | { | ||||
| BuildModule(module, typeInfo, service); | |||||
| BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); | |||||
| BuildModule(module, typeInfo, service, services); | |||||
| BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service, services); | |||||
| }); | }); | ||||
| builtTypes.Add(typeInfo); | builtTypes.Add(typeInfo); | ||||
| } | } | ||||
| } | } | ||||
| private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, CommandService service) | |||||
| private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, CommandService service, IServiceProvider services) | |||||
| { | { | ||||
| var attributes = typeInfo.GetCustomAttributes(); | var attributes = typeInfo.GetCustomAttributes(); | ||||
| builder.TypeInfo = typeInfo; | |||||
| foreach (var attribute in attributes) | foreach (var attribute in attributes) | ||||
| { | { | ||||
| @@ -117,6 +118,7 @@ namespace Discord.Commands | |||||
| break; | break; | ||||
| case GroupAttribute group: | case GroupAttribute group: | ||||
| builder.Name = builder.Name ?? group.Prefix; | builder.Name = builder.Name ?? group.Prefix; | ||||
| builder.Group = group.Prefix; | |||||
| builder.AddAliases(group.Prefix); | builder.AddAliases(group.Prefix); | ||||
| break; | break; | ||||
| case PreconditionAttribute precondition: | case PreconditionAttribute precondition: | ||||
| @@ -140,12 +142,12 @@ namespace Discord.Commands | |||||
| { | { | ||||
| builder.AddCommand((command) => | builder.AddCommand((command) => | ||||
| { | { | ||||
| BuildCommand(command, typeInfo, method, service); | |||||
| BuildCommand(command, typeInfo, method, service, services); | |||||
| }); | }); | ||||
| } | } | ||||
| } | } | ||||
| private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service) | |||||
| private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service, IServiceProvider serviceprovider) | |||||
| { | { | ||||
| var attributes = method.GetCustomAttributes(); | var attributes = method.GetCustomAttributes(); | ||||
| @@ -191,7 +193,7 @@ namespace Discord.Commands | |||||
| { | { | ||||
| builder.AddParameter((parameter) => | builder.AddParameter((parameter) => | ||||
| { | { | ||||
| BuildParameter(parameter, paramInfo, pos++, count, service); | |||||
| BuildParameter(parameter, paramInfo, pos++, count, service, serviceprovider); | |||||
| }); | }); | ||||
| } | } | ||||
| @@ -227,7 +229,7 @@ namespace Discord.Commands | |||||
| builder.Callback = ExecuteCallback; | builder.Callback = ExecuteCallback; | ||||
| } | } | ||||
| private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service) | |||||
| private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service, IServiceProvider services) | |||||
| { | { | ||||
| var attributes = paramInfo.GetCustomAttributes(); | var attributes = paramInfo.GetCustomAttributes(); | ||||
| var paramType = paramInfo.ParameterType; | var paramType = paramInfo.ParameterType; | ||||
| @@ -245,7 +247,7 @@ namespace Discord.Commands | |||||
| builder.Summary = summary.Text; | builder.Summary = summary.Text; | ||||
| break; | break; | ||||
| case OverrideTypeReaderAttribute typeReader: | case OverrideTypeReaderAttribute typeReader: | ||||
| builder.TypeReader = GetTypeReader(service, paramType, typeReader.TypeReader); | |||||
| builder.TypeReader = GetTypeReader(service, paramType, typeReader.TypeReader, services); | |||||
| break; | break; | ||||
| case ParamArrayAttribute _: | case ParamArrayAttribute _: | ||||
| builder.IsMultiple = true; | builder.IsMultiple = true; | ||||
| @@ -285,7 +287,7 @@ namespace Discord.Commands | |||||
| } | } | ||||
| } | } | ||||
| private static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType) | |||||
| private static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services) | |||||
| { | { | ||||
| var readers = service.GetTypeReaders(paramType); | var readers = service.GetTypeReaders(paramType); | ||||
| TypeReader reader = null; | TypeReader reader = null; | ||||
| @@ -296,7 +298,7 @@ namespace Discord.Commands | |||||
| } | } | ||||
| //We dont have a cached type reader, create one | //We dont have a cached type reader, create one | ||||
| reader = ReflectionUtils.CreateObject<TypeReader>(typeReaderType.GetTypeInfo(), service, EmptyServiceProvider.Instance); | |||||
| reader = ReflectionUtils.CreateObject<TypeReader>(typeReaderType.GetTypeInfo(), service, services); | |||||
| service.AddTypeReader(paramType, reader); | service.AddTypeReader(paramType, reader); | ||||
| return reader; | return reader; | ||||
| @@ -1,5 +1,3 @@ | |||||
| using Discord.Commands.Builders; | |||||
| using Discord.Logging; | |||||
| using System; | using System; | ||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| @@ -8,6 +6,9 @@ using System.Linq; | |||||
| using System.Reflection; | using System.Reflection; | ||||
| using System.Threading; | using System.Threading; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Microsoft.Extensions.DependencyInjection; | |||||
| using Discord.Commands.Builders; | |||||
| using Discord.Logging; | |||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| { | { | ||||
| @@ -85,7 +86,8 @@ namespace Discord.Commands | |||||
| var builder = new ModuleBuilder(this, null, primaryAlias); | var builder = new ModuleBuilder(this, null, primaryAlias); | ||||
| buildFunc(builder); | buildFunc(builder); | ||||
| var module = builder.Build(this); | |||||
| var module = builder.Build(this, null); | |||||
| return LoadModuleInternal(module); | return LoadModuleInternal(module); | ||||
| } | } | ||||
| finally | finally | ||||
| @@ -93,8 +95,8 @@ namespace Discord.Commands | |||||
| _moduleLock.Release(); | _moduleLock.Release(); | ||||
| } | } | ||||
| } | } | ||||
| public Task<ModuleInfo> AddModuleAsync<T>() => AddModuleAsync(typeof(T)); | |||||
| public async Task<ModuleInfo> AddModuleAsync(Type type) | |||||
| public Task<ModuleInfo> AddModuleAsync<T>(IServiceProvider services) => AddModuleAsync(typeof(T), services); | |||||
| public async Task<ModuleInfo> AddModuleAsync(Type type, IServiceProvider services) | |||||
| { | { | ||||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | await _moduleLock.WaitAsync().ConfigureAwait(false); | ||||
| try | try | ||||
| @@ -104,7 +106,7 @@ namespace Discord.Commands | |||||
| if (_typedModuleDefs.ContainsKey(type)) | if (_typedModuleDefs.ContainsKey(type)) | ||||
| throw new ArgumentException($"This module has already been added."); | throw new ArgumentException($"This module has already been added."); | ||||
| var module = (await ModuleClassBuilder.BuildAsync(this, typeInfo).ConfigureAwait(false)).FirstOrDefault(); | |||||
| var module = (await ModuleClassBuilder.BuildAsync(this, services, typeInfo).ConfigureAwait(false)).FirstOrDefault(); | |||||
| if (module.Value == default(ModuleInfo)) | if (module.Value == default(ModuleInfo)) | ||||
| throw new InvalidOperationException($"Could not build the module {type.FullName}, did you pass an invalid type?"); | throw new InvalidOperationException($"Could not build the module {type.FullName}, did you pass an invalid type?"); | ||||
| @@ -118,13 +120,13 @@ namespace Discord.Commands | |||||
| _moduleLock.Release(); | _moduleLock.Release(); | ||||
| } | } | ||||
| } | } | ||||
| public async Task<IEnumerable<ModuleInfo>> AddModulesAsync(Assembly assembly) | |||||
| public async Task<IEnumerable<ModuleInfo>> AddModulesAsync(Assembly assembly, IServiceProvider services) | |||||
| { | { | ||||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | await _moduleLock.WaitAsync().ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| var types = await ModuleClassBuilder.SearchAsync(assembly, this).ConfigureAwait(false); | var types = await ModuleClassBuilder.SearchAsync(assembly, this).ConfigureAwait(false); | ||||
| var moduleDefs = await ModuleClassBuilder.BuildAsync(types, this).ConfigureAwait(false); | |||||
| var moduleDefs = await ModuleClassBuilder.BuildAsync(types, this, services).ConfigureAwait(false); | |||||
| foreach (var info in moduleDefs) | foreach (var info in moduleDefs) | ||||
| { | { | ||||
| @@ -224,7 +226,7 @@ namespace Discord.Commands | |||||
| var readers = _typeReaders.GetOrAdd(typeof(Nullable<>).MakeGenericType(valueType), x => new ConcurrentDictionary<Type, TypeReader>()); | var readers = _typeReaders.GetOrAdd(typeof(Nullable<>).MakeGenericType(valueType), x => new ConcurrentDictionary<Type, TypeReader>()); | ||||
| var nullableReader = NullableTypeReader.Create(valueType, valueTypeReader); | var nullableReader = NullableTypeReader.Create(valueType, valueTypeReader); | ||||
| readers[nullableReader.GetType()] = nullableReader; | readers[nullableReader.GetType()] = nullableReader; | ||||
| } | |||||
| } | |||||
| internal IDictionary<Type, TypeReader> GetTypeReaders(Type type) | internal IDictionary<Type, TypeReader> GetTypeReaders(Type type) | ||||
| { | { | ||||
| if (_typeReaders.TryGetValue(type, out var definedTypeReaders)) | if (_typeReaders.TryGetValue(type, out var definedTypeReaders)) | ||||
| @@ -277,92 +279,94 @@ namespace Discord.Commands | |||||
| public async Task<IResult> ExecuteAsync(ICommandContext context, string input, IServiceProvider services = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) | public async Task<IResult> ExecuteAsync(ICommandContext context, string input, IServiceProvider services = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) | ||||
| { | { | ||||
| services = services ?? EmptyServiceProvider.Instance; | services = services ?? EmptyServiceProvider.Instance; | ||||
| var searchResult = Search(context, input); | |||||
| if (!searchResult.IsSuccess) | |||||
| return searchResult; | |||||
| var commands = searchResult.Commands; | |||||
| var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>(); | |||||
| foreach (var match in commands) | |||||
| using (var scope = services.CreateScope()) | |||||
| { | { | ||||
| preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false); | |||||
| } | |||||
| var searchResult = Search(context, input); | |||||
| if (!searchResult.IsSuccess) | |||||
| return searchResult; | |||||
| var successfulPreconditions = preconditionResults | |||||
| .Where(x => x.Value.IsSuccess) | |||||
| .ToArray(); | |||||
| var commands = searchResult.Commands; | |||||
| var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>(); | |||||
| if (successfulPreconditions.Length == 0) | |||||
| { | |||||
| //All preconditions failed, return the one from the highest priority command | |||||
| var bestCandidate = preconditionResults | |||||
| .OrderByDescending(x => x.Key.Command.Priority) | |||||
| .FirstOrDefault(x => !x.Value.IsSuccess); | |||||
| return bestCandidate.Value; | |||||
| } | |||||
| foreach (var match in commands) | |||||
| { | |||||
| preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, scope.ServiceProvider).ConfigureAwait(false); | |||||
| } | |||||
| //If we get this far, at least one precondition was successful. | |||||
| var successfulPreconditions = preconditionResults | |||||
| .Where(x => x.Value.IsSuccess) | |||||
| .ToArray(); | |||||
| var parseResultsDict = new Dictionary<CommandMatch, ParseResult>(); | |||||
| foreach (var pair in successfulPreconditions) | |||||
| { | |||||
| var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false); | |||||
| if (successfulPreconditions.Length == 0) | |||||
| { | |||||
| //All preconditions failed, return the one from the highest priority command | |||||
| var bestCandidate = preconditionResults | |||||
| .OrderByDescending(x => x.Key.Command.Priority) | |||||
| .FirstOrDefault(x => !x.Value.IsSuccess); | |||||
| return bestCandidate.Value; | |||||
| } | |||||
| if (parseResult.Error == CommandError.MultipleMatches) | |||||
| //If we get this far, at least one precondition was successful. | |||||
| var parseResultsDict = new Dictionary<CommandMatch, ParseResult>(); | |||||
| foreach (var pair in successfulPreconditions) | |||||
| { | { | ||||
| IReadOnlyList<TypeReaderValue> argList, paramList; | |||||
| switch (multiMatchHandling) | |||||
| var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, scope.ServiceProvider).ConfigureAwait(false); | |||||
| if (parseResult.Error == CommandError.MultipleMatches) | |||||
| { | { | ||||
| case MultiMatchHandling.Best: | |||||
| argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); | |||||
| paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); | |||||
| parseResult = ParseResult.FromSuccess(argList, paramList); | |||||
| break; | |||||
| IReadOnlyList<TypeReaderValue> argList, paramList; | |||||
| switch (multiMatchHandling) | |||||
| { | |||||
| case MultiMatchHandling.Best: | |||||
| argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); | |||||
| paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); | |||||
| parseResult = ParseResult.FromSuccess(argList, paramList); | |||||
| break; | |||||
| } | |||||
| } | } | ||||
| } | |||||
| parseResultsDict[pair.Key] = parseResult; | |||||
| } | |||||
| parseResultsDict[pair.Key] = parseResult; | |||||
| } | |||||
| // Calculates the 'score' of a command given a parse result | |||||
| float CalculateScore(CommandMatch match, ParseResult parseResult) | |||||
| { | |||||
| float argValuesScore = 0, paramValuesScore = 0; | |||||
| if (match.Command.Parameters.Count > 0) | |||||
| // Calculates the 'score' of a command given a parse result | |||||
| float CalculateScore(CommandMatch match, ParseResult parseResult) | |||||
| { | { | ||||
| var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; | |||||
| var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; | |||||
| float argValuesScore = 0, paramValuesScore = 0; | |||||
| if (match.Command.Parameters.Count > 0) | |||||
| { | |||||
| var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; | |||||
| var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; | |||||
| argValuesScore = argValuesSum / match.Command.Parameters.Count; | |||||
| paramValuesScore = paramValuesSum / match.Command.Parameters.Count; | |||||
| argValuesScore = argValuesSum / match.Command.Parameters.Count; | |||||
| paramValuesScore = paramValuesSum / match.Command.Parameters.Count; | |||||
| } | |||||
| var totalArgsScore = (argValuesScore + paramValuesScore) / 2; | |||||
| return match.Command.Priority + totalArgsScore * 0.99f; | |||||
| } | } | ||||
| var totalArgsScore = (argValuesScore + paramValuesScore) / 2; | |||||
| return match.Command.Priority + totalArgsScore * 0.99f; | |||||
| } | |||||
| //Order the parse results by their score so that we choose the most likely result to execute | |||||
| var parseResults = parseResultsDict | |||||
| .OrderByDescending(x => CalculateScore(x.Key, x.Value)); | |||||
| //Order the parse results by their score so that we choose the most likely result to execute | |||||
| var parseResults = parseResultsDict | |||||
| .OrderByDescending(x => CalculateScore(x.Key, x.Value)); | |||||
| var successfulParses = parseResults | |||||
| .Where(x => x.Value.IsSuccess) | |||||
| .ToArray(); | |||||
| var successfulParses = parseResults | |||||
| .Where(x => x.Value.IsSuccess) | |||||
| .ToArray(); | |||||
| if (successfulParses.Length == 0) | |||||
| { | |||||
| //All parses failed, return the one from the highest priority command, using score as a tie breaker | |||||
| var bestMatch = parseResults | |||||
| .FirstOrDefault(x => !x.Value.IsSuccess); | |||||
| return bestMatch.Value; | |||||
| } | |||||
| if (successfulParses.Length == 0) | |||||
| { | |||||
| //All parses failed, return the one from the highest priority command, using score as a tie breaker | |||||
| var bestMatch = parseResults | |||||
| .FirstOrDefault(x => !x.Value.IsSuccess); | |||||
| return bestMatch.Value; | |||||
| //If we get this far, at least one parse was successful. Execute the most likely overload. | |||||
| var chosenOverload = successfulParses[0]; | |||||
| return await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, scope.ServiceProvider).ConfigureAwait(false); | |||||
| } | } | ||||
| //If we get this far, at least one parse was successful. Execute the most likely overload. | |||||
| var chosenOverload = successfulParses[0]; | |||||
| return await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,4 +1,6 @@ | |||||
| namespace Discord.Commands | |||||
| using System; | |||||
| namespace Discord.Commands | |||||
| { | { | ||||
| public class CommandServiceConfig | public class CommandServiceConfig | ||||
| { | { | ||||
| @@ -18,5 +20,11 @@ | |||||
| /// <summary> Determines whether extra parameters should be ignored. </summary> | /// <summary> Determines whether extra parameters should be ignored. </summary> | ||||
| public bool IgnoreExtraArgs { get; set; } = false; | public bool IgnoreExtraArgs { get; set; } = false; | ||||
| ///// <summary> Gets or sets the <see cref="IServiceProvider"/> to use. </summary> | |||||
| //public IServiceProvider ServiceProvider { get; set; } = null; | |||||
| ///// <summary> Gets or sets a factory function for the <see cref="IServiceProvider"/> to use. </summary> | |||||
| //public Func<CommandService, IServiceProvider> ServiceProviderFactory { get; set; } = null; | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,4 +1,6 @@ | |||||
| namespace Discord.Commands | |||||
| using Discord.Commands.Builders; | |||||
| namespace Discord.Commands | |||||
| { | { | ||||
| internal interface IModuleBase | internal interface IModuleBase | ||||
| { | { | ||||
| @@ -7,5 +9,7 @@ | |||||
| void BeforeExecute(CommandInfo command); | void BeforeExecute(CommandInfo command); | ||||
| void AfterExecute(CommandInfo command); | void AfterExecute(CommandInfo command); | ||||
| void OnModuleBuilding(CommandService commandService, ModuleBuilder builder); | |||||
| } | } | ||||
| } | } | ||||
| @@ -2,7 +2,7 @@ using System; | |||||
| using System.Linq; | using System.Linq; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| using System.Reflection; | |||||
| using Discord.Commands.Builders; | using Discord.Commands.Builders; | ||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| @@ -13,6 +13,7 @@ namespace Discord.Commands | |||||
| public string Name { get; } | public string Name { get; } | ||||
| public string Summary { get; } | public string Summary { get; } | ||||
| public string Remarks { get; } | public string Remarks { get; } | ||||
| public string Group { get; } | |||||
| public IReadOnlyList<string> Aliases { get; } | public IReadOnlyList<string> Aliases { get; } | ||||
| public IReadOnlyList<CommandInfo> Commands { get; } | public IReadOnlyList<CommandInfo> Commands { get; } | ||||
| @@ -22,21 +23,26 @@ namespace Discord.Commands | |||||
| public ModuleInfo Parent { get; } | public ModuleInfo Parent { get; } | ||||
| public bool IsSubmodule => Parent != null; | public bool IsSubmodule => Parent != null; | ||||
| internal ModuleInfo(ModuleBuilder builder, CommandService service, ModuleInfo parent = null) | |||||
| //public TypeInfo TypeInfo { get; } | |||||
| internal ModuleInfo(ModuleBuilder builder, CommandService service, IServiceProvider services, ModuleInfo parent = null) | |||||
| { | { | ||||
| Service = service; | Service = service; | ||||
| Name = builder.Name; | Name = builder.Name; | ||||
| Summary = builder.Summary; | Summary = builder.Summary; | ||||
| Remarks = builder.Remarks; | Remarks = builder.Remarks; | ||||
| Group = builder.Group; | |||||
| Parent = parent; | Parent = parent; | ||||
| //TypeInfo = builder.TypeInfo; | |||||
| Aliases = BuildAliases(builder, service).ToImmutableArray(); | Aliases = BuildAliases(builder, service).ToImmutableArray(); | ||||
| Commands = builder.Commands.Select(x => x.Build(this, service)).ToImmutableArray(); | Commands = builder.Commands.Select(x => x.Build(this, service)).ToImmutableArray(); | ||||
| Preconditions = BuildPreconditions(builder).ToImmutableArray(); | Preconditions = BuildPreconditions(builder).ToImmutableArray(); | ||||
| Attributes = BuildAttributes(builder).ToImmutableArray(); | Attributes = BuildAttributes(builder).ToImmutableArray(); | ||||
| Submodules = BuildSubmodules(builder, service).ToImmutableArray(); | |||||
| Submodules = BuildSubmodules(builder, service, services).ToImmutableArray(); | |||||
| } | } | ||||
| private static IEnumerable<string> BuildAliases(ModuleBuilder builder, CommandService service) | private static IEnumerable<string> BuildAliases(ModuleBuilder builder, CommandService service) | ||||
| @@ -66,12 +72,12 @@ namespace Discord.Commands | |||||
| return result; | return result; | ||||
| } | } | ||||
| private List<ModuleInfo> BuildSubmodules(ModuleBuilder parent, CommandService service) | |||||
| private List<ModuleInfo> BuildSubmodules(ModuleBuilder parent, CommandService service, IServiceProvider services) | |||||
| { | { | ||||
| var result = new List<ModuleInfo>(); | var result = new List<ModuleInfo>(); | ||||
| foreach (var submodule in parent.Modules) | foreach (var submodule in parent.Modules) | ||||
| result.Add(submodule.Build(service, this)); | |||||
| result.Add(submodule.Build(service, services, this)); | |||||
| return result; | return result; | ||||
| } | } | ||||
| @@ -1,5 +1,6 @@ | |||||
| using System; | |||||
| using System; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Discord.Commands.Builders; | |||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| { | { | ||||
| @@ -23,15 +24,18 @@ namespace Discord.Commands | |||||
| { | { | ||||
| } | } | ||||
| protected virtual void OnModuleBuilding(CommandService commandService, ModuleBuilder builder) | |||||
| { | |||||
| } | |||||
| //IModuleBase | //IModuleBase | ||||
| void IModuleBase.SetContext(ICommandContext context) | void IModuleBase.SetContext(ICommandContext context) | ||||
| { | { | ||||
| var newValue = context as T; | var newValue = context as T; | ||||
| Context = newValue ?? throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}"); | Context = newValue ?? throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}"); | ||||
| } | } | ||||
| void IModuleBase.BeforeExecute(CommandInfo command) => BeforeExecute(command); | void IModuleBase.BeforeExecute(CommandInfo command) => BeforeExecute(command); | ||||
| void IModuleBase.AfterExecute(CommandInfo command) => AfterExecute(command); | void IModuleBase.AfterExecute(CommandInfo command) => AfterExecute(command); | ||||
| void IModuleBase.OnModuleBuilding(CommandService commandService, ModuleBuilder builder) => OnModuleBuilding(commandService, builder); | |||||
| } | } | ||||
| } | } | ||||