|
@@ -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 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 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); |
|
|
_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(); |
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |