Reorganized commands structure
This commit is contained in:
@@ -5,9 +5,13 @@ namespace Discord.Commands
|
|||||||
[AttributeUsage(AttributeTargets.Method)]
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
public class CommandAttribute : Attribute
|
public class CommandAttribute : Attribute
|
||||||
{
|
{
|
||||||
|
public string Text { get; }
|
||||||
public string Name { 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;
|
Name = name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/Discord.Net.Commands/Command.cs
Normal file
35
src/Discord.Net.Commands/Command.cs
Normal file
@@ -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;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -7,55 +9,22 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace Discord.Commands
|
namespace Discord.Commands
|
||||||
{
|
{
|
||||||
public class Module
|
public class CommandService
|
||||||
{
|
|
||||||
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
|
|
||||||
{
|
{
|
||||||
private readonly SemaphoreSlim _moduleLock;
|
private readonly SemaphoreSlim _moduleLock;
|
||||||
private readonly Dictionary<object, Module> _modules;
|
private readonly ConcurrentDictionary<object, Module> _modules;
|
||||||
|
private readonly ConcurrentDictionary<string, List<Command>> _map;
|
||||||
|
|
||||||
public CommandParser()
|
public IEnumerable<Module> Modules => _modules.Select(x => x.Value);
|
||||||
|
public IEnumerable<Command> Commands => _modules.SelectMany(x => x.Value.Commands);
|
||||||
|
|
||||||
|
public CommandService()
|
||||||
{
|
{
|
||||||
_modules = new Dictionary<object, Module>();
|
|
||||||
_moduleLock = new SemaphoreSlim(1, 1);
|
_moduleLock = new SemaphoreSlim(1, 1);
|
||||||
|
_modules = new ConcurrentDictionary<object, Module>();
|
||||||
|
_map = new ConcurrentDictionary<string, List<Command>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Module> Load(object module)
|
public async Task<Module> Load(object module)
|
||||||
{
|
{
|
||||||
await _moduleLock.WaitAsync().ConfigureAwait(false);
|
await _moduleLock.WaitAsync().ConfigureAwait(false);
|
||||||
@@ -63,9 +32,11 @@ namespace Discord.Commands
|
|||||||
{
|
{
|
||||||
if (_modules.ContainsKey(module))
|
if (_modules.ContainsKey(module))
|
||||||
throw new ArgumentException($"This module has already been loaded.");
|
throw new ArgumentException($"This module has already been loaded.");
|
||||||
|
|
||||||
var typeInfo = module.GetType().GetTypeInfo();
|
var typeInfo = module.GetType().GetTypeInfo();
|
||||||
if (typeInfo.GetCustomAttribute<ModuleAttribute>() == null)
|
if (typeInfo.GetCustomAttribute<ModuleAttribute>() == null)
|
||||||
throw new ArgumentException($"Modules must be marked with ModuleAttribute.");
|
throw new ArgumentException($"Modules must be marked with ModuleAttribute.");
|
||||||
|
|
||||||
return LoadInternal(module, typeInfo);
|
return LoadInternal(module, typeInfo);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -77,6 +48,14 @@ namespace Discord.Commands
|
|||||||
{
|
{
|
||||||
var loadedModule = new Module(module, typeInfo);
|
var loadedModule = new Module(module, typeInfo);
|
||||||
_modules[module] = loadedModule;
|
_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;
|
return loadedModule;
|
||||||
}
|
}
|
||||||
public async Task<IEnumerable<Module>> LoadAssembly(Assembly assembly)
|
public async Task<IEnumerable<Module>> LoadAssembly(Assembly assembly)
|
||||||
@@ -90,7 +69,7 @@ namespace Discord.Commands
|
|||||||
var typeInfo = type.GetTypeInfo();
|
var typeInfo = type.GetTypeInfo();
|
||||||
if (typeInfo.GetCustomAttribute<ModuleAttribute>() != null)
|
if (typeInfo.GetCustomAttribute<ModuleAttribute>() != null)
|
||||||
{
|
{
|
||||||
var module = CreateObject(typeInfo);
|
var module = ReflectionUtils.CreateObject(typeInfo);
|
||||||
modules.Add(LoadInternal(module, typeInfo));
|
modules.Add(LoadInternal(module, typeInfo));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,27 +86,60 @@ namespace Discord.Commands
|
|||||||
await _moduleLock.WaitAsync().ConfigureAwait(false);
|
await _moduleLock.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return _modules.Remove(module);
|
return UnloadInternal(module);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_moduleLock.Release();
|
_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();
|
string lowerInput = input.ToLowerInvariant();
|
||||||
if (constructor == null)
|
|
||||||
throw new InvalidOperationException($"Failed to find a valid constructor for \"{typeInfo.FullName}\"");
|
List<Command> bestGroup = null, group;
|
||||||
try
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
33
src/Discord.Net.Commands/Module.cs
Normal file
33
src/Discord.Net.Commands/Module.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Discord.Net.Commands/ReflectionUtils.cs
Normal file
24
src/Discord.Net.Commands/ReflectionUtils.cs
Normal file
@@ -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
|
namespace Discord.Commands
|
||||||
{
|
{
|
||||||
public struct CommandSearchResults
|
public struct SearchResults
|
||||||
{
|
{
|
||||||
IReadOnlyList<Command> Commands { get; }
|
IReadOnlyList<Command> Commands { get; }
|
||||||
int ArgsPos { get; }
|
int ArgsPos { get; }
|
||||||
|
|
||||||
public CommandSearchResults(IReadOnlyList<Command> commands, int argsPos)
|
public SearchResults(IReadOnlyList<Command> commands, int argsPos)
|
||||||
{
|
{
|
||||||
Commands = commands;
|
Commands = commands;
|
||||||
ArgsPos = argsPos;
|
ArgsPos = argsPos;
|
||||||
Reference in New Issue
Block a user