@@ -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; | |||
} | |||
} | |||
@@ -0,0 +1,35 @@ | |||
using System; | |||
using System.Reflection; | |||
namespace Discord.Commands | |||
{ | |||
public class Command | |||
{ | |||
private Action<IMessage> _action; | |||
public string Name { get; } | |||
public string Description { get; } | |||
public string Text { get; } | |||
internal Command(CommandAttribute attribute, MethodInfo methodInfo) | |||
{ | |||
var description = methodInfo.GetCustomAttribute<DescriptionAttribute>(); | |||
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 | |||
} | |||
} | |||
} |
@@ -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<string, List<Command>> _map; | |||
public CommandMap() | |||
{ | |||
_map = new ConcurrentDictionary<string, List<Command>>(); | |||
} | |||
public void Add(string key, Command cmd) | |||
{ | |||
var list = _map.GetOrAdd(key, _ => new List<Command>()); | |||
lock (list) | |||
list.Add(cmd); | |||
} | |||
public void Remove(string key, Command cmd) | |||
{ | |||
List<Command> list; | |||
if (_map.TryGetValue(key, out list)) | |||
{ | |||
lock (list) | |||
list.Remove(cmd); | |||
} | |||
} | |||
public IReadOnlyList<Command> Get(string key) | |||
{ | |||
List<Command> list; | |||
if (_map.TryGetValue(key, out list)) | |||
{ | |||
lock (list) | |||
return list.ToImmutableArray(); | |||
} | |||
return ImmutableArray.Create<Command>(); | |||
} | |||
//TODO: C#7 Candidate for tuple | |||
public CommandSearchResults Search(string input) | |||
{ | |||
string lowerInput = input.ToLowerInvariant(); | |||
List<Command> 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<Command> cmds; | |||
if (bestGroup != null) | |||
{ | |||
lock (bestGroup) | |||
cmds = bestGroup.ToImmutableArray(); | |||
} | |||
else | |||
cmds = ImmutableArray.Create<Command>(); | |||
return new CommandSearchResults(cmds, startPos); | |||
} | |||
} | |||
} |
@@ -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<Command> Commands { get; } | |||
internal Module(object parent, TypeInfo typeInfo) | |||
{ | |||
List<Command> commands = new List<Command>(); | |||
SearchClass(parent, commands, typeInfo); | |||
Commands = commands; | |||
} | |||
private void SearchClass(object parent, List<Command> commands, TypeInfo typeInfo) | |||
{ | |||
foreach (var method in typeInfo.DeclaredMethods) | |||
{ | |||
if (typeInfo.GetCustomAttribute<CommandAttribute>() != null) | |||
{ | |||
} | |||
} | |||
foreach (var type in typeInfo.DeclaredNestedTypes) | |||
{ | |||
if (typeInfo.GetCustomAttribute<GroupAttribute>() != 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<object, Module> _modules; | |||
private readonly ConcurrentDictionary<object, Module> _modules; | |||
private readonly ConcurrentDictionary<string, List<Command>> _map; | |||
public IEnumerable<Module> Modules => _modules.Select(x => x.Value); | |||
public IEnumerable<Command> Commands => _modules.SelectMany(x => x.Value.Commands); | |||
public CommandParser() | |||
public CommandService() | |||
{ | |||
_modules = new Dictionary<object, Module>(); | |||
_moduleLock = new SemaphoreSlim(1, 1); | |||
_modules = new ConcurrentDictionary<object, Module>(); | |||
_map = new ConcurrentDictionary<string, List<Command>>(); | |||
} | |||
public async Task<Module> 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<ModuleAttribute>() == 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<Command>()); | |||
lock (list) | |||
list.Add(cmd); | |||
} | |||
return loadedModule; | |||
} | |||
public async Task<IEnumerable<Module>> LoadAssembly(Assembly assembly) | |||
@@ -90,7 +69,7 @@ namespace Discord.Commands | |||
var typeInfo = type.GetTypeInfo(); | |||
if (typeInfo.GetCustomAttribute<ModuleAttribute>() != 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<Command> 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<Command> 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<Command> cmds; | |||
if (bestGroup != null) | |||
{ | |||
throw new InvalidOperationException($"Failed to create \"{typeInfo.FullName}\"", ex); | |||
lock (bestGroup) | |||
cmds = bestGroup.ToImmutableArray(); | |||
} | |||
else | |||
cmds = ImmutableArray.Create<Command>(); | |||
return new SearchResults(cmds, startPos); | |||
} | |||
} | |||
} |
@@ -0,0 +1,33 @@ | |||
using System.Collections.Generic; | |||
using System.Reflection; | |||
namespace Discord.Commands | |||
{ | |||
public class Module | |||
{ | |||
public string Name { get; } | |||
public IEnumerable<Command> Commands { get; } | |||
internal Module(object parent, TypeInfo typeInfo) | |||
{ | |||
List<Command> commands = new List<Command>(); | |||
SearchClass(parent, commands, typeInfo); | |||
Commands = commands; | |||
} | |||
private void SearchClass(object parent, List<Command> commands, TypeInfo typeInfo) | |||
{ | |||
foreach (var method in typeInfo.DeclaredMethods) | |||
{ | |||
var cmdAttr = method.GetCustomAttribute<CommandAttribute>(); | |||
if (cmdAttr != null) | |||
commands.Add(new Command(cmdAttr, method)); | |||
} | |||
foreach (var type in typeInfo.DeclaredNestedTypes) | |||
{ | |||
if (type.GetCustomAttribute<GroupAttribute>() != null) | |||
SearchClass(ReflectionUtils.CreateObject(type), commands, type); | |||
} | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} | |||
} |
@@ -2,12 +2,12 @@ | |||
namespace Discord.Commands | |||
{ | |||
public struct CommandSearchResults | |||
public struct SearchResults | |||
{ | |||
IReadOnlyList<Command> Commands { get; } | |||
int ArgsPos { get; } | |||
public CommandSearchResults(IReadOnlyList<Command> commands, int argsPos) | |||
public SearchResults(IReadOnlyList<Command> commands, int argsPos) | |||
{ | |||
Commands = commands; | |||
ArgsPos = argsPos; |