Reorganized commands structure
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
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.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 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);
|
||||
_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();
|
||||
}
|
||||
}
|
||||
|
||||
internal static object CreateObject(TypeInfo typeInfo)
|
||||
private bool UnloadInternal(object module)
|
||||
{
|
||||
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
|
||||
Module unloadedModule;
|
||||
if (_modules.TryRemove(module, out unloadedModule))
|
||||
{
|
||||
return constructor.Invoke(null);
|
||||
foreach (var cmd in unloadedModule.Commands)
|
||||
{
|
||||
List<Command> list;
|
||||
if (_map.TryGetValue(cmd.Text, out list))
|
||||
{
|
||||
lock (list)
|
||||
list.Remove(cmd);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
//TODO: C#7 Candidate for tuple
|
||||
public SearchResults Search(string input)
|
||||
{
|
||||
string lowerInput = input.ToLowerInvariant();
|
||||
|
||||
List<Command> bestGroup = null, group;
|
||||
int startPos = 0, endPos;
|
||||
|
||||
while (true)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to create \"{typeInfo.FullName}\"", ex);
|
||||
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 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
|
||||
{
|
||||
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;
|
||||
Reference in New Issue
Block a user