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