| @@ -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); | ||||