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