@@ -5,10 +5,14 @@ namespace Discord.Commands | |||
[AttributeUsage(AttributeTargets.Class)] | |||
public class GroupAttribute : Attribute | |||
{ | |||
public string Name { get; } | |||
public GroupAttribute(string name) | |||
public string Prefix { get; } | |||
public GroupAttribute() | |||
{ | |||
Name = name; | |||
Prefix = null; | |||
} | |||
public GroupAttribute(string prefix) | |||
{ | |||
Prefix = prefix; | |||
} | |||
} | |||
} |
@@ -5,5 +5,14 @@ namespace Discord.Commands | |||
[AttributeUsage(AttributeTargets.Class)] | |||
public class ModuleAttribute : Attribute | |||
{ | |||
public string Prefix { get; } | |||
public ModuleAttribute() | |||
{ | |||
Prefix = null; | |||
} | |||
public ModuleAttribute(string prefix) | |||
{ | |||
Prefix = prefix; | |||
} | |||
} | |||
} |
@@ -19,13 +19,13 @@ namespace Discord.Commands | |||
public Module Module { get; } | |||
public IReadOnlyList<CommandParameter> Parameters { get; } | |||
internal Command(Module module, object instance, CommandAttribute attribute, MethodInfo methodInfo) | |||
internal Command(Module module, object instance, CommandAttribute attribute, MethodInfo methodInfo, string groupPrefix) | |||
{ | |||
Module = module; | |||
_instance = instance; | |||
Name = methodInfo.Name; | |||
Text = attribute.Text; | |||
Text = groupPrefix + attribute.Text; | |||
var description = methodInfo.GetCustomAttribute<DescriptionAttribute>(); | |||
if (description != null) | |||
@@ -40,7 +40,7 @@ namespace Discord.Commands | |||
if (!searchResult.IsSuccess) | |||
return ParseResult.FromError(searchResult); | |||
return await CommandParser.ParseArgs(this, msg, searchResult.ArgText, 0).ConfigureAwait(false); | |||
return await CommandParser.ParseArgs(this, msg, searchResult.Text.Substring(Text.Length), 0).ConfigureAwait(false); | |||
} | |||
public async Task<ExecuteResult> Execute(IMessage msg, ParseResult parseResult) | |||
{ | |||
@@ -18,6 +18,7 @@ namespace Discord.Commands | |||
public CommandParameter(string name, string description, TypeReader reader, bool isOptional, bool isUnparsed, object defaultValue) | |||
{ | |||
_reader = reader; | |||
Name = name; | |||
IsOptional = isOptional; | |||
IsUnparsed = isUnparsed; | |||
DefaultValue = defaultValue; | |||
@@ -66,7 +66,7 @@ namespace Discord.Commands | |||
else | |||
{ | |||
curParam = command.Parameters.Count > argList.Count ? command.Parameters[argList.Count] : null; | |||
if (curParam.IsUnparsed) | |||
if (curParam != null && curParam.IsUnparsed) | |||
{ | |||
argBuilder.Append(c); | |||
continue; | |||
@@ -15,7 +15,7 @@ namespace Discord.Commands | |||
private readonly SemaphoreSlim _moduleLock; | |||
private readonly ConcurrentDictionary<object, Module> _modules; | |||
private readonly ConcurrentDictionary<string, List<Command>> _map; | |||
private readonly Dictionary<Type, TypeReader> _typeReaders; | |||
private readonly ConcurrentDictionary<Type, TypeReader> _typeReaders; | |||
public IEnumerable<Module> Modules => _modules.Select(x => x.Value); | |||
public IEnumerable<Command> Commands => _modules.SelectMany(x => x.Value.Commands); | |||
@@ -25,7 +25,7 @@ namespace Discord.Commands | |||
_moduleLock = new SemaphoreSlim(1, 1); | |||
_modules = new ConcurrentDictionary<object, Module>(); | |||
_map = new ConcurrentDictionary<string, List<Command>>(); | |||
_typeReaders = new Dictionary<Type, TypeReader> | |||
_typeReaders = new ConcurrentDictionary<Type, TypeReader> | |||
{ | |||
[typeof(string)] = new GenericTypeReader((m, s) => Task.FromResult(TypeReaderResult.FromSuccess(s))), | |||
[typeof(byte)] = new GenericTypeReader((m, s) => | |||
@@ -143,19 +143,20 @@ namespace Discord.Commands | |||
throw new ArgumentException($"This module has already been loaded."); | |||
var typeInfo = moduleInstance.GetType().GetTypeInfo(); | |||
if (typeInfo.GetCustomAttribute<ModuleAttribute>() == null) | |||
var moduleAttr = typeInfo.GetCustomAttribute<ModuleAttribute>(); | |||
if (moduleAttr != null) | |||
throw new ArgumentException($"Modules must be marked with ModuleAttribute."); | |||
return LoadInternal(moduleInstance, typeInfo); | |||
return LoadInternal(moduleInstance, moduleAttr, typeInfo); | |||
} | |||
finally | |||
{ | |||
_moduleLock.Release(); | |||
} | |||
} | |||
private Module LoadInternal(object moduleInstance, TypeInfo typeInfo) | |||
private Module LoadInternal(object moduleInstance, ModuleAttribute moduleAttr, TypeInfo typeInfo) | |||
{ | |||
var loadedModule = new Module(this, moduleInstance, typeInfo); | |||
var loadedModule = new Module(this, moduleInstance, moduleAttr, typeInfo); | |||
_modules[moduleInstance] = loadedModule; | |||
foreach (var cmd in loadedModule.Commands) | |||
@@ -176,10 +177,11 @@ namespace Discord.Commands | |||
foreach (var type in assembly.ExportedTypes) | |||
{ | |||
var typeInfo = type.GetTypeInfo(); | |||
if (typeInfo.GetCustomAttribute<ModuleAttribute>() != null) | |||
var moduleAttr = typeInfo.GetCustomAttribute<ModuleAttribute>(); | |||
if (moduleAttr != null) | |||
{ | |||
var moduleInstance = ReflectionUtils.CreateObject(typeInfo); | |||
modules.Add(LoadInternal(moduleInstance, typeInfo)); | |||
modules.Add(LoadInternal(moduleInstance, moduleAttr, typeInfo)); | |||
} | |||
} | |||
return modules.ToImmutable(); | |||
@@ -239,30 +241,34 @@ namespace Discord.Commands | |||
{ | |||
string lowerInput = input.ToLowerInvariant(); | |||
List<Command> bestGroup = null, group; | |||
int startPos = 0, endPos; | |||
ImmutableArray<Command>.Builder matches = null; | |||
List<Command> group; | |||
int pos = -1; | |||
while (true) | |||
{ | |||
endPos = input.IndexOf(' ', startPos); | |||
string cmdText = endPos == -1 ? input.Substring(startPos) : input.Substring(startPos, endPos - startPos); | |||
pos = input.IndexOf(' ', pos + 1); | |||
string cmdText = pos == -1 ? input : input.Substring(0, pos); | |||
if (!_map.TryGetValue(cmdText, out group)) | |||
break; | |||
bestGroup = group; | |||
if (endPos == -1) | |||
lock (group) | |||
{ | |||
if (matches == null) | |||
matches = ImmutableArray.CreateBuilder<Command>(group.Count); | |||
for (int i = 0; i < group.Count; i++) | |||
matches.Add(group[i]); | |||
} | |||
if (pos == -1) | |||
{ | |||
startPos = input.Length; | |||
pos = input.Length; | |||
break; | |||
} | |||
else | |||
startPos = endPos + 1; | |||
} | |||
if (bestGroup != null) | |||
{ | |||
lock (bestGroup) | |||
return SearchResult.FromSuccess(bestGroup.ToImmutableArray(), input.Substring(startPos)); | |||
} | |||
if (matches != null) | |||
return SearchResult.FromSuccess(input, matches.ToImmutable()); | |||
else | |||
return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); | |||
} | |||
@@ -275,7 +281,7 @@ namespace Discord.Commands | |||
return searchResult; | |||
var commands = searchResult.Commands; | |||
for (int i = 0; i < commands.Count; i++) | |||
for (int i = commands.Count - 1; i >= 0; i++) | |||
{ | |||
var parseResult = await commands[i].Parse(message, searchResult); | |||
if (!parseResult.IsSuccess) | |||
@@ -15,7 +15,7 @@ | |||
public static bool HasStringPrefix(this IMessage msg, string str, ref int argPos) | |||
{ | |||
var text = msg.RawText; | |||
str = str + ' '; | |||
//str = str + ' '; | |||
if (text.StartsWith(str)) | |||
{ | |||
argPos = str.Length; | |||
@@ -12,29 +12,39 @@ namespace Discord.Commands | |||
public IEnumerable<Command> Commands { get; } | |||
internal object Instance { get; } | |||
internal Module(CommandService service, object instance, TypeInfo typeInfo) | |||
internal Module(CommandService service, object instance, ModuleAttribute moduleAttr, TypeInfo typeInfo) | |||
{ | |||
Service = service; | |||
Name = typeInfo.Name; | |||
Instance = instance; | |||
List<Command> commands = new List<Command>(); | |||
SearchClass(instance, commands, typeInfo); | |||
SearchClass(instance, commands, typeInfo, moduleAttr.Prefix ?? ""); | |||
Commands = commands; | |||
} | |||
private void SearchClass(object instance, List<Command> commands, TypeInfo typeInfo) | |||
private void SearchClass(object instance, List<Command> commands, TypeInfo typeInfo, string groupPrefix) | |||
{ | |||
if (groupPrefix != "") | |||
groupPrefix += " "; | |||
foreach (var method in typeInfo.DeclaredMethods) | |||
{ | |||
var cmdAttr = method.GetCustomAttribute<CommandAttribute>(); | |||
if (cmdAttr != null) | |||
commands.Add(new Command(this, instance, cmdAttr, method)); | |||
commands.Add(new Command(this, instance, cmdAttr, method, groupPrefix)); | |||
} | |||
foreach (var type in typeInfo.DeclaredNestedTypes) | |||
{ | |||
if (type.GetCustomAttribute<GroupAttribute>() != null) | |||
SearchClass(ReflectionUtils.CreateObject(type), commands, type); | |||
var groupAttrib = type.GetCustomAttribute<GroupAttribute>(); | |||
if (groupAttrib != null) | |||
{ | |||
string nextGroupPrefix; | |||
if (groupAttrib.Prefix != null) | |||
nextGroupPrefix = groupPrefix + groupAttrib.Prefix ?? type.Name; | |||
else | |||
nextGroupPrefix = groupPrefix; | |||
SearchClass(ReflectionUtils.CreateObject(type), commands, type, nextGroupPrefix); | |||
} | |||
} | |||
} | |||
@@ -6,24 +6,24 @@ namespace Discord.Commands | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
public struct SearchResult : IResult | |||
{ | |||
public string Text { get; } | |||
public IReadOnlyList<Command> Commands { get; } | |||
public string ArgText { get; } | |||
public CommandError? Error { get; } | |||
public string ErrorReason { get; } | |||
public bool IsSuccess => !Error.HasValue; | |||
private SearchResult(IReadOnlyList<Command> commands, string argText, CommandError? error, string errorReason) | |||
private SearchResult(string text, IReadOnlyList<Command> commands, CommandError? error, string errorReason) | |||
{ | |||
Text = text; | |||
Commands = commands; | |||
ArgText = argText; | |||
Error = error; | |||
ErrorReason = errorReason; | |||
} | |||
internal static SearchResult FromSuccess(IReadOnlyList<Command> commands, string argText) | |||
=> new SearchResult(commands, argText, null, null); | |||
internal static SearchResult FromSuccess(string text, IReadOnlyList<Command> commands) | |||
=> new SearchResult(text, commands, null, null); | |||
internal static SearchResult FromError(CommandError error, string reason) | |||
=> new SearchResult(null, null, error, reason); | |||