diff --git a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs index e28017915..db4e877d8 100644 --- a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs @@ -5,9 +5,13 @@ namespace Discord.Commands [AttributeUsage(AttributeTargets.Method)] public class CommandAttribute : Attribute { + public string Text { get; } public string Name { get; } - public CommandAttribute(string name) + + public CommandAttribute(string name) : this(name, name) { } + public CommandAttribute(string text, string name) { + Text = text.ToLowerInvariant(); Name = name; } } diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs new file mode 100644 index 000000000..568b645d9 --- /dev/null +++ b/src/Discord.Net.Commands/Command.cs @@ -0,0 +1,35 @@ +using System; +using System.Reflection; + +namespace Discord.Commands +{ + public class Command + { + private Action _action; + + public string Name { get; } + public string Description { get; } + public string Text { get; } + + internal Command(CommandAttribute attribute, MethodInfo methodInfo) + { + var description = methodInfo.GetCustomAttribute(); + if (description != null) + Description = description.Text; + + Name = attribute.Name; + Text = attribute.Text; + } + + public void Invoke(IMessage msg) + { + _action.Invoke(msg); + } + + private void BuildAction() + { + _action = null; + //TODO: Implement + } + } +} diff --git a/src/Discord.Net.Commands/CommandMap.cs b/src/Discord.Net.Commands/CommandMap.cs deleted file mode 100644 index f99073189..000000000 --- a/src/Discord.Net.Commands/CommandMap.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Immutable; - -namespace Discord.Commands -{ - public class CommandMap - { - private readonly ConcurrentDictionary> _map; - - public CommandMap() - { - _map = new ConcurrentDictionary>(); - } - - public void Add(string key, Command cmd) - { - var list = _map.GetOrAdd(key, _ => new List()); - lock (list) - list.Add(cmd); - } - public void Remove(string key, Command cmd) - { - List list; - if (_map.TryGetValue(key, out list)) - { - lock (list) - list.Remove(cmd); - } - } - public IReadOnlyList Get(string key) - { - List list; - if (_map.TryGetValue(key, out list)) - { - lock (list) - return list.ToImmutableArray(); - } - return ImmutableArray.Create(); - } - - //TODO: C#7 Candidate for tuple - public CommandSearchResults Search(string input) - { - string lowerInput = input.ToLowerInvariant(); - - List bestGroup = null, group; - int startPos = 0, endPos; - - while (true) - { - endPos = input.IndexOf(' ', startPos); - string cmdText = endPos == -1 ? input.Substring(startPos) : input.Substring(startPos, endPos - startPos); - startPos = endPos + 1; - if (!_map.TryGetValue(cmdText, out group)) - break; - bestGroup = group; - } - - ImmutableArray cmds; - if (bestGroup != null) - { - lock (bestGroup) - cmds = bestGroup.ToImmutableArray(); - } - else - cmds = ImmutableArray.Create(); - return new CommandSearchResults(cmds, startPos); - } - } -} diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandService.cs similarity index 51% rename from src/Discord.Net.Commands/CommandParser.cs rename to src/Discord.Net.Commands/CommandService.cs index 8157a3afd..2b9555b73 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Reflection; using System.Threading; @@ -7,55 +9,22 @@ using System.Threading.Tasks; namespace Discord.Commands { - public class Module - { - public string Name { get; } - public IEnumerable Commands { get; } - - internal Module(object parent, TypeInfo typeInfo) - { - List commands = new List(); - SearchClass(parent, commands, typeInfo); - Commands = commands; - } - - private void SearchClass(object parent, List commands, TypeInfo typeInfo) - { - foreach (var method in typeInfo.DeclaredMethods) - { - if (typeInfo.GetCustomAttribute() != null) - { - - } - } - foreach (var type in typeInfo.DeclaredNestedTypes) - { - if (typeInfo.GetCustomAttribute() != null) - { - SearchClass(CommandParser.CreateObject(typeInfo), commands, type); - } - } - } - } - public class Command - { - public string SourceName { get; } - - internal Command(TypeInfo typeInfo) - { - } - } - - public class CommandParser + public class CommandService { private readonly SemaphoreSlim _moduleLock; - private readonly Dictionary _modules; + private readonly ConcurrentDictionary _modules; + private readonly ConcurrentDictionary> _map; + + public IEnumerable Modules => _modules.Select(x => x.Value); + public IEnumerable Commands => _modules.SelectMany(x => x.Value.Commands); - public CommandParser() + public CommandService() { - _modules = new Dictionary(); _moduleLock = new SemaphoreSlim(1, 1); + _modules = new ConcurrentDictionary(); + _map = new ConcurrentDictionary>(); } + public async Task Load(object module) { await _moduleLock.WaitAsync().ConfigureAwait(false); @@ -63,9 +32,11 @@ namespace Discord.Commands { if (_modules.ContainsKey(module)) throw new ArgumentException($"This module has already been loaded."); + var typeInfo = module.GetType().GetTypeInfo(); if (typeInfo.GetCustomAttribute() == null) throw new ArgumentException($"Modules must be marked with ModuleAttribute."); + return LoadInternal(module, typeInfo); } finally @@ -77,6 +48,14 @@ namespace Discord.Commands { var loadedModule = new Module(module, typeInfo); _modules[module] = loadedModule; + + foreach (var cmd in loadedModule.Commands) + { + var list = _map.GetOrAdd(cmd.Text, _ => new List()); + lock (list) + list.Add(cmd); + } + return loadedModule; } public async Task> LoadAssembly(Assembly assembly) @@ -90,7 +69,7 @@ namespace Discord.Commands var typeInfo = type.GetTypeInfo(); if (typeInfo.GetCustomAttribute() != null) { - var module = CreateObject(typeInfo); + var module = ReflectionUtils.CreateObject(typeInfo); modules.Add(LoadInternal(module, typeInfo)); } } @@ -107,27 +86,60 @@ namespace Discord.Commands await _moduleLock.WaitAsync().ConfigureAwait(false); try { - return _modules.Remove(module); + return UnloadInternal(module); } finally { _moduleLock.Release(); } } + private bool UnloadInternal(object module) + { + Module unloadedModule; + if (_modules.TryRemove(module, out unloadedModule)) + { + foreach (var cmd in unloadedModule.Commands) + { + List list; + if (_map.TryGetValue(cmd.Text, out list)) + { + lock (list) + list.Remove(cmd); + } + } + return true; + } + else + return false; + } - internal static object CreateObject(TypeInfo typeInfo) + //TODO: C#7 Candidate for tuple + public SearchResults Search(string input) { - var constructor = typeInfo.DeclaredConstructors.Where(x => x.GetParameters().Length == 0).FirstOrDefault(); - if (constructor == null) - throw new InvalidOperationException($"Failed to find a valid constructor for \"{typeInfo.FullName}\""); - try + string lowerInput = input.ToLowerInvariant(); + + List bestGroup = null, group; + int startPos = 0, endPos; + + while (true) { - return constructor.Invoke(null); + endPos = input.IndexOf(' ', startPos); + string cmdText = endPos == -1 ? input.Substring(startPos) : input.Substring(startPos, endPos - startPos); + startPos = endPos + 1; + if (!_map.TryGetValue(cmdText, out group)) + break; + bestGroup = group; } - catch (Exception ex) + + ImmutableArray cmds; + if (bestGroup != null) { - throw new InvalidOperationException($"Failed to create \"{typeInfo.FullName}\"", ex); + lock (bestGroup) + cmds = bestGroup.ToImmutableArray(); } + else + cmds = ImmutableArray.Create(); + return new SearchResults(cmds, startPos); } } } diff --git a/src/Discord.Net.Commands/Module.cs b/src/Discord.Net.Commands/Module.cs new file mode 100644 index 000000000..230b3abd7 --- /dev/null +++ b/src/Discord.Net.Commands/Module.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Reflection; + +namespace Discord.Commands +{ + public class Module + { + public string Name { get; } + public IEnumerable Commands { get; } + + internal Module(object parent, TypeInfo typeInfo) + { + List commands = new List(); + SearchClass(parent, commands, typeInfo); + Commands = commands; + } + + private void SearchClass(object parent, List commands, TypeInfo typeInfo) + { + foreach (var method in typeInfo.DeclaredMethods) + { + var cmdAttr = method.GetCustomAttribute(); + if (cmdAttr != null) + commands.Add(new Command(cmdAttr, method)); + } + foreach (var type in typeInfo.DeclaredNestedTypes) + { + if (type.GetCustomAttribute() != null) + SearchClass(ReflectionUtils.CreateObject(type), commands, type); + } + } + } +} diff --git a/src/Discord.Net.Commands/ReflectionUtils.cs b/src/Discord.Net.Commands/ReflectionUtils.cs new file mode 100644 index 000000000..28672a06f --- /dev/null +++ b/src/Discord.Net.Commands/ReflectionUtils.cs @@ -0,0 +1,24 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace Discord.Commands +{ + internal class ReflectionUtils + { + internal static object CreateObject(TypeInfo typeInfo) + { + var constructor = typeInfo.DeclaredConstructors.Where(x => x.GetParameters().Length == 0).FirstOrDefault(); + if (constructor == null) + throw new InvalidOperationException($"Failed to find a valid constructor for \"{typeInfo.FullName}\""); + try + { + return constructor.Invoke(null); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to create \"{typeInfo.FullName}\"", ex); + } + } + } +} diff --git a/src/Discord.Net.Commands/CommandSearchResults.cs b/src/Discord.Net.Commands/SearchResults.cs similarity index 66% rename from src/Discord.Net.Commands/CommandSearchResults.cs rename to src/Discord.Net.Commands/SearchResults.cs index 4e1cbe025..724b61ecc 100644 --- a/src/Discord.Net.Commands/CommandSearchResults.cs +++ b/src/Discord.Net.Commands/SearchResults.cs @@ -2,12 +2,12 @@ namespace Discord.Commands { - public struct CommandSearchResults + public struct SearchResults { IReadOnlyList Commands { get; } int ArgsPos { get; } - public CommandSearchResults(IReadOnlyList commands, int argsPos) + public SearchResults(IReadOnlyList commands, int argsPos) { Commands = commands; ArgsPos = argsPos;