Browse Source

Cleaned up CommandsPlugin, added CommandMap, new parameter declaration and aliases.

tags/docs-0.9
RogueException 9 years ago
parent
commit
44fb33b511
7 changed files with 348 additions and 185 deletions
  1. +3
    -0
      src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj
  2. +84
    -5
      src/Discord.Net.Commands/Command.cs
  3. +66
    -64
      src/Discord.Net.Commands/CommandBuilder.cs
  4. +84
    -0
      src/Discord.Net.Commands/CommandMap.cs
  5. +47
    -33
      src/Discord.Net.Commands/CommandParser.cs
  6. +1
    -1
      src/Discord.Net.Commands/CommandsPlugin.Events.cs
  7. +63
    -82
      src/Discord.Net.Commands/CommandsPlugin.cs

+ 3
- 0
src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj View File

@@ -43,6 +43,9 @@
<Compile Include="..\Discord.Net.Commands\CommandBuilder.cs">
<Link>CommandBuilder.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Commands\CommandMap.cs">
<Link>CommandMap.cs</Link>
</Compile>
<Compile Include="..\Discord.Net.Commands\CommandParser.cs">
<Link>CommandParser.cs</Link>
</Compile>


+ 84
- 5
src/Discord.Net.Commands/Command.cs View File

@@ -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<CommandEventArgs, Task> Handler;

public IEnumerable<string> Aliases => _aliases;
private string[] _aliases;

public IEnumerable<CommandParameter> Parameters => _parameters;
private CommandParameter[] _parameters;

private Func<CommandEventArgs, Task> _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<CommandEventArgs, Task> func)
{
_handler = func;
}
internal void SetHandler(Action<CommandEventArgs> 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;
}
}
}

+ 66
- 64
src/Discord.Net.Commands/CommandBuilder.cs View File

@@ -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<CommandParameter> _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<CommandParameter>();
_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<CommandEventArgs, Task> func)
public void Do(Func<CommandEventArgs, Task> func)
{
_command.Handler = func;
return this;
_command.SetHandler(func);
Build();
}
public CommandBuilder Do(Action<CommandEventArgs> func)
public void Do(Action<CommandEventArgs> 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);
}
}
}

+ 84
- 0
src/Discord.Net.Commands/CommandMap.cs View File

@@ -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<string, CommandMap> _subCommands;

public CommandMap(CommandMap parent)
{
_parent = parent;
_subCommands = new Dictionary<string, CommandMap>();
}

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;
}
}
}
}

+ 47
- 33
src/Discord.Net.Commands/CommandParser.cs View File

@@ -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<CommandPart> argList = new List<CommandPart>();

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;
}


+ 1
- 1
src/Discord.Net.Commands/CommandsPlugin.Events.cs View File

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


+ 63
- 82
src/Discord.Net.Commands/CommandsPlugin.cs View File

@@ -10,29 +10,30 @@ namespace Discord.Commands
public partial class CommandsPlugin
{
private readonly DiscordClient _client;
private Func<User, int> _getPermissions;

private Dictionary<string, Command> _commands;
private readonly Func<User, int> _getPermissions;
public Dictionary<string, Command> Commands => _commands;
public IEnumerable<Command> Commands => _commands;
private readonly List<Command> _commands;

internal CommandMap Map => _map;
private readonly CommandMap _map;

public char CommandChar { get { return CommandChars[0]; } set { CommandChars = new List<char> { value }; } } // This could possibly be removed entirely. Not sure.
public List<char> CommandChars { get; set; }
public bool UseCommandChar { get; set; }
public IEnumerable<char> 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<User, int> getPermissions = null, bool builtInHelp = false)
{
_client = client; // Wait why is this even set
_client = client;
_getPermissions = getPermissions;
_commands = new Dictionary<string, Command>();
_commands = new List<Command>();
_map = new CommandMap(null);

CommandChar = '!'; // Kept around to keep from possibly throwing an error. Might not be necessary.
CommandChars = new List<char> { '!', '?', '/' };
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<string, Command> 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 <command>` 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);
}
}
}

Loading…
Cancel
Save