| @@ -0,0 +1,9 @@ | |||||
| using System; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| [AttributeUsage(AttributeTargets.Class)] | |||||
| public class DontAutoLoadAttribute : Attribute | |||||
| { | |||||
| } | |||||
| } | |||||
| @@ -1,8 +0,0 @@ | |||||
| using System; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| public class InAttribute : Attribute | |||||
| { | |||||
| } | |||||
| } | |||||
| @@ -1,22 +0,0 @@ | |||||
| using System; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| [AttributeUsage(AttributeTargets.Class)] | |||||
| public class ModuleAttribute : Attribute | |||||
| { | |||||
| public string Prefix { get; } | |||||
| public bool AutoLoad { get; set; } | |||||
| public ModuleAttribute() | |||||
| { | |||||
| Prefix = null; | |||||
| AutoLoad = true; | |||||
| } | |||||
| public ModuleAttribute(string prefix) | |||||
| { | |||||
| Prefix = prefix; | |||||
| AutoLoad = true; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -6,6 +6,6 @@ namespace Discord.Commands | |||||
| [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] | ||||
| public abstract class PreconditionAttribute : Attribute | public abstract class PreconditionAttribute : Attribute | ||||
| { | { | ||||
| public abstract Task<PreconditionResult> CheckPermissions(CommandContext context, Command executingCommand, object moduleInstance); | |||||
| public abstract Task<PreconditionResult> CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map); | |||||
| } | } | ||||
| } | } | ||||
| @@ -21,7 +21,7 @@ namespace Discord.Commands | |||||
| Contexts = contexts; | Contexts = contexts; | ||||
| } | } | ||||
| public override Task<PreconditionResult> CheckPermissions(CommandContext context, Command executingCommand, object moduleInstance) | |||||
| public override Task<PreconditionResult> CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map) | |||||
| { | { | ||||
| bool isValid = false; | bool isValid = false; | ||||
| @@ -20,7 +20,7 @@ namespace Discord.Commands | |||||
| GuildPermission = null; | GuildPermission = null; | ||||
| } | } | ||||
| public override Task<PreconditionResult> CheckPermissions(CommandContext context, Command executingCommand, object moduleInstance) | |||||
| public override Task<PreconditionResult> CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map) | |||||
| { | { | ||||
| var guildUser = context.User as IGuildUser; | var guildUser = context.User as IGuildUser; | ||||
| @@ -10,16 +10,15 @@ using System.Threading.Tasks; | |||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| { | { | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class Command | |||||
| public class CommandInfo | |||||
| { | { | ||||
| private static readonly MethodInfo _convertParamsMethod = typeof(Command).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); | |||||
| private static readonly MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); | |||||
| private static readonly ConcurrentDictionary<Type, Func<IEnumerable<object>, object>> _arrayConverters = new ConcurrentDictionary<Type, Func<IEnumerable<object>, object>>(); | private static readonly ConcurrentDictionary<Type, Func<IEnumerable<object>, object>> _arrayConverters = new ConcurrentDictionary<Type, Func<IEnumerable<object>, object>>(); | ||||
| private readonly object _instance; | |||||
| private readonly Func<CommandContext, IReadOnlyList<object>, Task> _action; | |||||
| private readonly Func<CommandContext, object[], Task> _action; | |||||
| public MethodInfo Source { get; } | public MethodInfo Source { get; } | ||||
| public Module Module { get; } | |||||
| public ModuleInfo Module { get; } | |||||
| public string Name { get; } | public string Name { get; } | ||||
| public string Summary { get; } | public string Summary { get; } | ||||
| public string Remarks { get; } | public string Remarks { get; } | ||||
| @@ -30,13 +29,12 @@ namespace Discord.Commands | |||||
| public IReadOnlyList<CommandParameter> Parameters { get; } | public IReadOnlyList<CommandParameter> Parameters { get; } | ||||
| public IReadOnlyList<PreconditionAttribute> Preconditions { get; } | public IReadOnlyList<PreconditionAttribute> Preconditions { get; } | ||||
| internal Command(MethodInfo source, Module module, object instance, CommandAttribute attribute, string groupPrefix) | |||||
| internal CommandInfo(MethodInfo source, ModuleInfo module, CommandAttribute attribute, string groupPrefix) | |||||
| { | { | ||||
| try | try | ||||
| { | { | ||||
| Source = source; | Source = source; | ||||
| Module = module; | Module = module; | ||||
| _instance = instance; | |||||
| Name = source.Name; | Name = source.Name; | ||||
| @@ -85,18 +83,18 @@ namespace Discord.Commands | |||||
| } | } | ||||
| } | } | ||||
| public async Task<PreconditionResult> CheckPreconditions(CommandContext context) | |||||
| public async Task<PreconditionResult> CheckPreconditions(CommandContext context, IDependencyMap map = null) | |||||
| { | { | ||||
| foreach (PreconditionAttribute precondition in Module.Preconditions) | foreach (PreconditionAttribute precondition in Module.Preconditions) | ||||
| { | { | ||||
| var result = await precondition.CheckPermissions(context, this, Module.Instance).ConfigureAwait(false); | |||||
| var result = await precondition.CheckPermissions(context, this, map).ConfigureAwait(false); | |||||
| if (!result.IsSuccess) | if (!result.IsSuccess) | ||||
| return result; | return result; | ||||
| } | } | ||||
| foreach (PreconditionAttribute precondition in Preconditions) | foreach (PreconditionAttribute precondition in Preconditions) | ||||
| { | { | ||||
| var result = await precondition.CheckPermissions(context, this, Module.Instance).ConfigureAwait(false); | |||||
| var result = await precondition.CheckPermissions(context, this, map).ConfigureAwait(false); | |||||
| if (!result.IsSuccess) | if (!result.IsSuccess) | ||||
| return result; | return result; | ||||
| } | } | ||||
| @@ -169,11 +167,9 @@ namespace Discord.Commands | |||||
| private IReadOnlyList<CommandParameter> BuildParameters(MethodInfo methodInfo) | private IReadOnlyList<CommandParameter> BuildParameters(MethodInfo methodInfo) | ||||
| { | { | ||||
| var parameters = methodInfo.GetParameters(); | var parameters = methodInfo.GetParameters(); | ||||
| if (parameters.Length == 0 || parameters[0].ParameterType != typeof(CommandContext)) | |||||
| throw new InvalidOperationException($"The first parameter of a command must be {nameof(CommandContext)}."); | |||||
| var paramBuilder = ImmutableArray.CreateBuilder<CommandParameter>(parameters.Length - 1); | |||||
| for (int i = 1; i < parameters.Length; i++) | |||||
| var paramBuilder = ImmutableArray.CreateBuilder<CommandParameter>(parameters.Length); | |||||
| for (int i = 0; i < parameters.Length; i++) | |||||
| { | { | ||||
| var parameter = parameters[i]; | var parameter = parameters[i]; | ||||
| var type = parameter.ParameterType; | var type = parameter.ParameterType; | ||||
| @@ -209,19 +205,23 @@ namespace Discord.Commands | |||||
| } | } | ||||
| return paramBuilder.ToImmutable(); | return paramBuilder.ToImmutable(); | ||||
| } | } | ||||
| private Func<CommandContext, IReadOnlyList<object>, Task> BuildAction(MethodInfo methodInfo) | |||||
| private Func<CommandContext, object[], Task> BuildAction(MethodInfo methodInfo) | |||||
| { | { | ||||
| if (methodInfo.ReturnType != typeof(Task)) | if (methodInfo.ReturnType != typeof(Task)) | ||||
| throw new InvalidOperationException("Commands must return a non-generic Task."); | throw new InvalidOperationException("Commands must return a non-generic Task."); | ||||
| return (msg, args) => | |||||
| return (context, args) => | |||||
| { | { | ||||
| object[] newArgs = new object[args.Count + 1]; | |||||
| newArgs[0] = msg; | |||||
| for (int i = 0; i < args.Count; i++) | |||||
| newArgs[i + 1] = args[i]; | |||||
| var result = methodInfo.Invoke(_instance, newArgs); | |||||
| return result as Task ?? Task.CompletedTask; | |||||
| var instance = Module.CreateInstance(); | |||||
| instance.Context = context; | |||||
| try | |||||
| { | |||||
| return methodInfo.Invoke(instance, args) as Task ?? Task.CompletedTask; | |||||
| } | |||||
| finally | |||||
| { | |||||
| (instance as IDisposable)?.Dispose(); | |||||
| } | |||||
| }; | }; | ||||
| } | } | ||||
| @@ -13,7 +13,7 @@ namespace Discord.Commands | |||||
| QuotedParameter | QuotedParameter | ||||
| } | } | ||||
| public static async Task<ParseResult> ParseArgs(Command command, CommandContext context, string input, int startPos) | |||||
| public static async Task<ParseResult> ParseArgs(CommandInfo command, CommandContext context, string input, int startPos) | |||||
| { | { | ||||
| CommandParameter curParam = null; | CommandParameter curParam = null; | ||||
| StringBuilder argBuilder = new StringBuilder(input.Length); | StringBuilder argBuilder = new StringBuilder(input.Length); | ||||
| @@ -11,18 +11,20 @@ namespace Discord.Commands | |||||
| { | { | ||||
| public class CommandService | public class CommandService | ||||
| { | { | ||||
| private static readonly TypeInfo _moduleTypeInfo = typeof(ModuleBase).GetTypeInfo(); | |||||
| private readonly SemaphoreSlim _moduleLock; | private readonly SemaphoreSlim _moduleLock; | ||||
| private readonly ConcurrentDictionary<Type, Module> _modules; | |||||
| private readonly ConcurrentDictionary<Type, ModuleInfo> _moduleDefs; | |||||
| private readonly ConcurrentDictionary<Type, TypeReader> _typeReaders; | private readonly ConcurrentDictionary<Type, TypeReader> _typeReaders; | ||||
| private readonly CommandMap _map; | private readonly CommandMap _map; | ||||
| public IEnumerable<Module> Modules => _modules.Select(x => x.Value); | |||||
| public IEnumerable<Command> Commands => _modules.SelectMany(x => x.Value.Commands); | |||||
| public IEnumerable<ModuleInfo> Modules => _moduleDefs.Select(x => x.Value); | |||||
| public IEnumerable<CommandInfo> Commands => _moduleDefs.SelectMany(x => x.Value.Commands); | |||||
| public CommandService() | public CommandService() | ||||
| { | { | ||||
| _moduleLock = new SemaphoreSlim(1, 1); | _moduleLock = new SemaphoreSlim(1, 1); | ||||
| _modules = new ConcurrentDictionary<Type, Module>(); | |||||
| _moduleDefs = new ConcurrentDictionary<Type, ModuleInfo>(); | |||||
| _map = new CommandMap(); | _map = new CommandMap(); | ||||
| _typeReaders = new ConcurrentDictionary<Type, TypeReader> | _typeReaders = new ConcurrentDictionary<Type, TypeReader> | ||||
| { | { | ||||
| @@ -45,7 +47,6 @@ namespace Discord.Commands | |||||
| [typeof(IMessage)] = new MessageTypeReader<IMessage>(), | [typeof(IMessage)] = new MessageTypeReader<IMessage>(), | ||||
| [typeof(IUserMessage)] = new MessageTypeReader<IUserMessage>(), | [typeof(IUserMessage)] = new MessageTypeReader<IUserMessage>(), | ||||
| //[typeof(ISystemMessage)] = new MessageTypeReader<ISystemMessage>(), | |||||
| [typeof(IChannel)] = new ChannelTypeReader<IChannel>(), | [typeof(IChannel)] = new ChannelTypeReader<IChannel>(), | ||||
| [typeof(IDMChannel)] = new ChannelTypeReader<IDMChannel>(), | [typeof(IDMChannel)] = new ChannelTypeReader<IDMChannel>(), | ||||
| [typeof(IGroupChannel)] = new ChannelTypeReader<IGroupChannel>(), | [typeof(IGroupChannel)] = new ChannelTypeReader<IGroupChannel>(), | ||||
| @@ -55,120 +56,99 @@ namespace Discord.Commands | |||||
| [typeof(ITextChannel)] = new ChannelTypeReader<ITextChannel>(), | [typeof(ITextChannel)] = new ChannelTypeReader<ITextChannel>(), | ||||
| [typeof(IVoiceChannel)] = new ChannelTypeReader<IVoiceChannel>(), | [typeof(IVoiceChannel)] = new ChannelTypeReader<IVoiceChannel>(), | ||||
| //[typeof(IGuild)] = new GuildTypeReader<IGuild>(), | |||||
| [typeof(IRole)] = new RoleTypeReader<IRole>(), | [typeof(IRole)] = new RoleTypeReader<IRole>(), | ||||
| //[typeof(IInvite)] = new InviteTypeReader<IInvite>(), | |||||
| //[typeof(IInviteMetadata)] = new InviteTypeReader<IInviteMetadata>(), | |||||
| [typeof(IUser)] = new UserTypeReader<IUser>(), | [typeof(IUser)] = new UserTypeReader<IUser>(), | ||||
| [typeof(IGroupUser)] = new UserTypeReader<IGroupUser>(), | [typeof(IGroupUser)] = new UserTypeReader<IGroupUser>(), | ||||
| [typeof(IGuildUser)] = new UserTypeReader<IGuildUser>(), | [typeof(IGuildUser)] = new UserTypeReader<IGuildUser>(), | ||||
| }; | }; | ||||
| } | } | ||||
| public void AddTypeReader<T>(TypeReader reader) | |||||
| { | |||||
| _typeReaders[typeof(T)] = reader; | |||||
| } | |||||
| public void AddTypeReader(Type type, TypeReader reader) | |||||
| { | |||||
| _typeReaders[type] = reader; | |||||
| } | |||||
| internal TypeReader GetTypeReader(Type type) | |||||
| { | |||||
| TypeReader reader; | |||||
| if (_typeReaders.TryGetValue(type, out reader)) | |||||
| return reader; | |||||
| return null; | |||||
| } | |||||
| public async Task<Module> Load(object moduleInstance) | |||||
| //Modules | |||||
| public async Task<ModuleInfo> AddModule<T>(IDependencyMap dependencyMap = null) | |||||
| { | { | ||||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | await _moduleLock.WaitAsync().ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| if (_modules.ContainsKey(moduleInstance.GetType())) | |||||
| throw new ArgumentException($"This module has already been loaded."); | |||||
| if (_moduleDefs.ContainsKey(typeof(T))) | |||||
| throw new ArgumentException($"This module has already been added."); | |||||
| var typeInfo = moduleInstance.GetType().GetTypeInfo(); | |||||
| var moduleAttr = typeInfo.GetCustomAttribute<ModuleAttribute>(); | |||||
| if (moduleAttr == null) | |||||
| throw new ArgumentException($"Modules must be marked with ModuleAttribute."); | |||||
| var typeInfo = typeof(T).GetTypeInfo(); | |||||
| if (!_moduleTypeInfo.IsAssignableFrom(typeInfo)) | |||||
| throw new ArgumentException($"Modules must inherit ModuleBase."); | |||||
| return LoadInternal(moduleInstance, moduleAttr, typeInfo, null); | |||||
| return AddModuleInternal(typeInfo, dependencyMap); | |||||
| } | } | ||||
| finally | finally | ||||
| { | { | ||||
| _moduleLock.Release(); | _moduleLock.Release(); | ||||
| } | } | ||||
| } | } | ||||
| private Module LoadInternal(object moduleInstance, ModuleAttribute moduleAttr, TypeInfo typeInfo, IDependencyMap dependencyMap) | |||||
| { | |||||
| if (_modules.ContainsKey(moduleInstance.GetType())) | |||||
| return _modules[moduleInstance.GetType()]; | |||||
| var loadedModule = new Module(typeInfo, this, moduleInstance, moduleAttr, dependencyMap); | |||||
| _modules[moduleInstance.GetType()] = loadedModule; | |||||
| foreach (var cmd in loadedModule.Commands) | |||||
| _map.AddCommand(cmd); | |||||
| return loadedModule; | |||||
| } | |||||
| public async Task<IEnumerable<Module>> LoadAssembly(Assembly assembly, IDependencyMap dependencyMap = null) | |||||
| public async Task<IEnumerable<ModuleInfo>> AddModules(Assembly assembly, IDependencyMap dependencyMap = null) | |||||
| { | { | ||||
| var modules = ImmutableArray.CreateBuilder<Module>(); | |||||
| var moduleDefs = ImmutableArray.CreateBuilder<ModuleInfo>(); | |||||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | await _moduleLock.WaitAsync().ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| foreach (var type in assembly.ExportedTypes) | foreach (var type in assembly.ExportedTypes) | ||||
| { | { | ||||
| var typeInfo = type.GetTypeInfo(); | |||||
| var moduleAttr = typeInfo.GetCustomAttribute<ModuleAttribute>(); | |||||
| if (moduleAttr != null && moduleAttr.AutoLoad) | |||||
| if (!_moduleDefs.ContainsKey(type)) | |||||
| { | { | ||||
| var moduleInstance = ReflectionUtils.CreateObject(typeInfo, this, dependencyMap); | |||||
| modules.Add(LoadInternal(moduleInstance, moduleAttr, typeInfo, dependencyMap)); | |||||
| var typeInfo = type.GetTypeInfo(); | |||||
| if (_moduleTypeInfo.IsAssignableFrom(typeInfo)) | |||||
| { | |||||
| var dontAutoLoad = typeInfo.GetCustomAttribute<DontAutoLoadAttribute>(); | |||||
| if (dontAutoLoad == null) | |||||
| moduleDefs.Add(AddModuleInternal(typeInfo, dependencyMap)); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| return modules.ToImmutable(); | |||||
| return moduleDefs.ToImmutable(); | |||||
| } | } | ||||
| finally | finally | ||||
| { | { | ||||
| _moduleLock.Release(); | _moduleLock.Release(); | ||||
| } | } | ||||
| } | } | ||||
| private ModuleInfo AddModuleInternal(TypeInfo typeInfo, IDependencyMap dependencyMap) | |||||
| { | |||||
| var moduleDef = new ModuleInfo(typeInfo, this, dependencyMap); | |||||
| _moduleDefs[typeInfo.BaseType] = moduleDef; | |||||
| foreach (var cmd in moduleDef.Commands) | |||||
| _map.AddCommand(cmd); | |||||
| return moduleDef; | |||||
| } | |||||
| public async Task<bool> Unload(Module module) | |||||
| public async Task<bool> RemoveModule(ModuleInfo module) | |||||
| { | { | ||||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | await _moduleLock.WaitAsync().ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| return UnloadInternal(module.Instance); | |||||
| return RemoveModuleInternal(module.Source.BaseType); | |||||
| } | } | ||||
| finally | finally | ||||
| { | { | ||||
| _moduleLock.Release(); | _moduleLock.Release(); | ||||
| } | } | ||||
| } | } | ||||
| public async Task<bool> Unload(object moduleInstance) | |||||
| public async Task<bool> RemoveModule<T>() | |||||
| { | { | ||||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | await _moduleLock.WaitAsync().ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| return UnloadInternal(moduleInstance); | |||||
| return RemoveModuleInternal(typeof(T)); | |||||
| } | } | ||||
| finally | finally | ||||
| { | { | ||||
| _moduleLock.Release(); | _moduleLock.Release(); | ||||
| } | } | ||||
| } | } | ||||
| private bool UnloadInternal(object module) | |||||
| private bool RemoveModuleInternal(Type type) | |||||
| { | { | ||||
| Module unloadedModule; | |||||
| if (_modules.TryRemove(module.GetType(), out unloadedModule)) | |||||
| ModuleInfo unloadedModule; | |||||
| if (_moduleDefs.TryRemove(type, out unloadedModule)) | |||||
| { | { | ||||
| foreach (var cmd in unloadedModule.Commands) | foreach (var cmd in unloadedModule.Commands) | ||||
| _map.RemoveCommand(cmd); | _map.RemoveCommand(cmd); | ||||
| @@ -178,6 +158,24 @@ namespace Discord.Commands | |||||
| return false; | return false; | ||||
| } | } | ||||
| //Type Readers | |||||
| public void AddTypeReader<T>(TypeReader reader) | |||||
| { | |||||
| _typeReaders[typeof(T)] = reader; | |||||
| } | |||||
| public void AddTypeReader(Type type, TypeReader reader) | |||||
| { | |||||
| _typeReaders[type] = reader; | |||||
| } | |||||
| internal TypeReader GetTypeReader(Type type) | |||||
| { | |||||
| TypeReader reader; | |||||
| if (_typeReaders.TryGetValue(type, out reader)) | |||||
| return reader; | |||||
| return null; | |||||
| } | |||||
| //Execution | |||||
| public SearchResult Search(CommandContext context, int argPos) => Search(context, context.Message.Content.Substring(argPos)); | public SearchResult Search(CommandContext context, int argPos) => Search(context, context.Message.Content.Substring(argPos)); | ||||
| public SearchResult Search(CommandContext context, string input) | public SearchResult Search(CommandContext context, string input) | ||||
| { | { | ||||
| @@ -16,7 +16,7 @@ namespace Discord.Commands | |||||
| _nodes = new ConcurrentDictionary<string, CommandMapNode>(); | _nodes = new ConcurrentDictionary<string, CommandMapNode>(); | ||||
| } | } | ||||
| public void AddCommand(Command command) | |||||
| public void AddCommand(CommandInfo command) | |||||
| { | { | ||||
| foreach (string text in command.Aliases) | foreach (string text in command.Aliases) | ||||
| { | { | ||||
| @@ -35,7 +35,7 @@ namespace Discord.Commands | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| public void RemoveCommand(Command command) | |||||
| public void RemoveCommand(CommandInfo command) | |||||
| { | { | ||||
| foreach (string text in command.Aliases) | foreach (string text in command.Aliases) | ||||
| { | { | ||||
| @@ -60,7 +60,7 @@ namespace Discord.Commands | |||||
| } | } | ||||
| } | } | ||||
| public IEnumerable<Command> GetCommands(string text) | |||||
| public IEnumerable<CommandInfo> GetCommands(string text) | |||||
| { | { | ||||
| int nextSpace = NextWhitespace(text); | int nextSpace = NextWhitespace(text); | ||||
| string name; | string name; | ||||
| @@ -76,7 +76,7 @@ namespace Discord.Commands | |||||
| if (_nodes.TryGetValue(name, out nextNode)) | if (_nodes.TryGetValue(name, out nextNode)) | ||||
| return nextNode.GetCommands(text, nextSpace + 1); | return nextNode.GetCommands(text, nextSpace + 1); | ||||
| else | else | ||||
| return Enumerable.Empty<Command>(); | |||||
| return Enumerable.Empty<CommandInfo>(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -9,7 +9,7 @@ namespace Discord.Commands | |||||
| private readonly ConcurrentDictionary<string, CommandMapNode> _nodes; | private readonly ConcurrentDictionary<string, CommandMapNode> _nodes; | ||||
| private readonly string _name; | private readonly string _name; | ||||
| private readonly object _lockObj = new object(); | private readonly object _lockObj = new object(); | ||||
| private ImmutableArray<Command> _commands; | |||||
| private ImmutableArray<CommandInfo> _commands; | |||||
| public bool IsEmpty => _commands.Length == 0 && _nodes.Count == 0; | public bool IsEmpty => _commands.Length == 0 && _nodes.Count == 0; | ||||
| @@ -17,10 +17,10 @@ namespace Discord.Commands | |||||
| { | { | ||||
| _name = name; | _name = name; | ||||
| _nodes = new ConcurrentDictionary<string, CommandMapNode>(); | _nodes = new ConcurrentDictionary<string, CommandMapNode>(); | ||||
| _commands = ImmutableArray.Create<Command>(); | |||||
| _commands = ImmutableArray.Create<CommandInfo>(); | |||||
| } | } | ||||
| public void AddCommand(string text, int index, Command command) | |||||
| public void AddCommand(string text, int index, CommandInfo command) | |||||
| { | { | ||||
| int nextSpace = text.IndexOf(' ', index); | int nextSpace = text.IndexOf(' ', index); | ||||
| string name; | string name; | ||||
| @@ -41,7 +41,7 @@ namespace Discord.Commands | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| public void RemoveCommand(string text, int index, Command command) | |||||
| public void RemoveCommand(string text, int index, CommandInfo command) | |||||
| { | { | ||||
| int nextSpace = text.IndexOf(' ', index); | int nextSpace = text.IndexOf(' ', index); | ||||
| string name; | string name; | ||||
| @@ -68,7 +68,7 @@ namespace Discord.Commands | |||||
| } | } | ||||
| } | } | ||||
| public IEnumerable<Command> GetCommands(string text, int index) | |||||
| public IEnumerable<CommandInfo> GetCommands(string text, int index) | |||||
| { | { | ||||
| int nextSpace = text.IndexOf(' ', index); | int nextSpace = text.IndexOf(' ', index); | ||||
| string name; | string name; | ||||
| @@ -0,0 +1,15 @@ | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| public abstract class ModuleBase | |||||
| { | |||||
| public IDiscordClient Client { get; internal set; } | |||||
| public CommandContext Context { get; internal set; } | |||||
| protected virtual async Task ReplyAsync(string message, bool isTTS = false, RequestOptions options = null) | |||||
| { | |||||
| await Context.Channel.SendMessageAsync(message, isTTS, options).ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,4 +1,5 @@ | |||||
| using System.Collections.Generic; | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Reflection; | using System.Reflection; | ||||
| @@ -6,26 +7,31 @@ using System.Reflection; | |||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| { | { | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class Module | |||||
| public class ModuleInfo | |||||
| { | { | ||||
| internal readonly Func<ModuleBase> _builder; | |||||
| public TypeInfo Source { get; } | public TypeInfo Source { get; } | ||||
| public CommandService Service { get; } | public CommandService Service { get; } | ||||
| public string Name { get; } | public string Name { get; } | ||||
| public string Prefix { get; } | public string Prefix { get; } | ||||
| public string Summary { get; } | public string Summary { get; } | ||||
| public string Remarks { get; } | public string Remarks { get; } | ||||
| public IEnumerable<Command> Commands { get; } | |||||
| internal object Instance { get; } | |||||
| public IEnumerable<CommandInfo> Commands { get; } | |||||
| public IReadOnlyList<PreconditionAttribute> Preconditions { get; } | public IReadOnlyList<PreconditionAttribute> Preconditions { get; } | ||||
| internal Module(TypeInfo source, CommandService service, object instance, ModuleAttribute moduleAttr, IDependencyMap dependencyMap) | |||||
| internal ModuleInfo(TypeInfo source, CommandService service, IDependencyMap dependencyMap) | |||||
| { | { | ||||
| Source = source; | Source = source; | ||||
| Service = service; | Service = service; | ||||
| Name = source.Name; | Name = source.Name; | ||||
| Prefix = moduleAttr.Prefix ?? ""; | |||||
| Instance = instance; | |||||
| _builder = ReflectionUtils.CreateBuilder<ModuleBase>(source, Service, dependencyMap); | |||||
| var groupAttr = source.GetCustomAttribute<GroupAttribute>(); | |||||
| if (groupAttr != null) | |||||
| Prefix = groupAttr.Prefix; | |||||
| else | |||||
| Prefix = ""; | |||||
| var nameAttr = source.GetCustomAttribute<NameAttribute>(); | var nameAttr = source.GetCustomAttribute<NameAttribute>(); | ||||
| if (nameAttr != null) | if (nameAttr != null) | ||||
| @@ -39,20 +45,19 @@ namespace Discord.Commands | |||||
| if (remarksAttr != null) | if (remarksAttr != null) | ||||
| Remarks = remarksAttr.Text; | Remarks = remarksAttr.Text; | ||||
| List<Command> commands = new List<Command>(); | |||||
| SearchClass(source, instance, commands, Prefix, dependencyMap); | |||||
| List<CommandInfo> commands = new List<CommandInfo>(); | |||||
| SearchClass(source, commands, Prefix, dependencyMap); | |||||
| Commands = commands; | Commands = commands; | ||||
| Preconditions = BuildPreconditions(); | |||||
| Preconditions = Source.GetCustomAttributes<PreconditionAttribute>().ToImmutableArray(); | |||||
| } | } | ||||
| private void SearchClass(TypeInfo parentType, object instance, List<Command> commands, string groupPrefix, IDependencyMap dependencyMap) | |||||
| private void SearchClass(TypeInfo parentType, List<CommandInfo> commands, string groupPrefix, IDependencyMap dependencyMap) | |||||
| { | { | ||||
| foreach (var method in parentType.DeclaredMethods) | foreach (var method in parentType.DeclaredMethods) | ||||
| { | { | ||||
| var cmdAttr = method.GetCustomAttribute<CommandAttribute>(); | var cmdAttr = method.GetCustomAttribute<CommandAttribute>(); | ||||
| if (cmdAttr != null) | if (cmdAttr != null) | ||||
| commands.Add(new Command(method, this, instance, cmdAttr, groupPrefix)); | |||||
| commands.Add(new CommandInfo(method, this, cmdAttr, groupPrefix)); | |||||
| } | } | ||||
| foreach (var type in parentType.DeclaredNestedTypes) | foreach (var type in parentType.DeclaredNestedTypes) | ||||
| { | { | ||||
| @@ -66,15 +71,13 @@ namespace Discord.Commands | |||||
| else | else | ||||
| nextGroupPrefix = groupAttrib.Prefix ?? type.Name.ToLowerInvariant(); | nextGroupPrefix = groupAttrib.Prefix ?? type.Name.ToLowerInvariant(); | ||||
| SearchClass(type, ReflectionUtils.CreateObject(type, Service, dependencyMap), commands, nextGroupPrefix, dependencyMap); | |||||
| SearchClass(type, commands, nextGroupPrefix, dependencyMap); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| private IReadOnlyList<PreconditionAttribute> BuildPreconditions() | |||||
| { | |||||
| return Source.GetCustomAttributes<PreconditionAttribute>().ToImmutableArray(); | |||||
| } | |||||
| internal ModuleBase CreateInstance() | |||||
| => _builder(); | |||||
| public override string ToString() => Name; | public override string ToString() => Name; | ||||
| private string DebuggerDisplay => Name; | private string DebuggerDisplay => Name; | ||||
| @@ -6,7 +6,10 @@ namespace Discord.Commands | |||||
| { | { | ||||
| internal class ReflectionUtils | internal class ReflectionUtils | ||||
| { | { | ||||
| internal static object CreateObject(TypeInfo typeInfo, CommandService service, IDependencyMap map = null) | |||||
| internal static T CreateObject<T>(TypeInfo typeInfo, CommandService service, IDependencyMap map = null) | |||||
| => CreateBuilder<T>(typeInfo, service, map)(); | |||||
| internal static Func<T> CreateBuilder<T>(TypeInfo typeInfo, CommandService service, IDependencyMap map = null) | |||||
| { | { | ||||
| var constructors = typeInfo.DeclaredConstructors.Where(x => !x.IsStatic).ToArray(); | var constructors = typeInfo.DeclaredConstructors.Where(x => !x.IsStatic).ToArray(); | ||||
| if (constructors.Length == 0) | if (constructors.Length == 0) | ||||
| @@ -14,7 +17,7 @@ namespace Discord.Commands | |||||
| else if (constructors.Length > 1) | else if (constructors.Length > 1) | ||||
| throw new InvalidOperationException($"Multiple constructors found for \"{typeInfo.FullName}\""); | throw new InvalidOperationException($"Multiple constructors found for \"{typeInfo.FullName}\""); | ||||
| var constructor = constructors[0]; | |||||
| var constructor = constructors[0]; | |||||
| ParameterInfo[] parameters = constructor.GetParameters(); | ParameterInfo[] parameters = constructor.GetParameters(); | ||||
| object[] args = new object[parameters.Length]; | object[] args = new object[parameters.Length]; | ||||
| @@ -34,14 +37,17 @@ namespace Discord.Commands | |||||
| args[i] = arg; | args[i] = arg; | ||||
| } | } | ||||
| try | |||||
| { | |||||
| return constructor.Invoke(args); | |||||
| } | |||||
| catch (Exception ex) | |||||
| return () => | |||||
| { | { | ||||
| throw new Exception($"Failed to create \"{typeInfo.FullName}\"", ex); | |||||
| } | |||||
| try | |||||
| { | |||||
| return (T)constructor.Invoke(args); | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| throw new Exception($"Failed to create \"{typeInfo.FullName}\"", ex); | |||||
| } | |||||
| }; | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -7,14 +7,14 @@ namespace Discord.Commands | |||||
| public struct SearchResult : IResult | public struct SearchResult : IResult | ||||
| { | { | ||||
| public string Text { get; } | public string Text { get; } | ||||
| public IReadOnlyList<Command> Commands { get; } | |||||
| public IReadOnlyList<CommandInfo> Commands { get; } | |||||
| public CommandError? Error { get; } | public CommandError? Error { get; } | ||||
| public string ErrorReason { get; } | public string ErrorReason { get; } | ||||
| public bool IsSuccess => !Error.HasValue; | public bool IsSuccess => !Error.HasValue; | ||||
| private SearchResult(string text, IReadOnlyList<Command> commands, CommandError? error, string errorReason) | |||||
| private SearchResult(string text, IReadOnlyList<CommandInfo> commands, CommandError? error, string errorReason) | |||||
| { | { | ||||
| Text = text; | Text = text; | ||||
| Commands = commands; | Commands = commands; | ||||
| @@ -22,7 +22,7 @@ namespace Discord.Commands | |||||
| ErrorReason = errorReason; | ErrorReason = errorReason; | ||||
| } | } | ||||
| public static SearchResult FromSuccess(string text, IReadOnlyList<Command> commands) | |||||
| public static SearchResult FromSuccess(string text, IReadOnlyList<CommandInfo> commands) | |||||
| => new SearchResult(text, commands, null, null); | => new SearchResult(text, commands, null, null); | ||||
| public static SearchResult FromError(CommandError error, string reason) | public static SearchResult FromError(CommandError error, string reason) | ||||
| => new SearchResult(null, null, error, reason); | => new SearchResult(null, null, error, reason); | ||||
| @@ -9,7 +9,7 @@ namespace Discord | |||||
| { | { | ||||
| //Based on https://github.com/dotnet/corefx/blob/d0dc5fc099946adc1035b34a8b1f6042eddb0c75/src/System.Threading.Tasks.Parallel/src/System/Threading/PlatformHelper.cs | //Based on https://github.com/dotnet/corefx/blob/d0dc5fc099946adc1035b34a8b1f6042eddb0c75/src/System.Threading.Tasks.Parallel/src/System/Threading/PlatformHelper.cs | ||||
| //Copyright (c) .NET Foundation and Contributors | //Copyright (c) .NET Foundation and Contributors | ||||
| public static class ConcurrentHashSet | |||||
| internal static class ConcurrentHashSet | |||||
| { | { | ||||
| private const int PROCESSOR_COUNT_REFRESH_INTERVAL_MS = 30000; | private const int PROCESSOR_COUNT_REFRESH_INTERVAL_MS = 30000; | ||||
| private static volatile int s_processorCount; | private static volatile int s_processorCount; | ||||