From 44fb33b511069dca485b81ed4e2fe9e40495d4c8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 29 Oct 2015 23:33:05 -0300 Subject: [PATCH] Cleaned up CommandsPlugin, added CommandMap, new parameter declaration and aliases. --- .../Discord.Net.Commands.csproj | 3 + src/Discord.Net.Commands/Command.cs | 89 ++++++++++- src/Discord.Net.Commands/CommandBuilder.cs | 130 ++++++++-------- src/Discord.Net.Commands/CommandMap.cs | 84 ++++++++++ src/Discord.Net.Commands/CommandParser.cs | 80 ++++++---- .../CommandsPlugin.Events.cs | 2 +- src/Discord.Net.Commands/CommandsPlugin.cs | 145 ++++++++---------- 7 files changed, 348 insertions(+), 185 deletions(-) create mode 100644 src/Discord.Net.Commands/CommandMap.cs diff --git a/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj b/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj index 69f042ac3..1de1757f1 100644 --- a/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj +++ b/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj @@ -43,6 +43,9 @@ CommandBuilder.cs + + CommandMap.cs + CommandParser.cs diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs index 2f4978566..50f958ba2 100644 --- a/src/Discord.Net.Commands/Command.cs +++ b/src/Discord.Net.Commands/Command.cs @@ -1,23 +1,102 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace Discord.Commands { + public sealed class CommandParameter + { + public string Name { get; } + public bool IsOptional { get; } + public bool IsCatchAll { get; } + + public CommandParameter(string name, bool isOptional, bool isCatchAll) + { + Name = name; + IsOptional = isOptional; + IsCatchAll = isCatchAll; + } + } + public sealed class Command { public string Text { get; } - public int? MinArgs { get; internal set; } - public int? MaxArgs { get; internal set; } + public int? MinArgs { get; private set; } + public int? MaxArgs { get; private set; } public int MinPerms { get; internal set; } public bool IsHidden { get; internal set; } public string Description { get; internal set; } - internal Func Handler; + + public IEnumerable Aliases => _aliases; + private string[] _aliases; + + public IEnumerable Parameters => _parameters; + private CommandParameter[] _parameters; + + private Func _handler; internal Command(string text) { Text = text; - IsHidden = false; // Set false by default to avoid null error - Description = "No description set for this command."; + IsHidden = false; + _aliases = new string[0]; + _parameters = new CommandParameter[0]; + } + + internal void SetAliases(string[] aliases) + { + _aliases = aliases; + } + + internal void SetParameters(CommandParameter[] parameters) + { + _parameters = parameters; + if (parameters != null) + { + if (parameters.Length == 0) + { + MinArgs = 0; + MaxArgs = 0; + } + else + { + if (parameters[parameters.Length - 1].IsCatchAll) + MaxArgs = null; + else + MaxArgs = parameters.Length; + + int? optionalStart = null; + for (int i = parameters.Length - 1; i >= 0; i--) + { + if (parameters[i].IsOptional) + optionalStart = i; + else + break; + } + if (optionalStart == null) + MinArgs = MaxArgs; + else + MinArgs = optionalStart.Value; + } + } + } + + internal void SetHandler(Func func) + { + _handler = func; + } + internal void SetHandler(Action func) + { + _handler = e => { func(e); return TaskHelper.CompletedTask; }; + } + + internal Task Run(CommandEventArgs args) + { + var task = _handler(args); + if (task != null) + return task; + else + return TaskHelper.CompletedTask; } } } diff --git a/src/Discord.Net.Commands/CommandBuilder.cs b/src/Discord.Net.Commands/CommandBuilder.cs index 13e3ff89f..981bc23df 100644 --- a/src/Discord.Net.Commands/CommandBuilder.cs +++ b/src/Discord.Net.Commands/CommandBuilder.cs @@ -1,50 +1,55 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace Discord.Commands { public sealed class CommandBuilder { + private readonly CommandsPlugin _plugin; private readonly Command _command; - public CommandBuilder(Command command) + private List _params; + private bool _hasOptional, _hasCatchAll; + private string _prefix; + + public CommandBuilder(CommandsPlugin plugin, Command command, string prefix) { + _plugin = plugin; _command = command; - } - - public CommandBuilder ArgsEqual(int argCount) - { - _command.MinArgs = argCount; - _command.MaxArgs = argCount; - return this; - } - public CommandBuilder ArgsAtLeast(int minArgCount) - { - _command.MinArgs = minArgCount; - _command.MaxArgs = null; - return this; - } - public CommandBuilder ArgsAtMost(int maxArgCount) + _params = new List(); + _prefix = prefix; + } + + public CommandBuilder Alias(params string[] aliases) { - _command.MinArgs = null; - _command.MaxArgs = maxArgCount; + aliases = aliases.Select(x => AppendPrefix(_prefix, x)).ToArray(); + _command.SetAliases(aliases); return this; } - public CommandBuilder ArgsBetween(int minArgCount, int maxArgCount) + public CommandBuilder Info(string description) { - _command.MinArgs = minArgCount; - _command.MaxArgs = maxArgCount; + _command.Description = description; return this; } - public CommandBuilder NoArgs() + public CommandBuilder Parameter(string name, bool isOptional = false, bool isCatchAll = false) { - _command.MinArgs = 0; - _command.MaxArgs = 0; + if (_hasCatchAll) + throw new Exception("No parameters may be added after the catch-all"); + if (_hasOptional && isOptional) + throw new Exception("Non-optional parameters may not be added after an optional one"); + + _params.Add(new CommandParameter(name, isOptional, isCatchAll)); + + if (isOptional) + _hasOptional = true; + if (isCatchAll) + _hasCatchAll = true; return this; } - public CommandBuilder AnyArgs() + public CommandBuilder IsHidden() { - _command.MinArgs = null; - _command.MaxArgs = null; + _command.IsHidden = true; return this; } @@ -54,32 +59,45 @@ namespace Discord.Commands return this; } - public CommandBuilder Desc(string desc) - { - _command.Description = desc; - return this; - } - - public CommandBuilder IsHidden() - { - _command.IsHidden = true; - return this; - } - - public CommandBuilder Do(Func func) + public void Do(Func func) { - _command.Handler = func; - return this; + _command.SetHandler(func); + Build(); } - public CommandBuilder Do(Action func) + public void Do(Action func) { - _command.Handler = e => { func(e); return TaskHelper.CompletedTask; }; - return this; + _command.SetHandler(func); + Build(); + } + private void Build() + { + _command.SetParameters(_params.ToArray()); + foreach (var alias in _command.Aliases) + _plugin.Map.AddCommand(alias, _command); + _plugin.AddCommand(_command); + } + + internal static string AppendPrefix(string prefix, string cmd) + { + if (cmd != "") + { + if (prefix != "") + return prefix + ' ' + cmd; + else + return cmd; + } + else + { + if (prefix != "") + return prefix; + else + throw new ArgumentOutOfRangeException(nameof(cmd)); + } } } public sealed class CommandGroupBuilder { - private readonly CommandsPlugin _plugin; + internal readonly CommandsPlugin _plugin; private readonly string _prefix; private int _defaultMinPermissions; @@ -104,25 +122,9 @@ namespace Discord.Commands => CreateCommand(""); public CommandBuilder CreateCommand(string cmd) { - string text; - if (cmd != "") - { - if (_prefix != "") - text = _prefix + ' ' + cmd; - else - text = cmd; - } - else - { - if (_prefix != "") - text = _prefix; - else - throw new ArgumentOutOfRangeException(nameof(cmd)); - } - var command = new Command(text); + var command = new Command(CommandBuilder.AppendPrefix(_prefix, cmd)); command.MinPerms = _defaultMinPermissions; - _plugin.AddCommand(command); - return new CommandBuilder(command); + return new CommandBuilder(_plugin, command, _prefix); } } } diff --git a/src/Discord.Net.Commands/CommandMap.cs b/src/Discord.Net.Commands/CommandMap.cs new file mode 100644 index 000000000..504479400 --- /dev/null +++ b/src/Discord.Net.Commands/CommandMap.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; + +namespace Discord.Commands +{ + internal class CommandMap + { + private CommandMap _parent; + private Command _command; + private readonly Dictionary _subCommands; + + public CommandMap(CommandMap parent) + { + _parent = parent; + _subCommands = new Dictionary(); + } + + public CommandMap GetMap(string text) + { + CommandMap map; + if (_subCommands.TryGetValue(text, out map)) + return map; + else + return null; + } + + public Command GetCommand() + { + if (_command != null) + return _command; + else if (_parent != null) + return _parent.GetCommand(); + else + return null; + } + public Command GetCommand(string text) + { + return GetCommand(0, text.Split(' ')); + } + public Command GetCommand(int index, string[] parts) + { + if (index != parts.Length) + { + string nextPart = parts[index]; + CommandMap nextGroup; + if (_subCommands.TryGetValue(nextPart, out nextGroup)) + { + var cmd = nextGroup.GetCommand(index + 1, parts); + if (cmd != null) + return cmd; + } + } + + if (_command != null) + return _command; + return null; + } + + public void AddCommand(string text, Command command) + { + AddCommand(0, text.Split(' '), command); + } + public void AddCommand(int index, string[] parts, Command command) + { + if (index != parts.Length) + { + string nextPart = parts[index]; + CommandMap nextGroup; + if (!_subCommands.TryGetValue(nextPart, out nextGroup)) + { + nextGroup = new CommandMap(this); + _subCommands.Add(nextPart, nextGroup); + } + nextGroup.AddCommand(index + 1, parts, command); + } + else + { + if (_command != null) + throw new InvalidOperationException("A command has already been added with this path."); + _command = command; + } + } + } +} diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index 76a4b4006..08241eb1b 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -15,37 +15,69 @@ namespace Discord.Commands } //TODO: Check support for escaping - public static class CommandParser + internal static class CommandParser { private enum CommandParserPart { None, - CommandName, Parameter, QuotedParameter, DoubleQuotedParameter } - public static bool Parse(string input, out string command, out CommandPart[] args) + public static bool ParseCommand(string input, CommandMap map, out Command command, out int endPos) { - return Parse(input, out command, out args, true); - } - public static bool ParseArgs(string input, out CommandPart[] args) - { - string ignored; - return Parse(input, out ignored, out args, false); + int startPosition = 0; + int endPosition = 0; + int inputLength = input.Length; + bool isEscaped = false; + command = null; + endPos = 0; + + if (input == "") + return false; + + while (endPosition < inputLength) + { + char currentChar = input[endPosition++]; + if (isEscaped) + isEscaped = false; + else if (currentChar == '\\') + isEscaped = true; + + if ((!isEscaped && currentChar == ' ') || endPosition >= inputLength) + { + int length = (currentChar == ' ' ? endPosition - 1 : endPosition) - startPosition; + string temp = input.Substring(startPosition, length); + if (temp == "") + startPosition = endPosition; + else + { + var newMap = map.GetMap(temp); + if (newMap != null) + { + map = newMap; + endPos = endPosition; + } + else + break; + startPosition = endPosition; + } + } + } + command = map.GetCommand(); //Work our way backwards to find a command that matches our input + return command != null; } - private static bool Parse(string input, out string command, out CommandPart[] args, bool parseCommand) + public static bool ParseArgs(string input, int startPos, Command command, out CommandPart[] args) { - CommandParserPart currentPart = parseCommand ? CommandParserPart.CommandName : CommandParserPart.None; - int startPosition = 0; - int endPosition = 0; + CommandParserPart currentPart = CommandParserPart.None; + int startPosition = startPos; + int endPosition = startPos; int inputLength = input.Length; bool isEscaped = false; List argList = new List(); - - command = null; + args = null; if (input == "") @@ -61,21 +93,6 @@ namespace Discord.Commands switch (currentPart) { - case CommandParserPart.CommandName: - if ((!isEscaped && currentChar == ' ') || endPosition >= inputLength) - { - int length = (currentChar == ' ' ? endPosition - 1 : endPosition) - startPosition; - string temp = input.Substring(startPosition, length); - if (temp == "") - startPosition = endPosition; - else - { - currentPart = CommandParserPart.None; - command = temp; - startPosition = endPosition; - } - } - break; case CommandParserPart.None: if ((!isEscaped && currentChar == '\"')) { @@ -126,9 +143,6 @@ namespace Discord.Commands } } - if (parseCommand && (command == null || command == "")) - return false; - args = argList.ToArray(); return true; } diff --git a/src/Discord.Net.Commands/CommandsPlugin.Events.cs b/src/Discord.Net.Commands/CommandsPlugin.Events.cs index 0b3f4ee14..da31fa6f7 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.Events.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.Events.cs @@ -22,7 +22,7 @@ namespace Discord.Commands } } - public enum CommandErrorType { Exception, UnknownCommand, BadPermissions, BadArgCount } + public enum CommandErrorType { Exception, UnknownCommand, BadPermissions, BadArgCount, InvalidInput } public class CommandErrorEventArgs : CommandEventArgs { public CommandErrorType ErrorType { get; } diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs index 7f7149b95..f15664348 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.cs @@ -10,29 +10,30 @@ namespace Discord.Commands public partial class CommandsPlugin { private readonly DiscordClient _client; - private Func _getPermissions; - - private Dictionary _commands; + private readonly Func _getPermissions; - public Dictionary Commands => _commands; + public IEnumerable Commands => _commands; + private readonly List _commands; + + internal CommandMap Map => _map; + private readonly CommandMap _map; - public char CommandChar { get { return CommandChars[0]; } set { CommandChars = new List { value }; } } // This could possibly be removed entirely. Not sure. - public List CommandChars { get; set; } - public bool UseCommandChar { get; set; } + public IEnumerable CommandChars { get { return _commandChars; } set { _commandChars = value.ToArray(); } } + private char[] _commandChars; + public bool RequireCommandCharInPublic { get; set; } public bool RequireCommandCharInPrivate { get; set; } public bool HelpInPublic { get; set; } public CommandsPlugin(DiscordClient client, Func getPermissions = null, bool builtInHelp = false) { - _client = client; // Wait why is this even set + _client = client; _getPermissions = getPermissions; - _commands = new Dictionary(); + _commands = new List(); + _map = new CommandMap(null); - CommandChar = '!'; // Kept around to keep from possibly throwing an error. Might not be necessary. - CommandChars = new List { '!', '?', '/' }; - UseCommandChar = true; + _commandChars = new char[] { '!' }; RequireCommandCharInPublic = true; RequireCommandCharInPrivate = true; HelpInPublic = true; @@ -40,9 +41,9 @@ namespace Discord.Commands if (builtInHelp) { CreateCommand("help") - .ArgsBetween(0, 1) + .Parameter("command", isOptional: true) .IsHidden() - .Desc("Returns information about commands.") + .Info("Returns information about commands.") .Do(async e => { if (e.Command.Text != "help") @@ -50,29 +51,18 @@ namespace Discord.Commands else { if (e.Args == null) - { - StringBuilder output = new StringBuilder(); - bool first = true; + { + int permissions = getPermissions(e.User); + StringBuilder output = new StringBuilder(); output.AppendLine("These are the commands you can use:"); output.Append("`"); - int permissions = getPermissions(e.User); - foreach (KeyValuePair k in _commands) - { - if (permissions >= k.Value.MinPerms && !k.Value.IsHidden) - if (first) - { - output.Append(k.Key); - first = false; - } - else - output.Append($", {k.Key}"); - } + output.Append(string.Join(", ", _commands.Select(x => permissions >= x.MinPerms && !x.IsHidden))); output.Append("`"); - if (CommandChars.Count == 1) - output.AppendLine($"{Environment.NewLine}You can use `{CommandChars[0]}` to call a command."); + if (_commandChars.Length == 1) + output.AppendLine($"\nYou can use `{_commandChars[0]}` to call a command."); else - output.AppendLine($"{Environment.NewLine}You can use `{String.Join(" ", CommandChars.Take(CommandChars.Count - 1))}` and `{CommandChars.Last()}` to call a command."); + output.AppendLine($"\nYou can use `{string.Join(" ", CommandChars.Take(_commandChars.Length - 1))}` and `{_commandChars.Last()}` to call a command."); output.AppendLine("`help ` can tell you more about how to use a command."); @@ -80,8 +70,9 @@ namespace Discord.Commands } else { - if (_commands.ContainsKey(e.Args[0])) - await Reply(e, CommandDetails(_commands[e.Args[0]])); + var cmd = _map.GetCommand(e.Args[0]); + if (cmd != null) + await Reply(e, CommandDetails(cmd)); else await Reply(e, $"`{e.Args[0]}` is not a valid command."); } @@ -99,7 +90,7 @@ namespace Discord.Commands string msg = e.Message.Text; if (msg.Length == 0) return; - if (UseCommandChar) + if (_commandChars.Length > 0) { bool isPrivate = e.Message.Channel.IsPrivate; bool hasCommandChar = CommandChars.Contains(msg[0]); @@ -112,44 +103,41 @@ namespace Discord.Commands return; // Same, but public. } - string cmd; - CommandPart[] args; - if (!CommandParser.Parse(msg, out cmd, out args)) - return; - - if (_commands.ContainsKey(cmd)) - { - Command comm = _commands[cmd]; - - //Clean args + //Parse command + Command command; + int argPos; + CommandParser.ParseCommand(msg, _map, out command, out argPos); + if (command == null) + { + CommandEventArgs errorArgs = new CommandEventArgs(e.Message, null, null, null); + RaiseCommandError(CommandErrorType.UnknownCommand, errorArgs); + return; + } + else + { + //Parse arguments + CommandPart[] args; + if (!CommandParser.ParseArgs(msg, argPos, command, out args)) + { + CommandEventArgs errorArgs = new CommandEventArgs(e.Message, null, null, null); + RaiseCommandError(CommandErrorType.InvalidInput, errorArgs); + return; + } int argCount = args.Length; - string[] newArgs = null; - - if (comm.MaxArgs != null && argCount > 0) - { - newArgs = new string[(int)comm.MaxArgs]; - for (int j = 0; j < newArgs.Length; j++) - newArgs[j] = args[j].Value; - } - else if (comm.MaxArgs == null && comm.MinArgs == null) - { - newArgs = new string[argCount]; - for (int j = 0; j < newArgs.Length; j++) - newArgs[j] = args[j].Value; - } + //Get information for the rest of the steps int userPermissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0; - var eventArgs = new CommandEventArgs(e.Message, comm, userPermissions, newArgs); + var eventArgs = new CommandEventArgs(e.Message, command, userPermissions, args.Select(x => x.Value).ToArray()); // Check permissions - if (userPermissions < comm.MinPerms) + if (userPermissions < command.MinPerms) { RaiseCommandError(CommandErrorType.BadPermissions, eventArgs); return; } - + //Check arg count - if (argCount < comm.MinArgs) + if (argCount < command.MinArgs) { RaiseCommandError(CommandErrorType.BadArgCount, eventArgs); return; @@ -159,21 +147,13 @@ namespace Discord.Commands try { RaiseRanCommand(eventArgs); - var task = comm.Handler(eventArgs); - if (task != null) - await task.ConfigureAwait(false); - } - catch (Exception ex) - { - RaiseCommandError(CommandErrorType.Exception, eventArgs, ex); - } - } - else - { - CommandEventArgs eventArgs = new CommandEventArgs(e.Message, null, null, null); - RaiseCommandError(CommandErrorType.UnknownCommand, eventArgs); - return; - } + await command.Run(eventArgs).ConfigureAwait(false); + } + catch (Exception ex) + { + RaiseCommandError(CommandErrorType.Exception, eventArgs, ex); + } + } }; } @@ -198,7 +178,7 @@ namespace Discord.Commands else if (command.MinArgs == null && command.MaxArgs != null) output.Append($" ≤{command.MaxArgs.ToString()} Args"); - output.Append($": {command.Description}"); + output.Append($": {command.Description ?? "No description set for this command."}"); return output.ToString(); } @@ -216,13 +196,14 @@ namespace Discord.Commands public CommandBuilder CreateCommand(string cmd) { var command = new Command(cmd); - _commands.Add(cmd, command); - return new CommandBuilder(command); + _commands.Add(command); + return new CommandBuilder(null, command, ""); } internal void AddCommand(Command command) { - _commands.Add(command.Text, command); + _commands.Add(command); + _map.AddCommand(command.Text, command); } } }