As many of you may be aware of by now, I’ve been spending a fair amount of time working on the Commands API, effectively re-writing much of it to fit in with the Sponge ecosystem today. I posted this on Github a few days ago, but in order to get more coverage and feedback, I’m cross posting here.
Note that this is NOT for API 7. The merge for this is targeted for the API 8 cycle.
This explanation is designed for developers who use commands in their plugins. For Sponge API 8, we’re undertaking one of the biggest refactors to the API - overhauling commands. I know this is a big change and it’s not likely to be universally popular, but we have technical reasons for doing it, as well as the fact that I believe this will allow for a more customisable system for API 8 and beyond.
Motivation
The command system is out of place in the current ecosystem and parts of it can be difficult to use. It doesn’t have much in the way of abstraction, and this is hindering potential development of it. I’ve also seen quite a few plugins eschew the argument system, preferring to just split the string and doing the parsing themselves. Consider the following points:
- It was built early in Sponge’s development (API 2, I believe), and hasn’t really seen much daylight since (unlike Texts, which is effectively on version three).
- The argument (parameter) building is just a load of wrapped elements, and can be difficult to follow (one example of this is Nucleus’ TP command)
- Many many objects can be created for each command, most of them duplicates of one another - we can try to reuse some simpler objects and potentially reduce Sponge’s footprint a little bit.
- It’s all API implemented, and cannot take advantage of optimisations through Sponge implementation, nor is it easy to make some changes without causing a major breakage (though this may be by design, see the consideration below)
- There are few interfaces, mostly final classes, and this makes it hard to make a slow transition to newer structures, effectively making it more difficult to make positive changes in a non-breaking manner.
So, as a result, I’ve been playing about with creating a new high level system to try to make it easier to resolve, if not resolve, some of the issues that currently plague the system.
More information can be found in the associated PR.
Cause and Context
Sponge as an API and a system is now very much centred around the tracking of causes. Events have a cause, and now commands do too. The source of a command will now ultimately be the Cause
of the command. Our cause tracking is already best in class, and with very little effort we can now make use of that in the Command system. Thus, most of our methods will now include a Cause
parameter.
This fits in well with Minecraft 1.13, as the cause of a command is no longer directly a CommandSource
, there will be the notion of a source
and a target
, the target assuming the role of the traditional CommandSource
. Usually, this will be the owner or creator of the nominal source of the cause, so will be the first CommandSource
in the cause stack - but not always. There are a few edge cases, however, where we might need to execute the command as someone, even if they are not responsible for the command, sudo
like, if you will.
As Zidane puts it:
I’m of the firm belief that once the concept of “Thing A runs command but executor is to see Thing B as invoker” comes about, singular “CommandSource” makes no sense anymore. Its the entire reason why the event API went to the Cause system: one or more things triggered the event.
This is one of the discussions around the API, do we do away with the CommandSource
completely, or do we still have a nominated source/target?
At the basic level (as it is at the moment), the process method on a command is now:
CommandResult process(Cause cause, String arguments) throws CommandException;
There are two directions we can go with this to define the target of this. Either:
- Update the method parameter to include a
CommandSource
, indicating the target of the command, who all the permission checks will be done under - Make more use of the Contexts in the Cause object to define how a command should respond
My current implementation (which is this way to test things and is in no way complete) is to add three keys to the context that can (but don’t have to be) used (there are four, but the fourth is likely to be removed):
-
COMMAND_SOURCE
(likely to be renamed asCOMMAND_TARGET
or something like that if this goes ahead): this contains what the traditionalCommandSource
would be. If not present, would be theCause#first(CommandSource.class)
. -
COMMAND_PERMISSION_SUBJECT
: theSubject
permission checks should be done on. If not present, defaults what theCOMMAND_SOURCE
would be. -
COMMAND_LOCATION
: the location that the command might be centred about (but otherwise not attached to an entity)
My thought is that with the combination of the Cause
and Context
, we can get a lot of information about the entire context of a command execution.
There are more discussions to be had around the form of the command methods and how they should work. They will be discussed in later sections.
Starting at a low level - the Command
interface
Some of you will use the ComandCallable
interface to manage your commands - this has now become the Command
interface. All of the methods that were on the original interface are now on this interface, except they all now take a Cause
instead of a CommandSource
.
One thing that we need to decide is whether the command target is a parameter on these methods, rather than expecting a developer to get the associated keys and checking for their presence first. So, the question is do we have:
CommandResult process(Cause cause, String arguments);
boolean testPermission(Cause cause);
or:
CommandResult process(Cause cause, CommandTarget target, String arguments);
boolean testPermission(Cause cause, Subject subject);
My feeling that we’ll likely go for the latter, as it provides a greater hint as to what the standard course of action is. In both cases, developers can solely rely on the Cause
if they prefer.
Going higher level - the Command.Builder
The CommandSpec.Builder
of API 8, Command.Builder
is your new one stop shop for building a command. There are a few key differences that developers must be aware of:
- “arguments” are now “parameters”, as this describes what they actually are.
- Some builder methods will start with
set
. This indicates that such an action will replace anything set previously -setExecutor(CommandExecutor)
called twice will end up with only the second executor being set. - Builder methods without
set
as a prefix will build on the previous state. Callingparameter(Parameter)
twice will add the result together - in this case, resulting in two parameters in the command. - Pluralised varargs methods are the same as chaining the singular varient together -
parameters(Parameter1, Parameter2)
is the same asparameter(Parameter1).parameter(Parameter2)
The CommandExecutor
and TargetedCommandExecutor
s
The CommandExecutor
is currently one method:
CommandResult execute(Cause cause, CommandContext context) throws CommandException;
This is subject to the previous discussion on whether the target of a command should be included in the function definition or not. However, something I noticed a lot of developers (myself included) did was build a command that is only suitable for, say, a player or the console. So, I’ve added the ability to add a TargetedCommandExecutor<T extends CommandSource>
, which you can use from the builder using targetedExecutor(TargetedCommandExecutor<T> executor, Class<T> sourceType)
. This is also a one method class (where T extends CommandSource
):
CommandResult execute(Cause cause, T source, CommandContext context) throws CommandException;
The idea is that you can then add multiple targeted executors to separate logic for console and players, for example. You can also add a custom error message for any targets that don’t fit your requirements on the builder by calling setTargetedExecutorErrorMessage(Text targetedExecutorError);
This addition should save some boilerplate code where you have to write:
if (!(src instanceof Player)) {
throw new CommandException(Text.of("You must be a player to run this!"));
}
// Command code
Child commands
We are retaining the ability to add subcommands using the child()
methods, they are relatively unchanged. However, there are some new methods pertaining to child commands that should fix some longstanding difficulties, from both a UX side and a developer side.
-
setRequirePermissionForChildren(boolean required);
: previously, subcommands required the parent command’s permission to be accessed. Now, subcommands do not need to tied to the parent permission - but by default, this is enabled. This does not affect any checks the child does itself. -
setChildExceptionBehavior(ChildExceptionBehavior exceptionBehavior);
: One of the biggest problems that players have with subcommands is that if the subcommand fails, and then the parent fails, the error message and subsequent usage you get is for the root command. This allows you to customise how this would actually work. There are three behaviours:-
RETHROW
: if a subcommand fails, then do not fall back to the command executor - display the error for the subcommand. This will be the new default. -
STORE
: if a subcommand fails, store the exception, but try to execute the parent command. If the parent fails, pass this error to the method along with the parent’s error, display both. -
SUPPRESS
: Current behaviour, if a subcommand fails, discard the error and fall back to the parent.
-
We expect most developers to use RETHROW
or STORE
as they see fit. For those of you that use Nucleus, an example of what STORE
might look like is if a subcommand errors in Nucleus - though it is unlikely to be exactly that formatting.
Flags
While this was previously part of GenericArguments
, I took the decision to split this out from the parameter system as there was a lot of confusion as to how the actual system worked - there has been at least one instance where someone thought that you needed a flag parameter per flag - when you should only have one flag parameter. It made enough sense to set it separately.
There is a new class, Flags
, and an associated builder, Flags.Builder
. The javadocs should be fairly self explanatory, but there are a couple of things to note:
- Specifying a long flag no longer requires a
-
at the beginning of a string. So, before,abc
specified three flags,-a -b -c
. Now, it specifies the long flag--abc
. -
UnknownFlagBehaviors
specify what happens when a flag like parameter is encountered, but not recognised. The default behaviour is, as before,ERROR
.
Parameters
The most drastic change to the API is parameters - previously known as arguments/command elements. The GenericArguments
class contained a lot of parsers for commands to use, but is not suited to API/impl splitting, and doesn’t fit with the rest of the Sponge ecosystem where builders prevail. It turns out that parameters are actually well suited to the builder pattern.
Replacements for GenericArguments.seq
and GenericArguments.firstOf
Before I describe the parameter ecosystem, it’s worth mentioning that the replacement for these are Parameter.seq(...)
and Parameter.firstOf(...)
. Of course, these return builders too, so you can have that fluid style you always wanted!
Parameter.firstOf(first).or(second).or(third).build();
Parameter.seq(first).then(second).then(third).build();
This will allow you to choose your style, some of you may like having the builder pattern for this. We give you that choice.
The Parameter
class
Previously, when defining your own custom parameter, you extended the CustomElement
class. This was a one size fits all class that was used for both parameter parsing and modifying parameters. The closest analogue to this class is the Parameter
interface. Parsing methods take the parameter key, a CommandArgs
(like before) and a CommandContext
(like before, but expanded). A Parameter
generally takes one (or more) arguments from CommandArgs
, turns the string into a usable value (or throws an exception) and inserts them into the CommandContext
under the provided key.
However like Command
, we have a Builder
that can be used to piece together your element, which we strongly recommend you use.
The Parameter.Builder
Parameter.Builder
is our high level solution to parameters. Such a solution allows us to separate the logic of parsing an argument (e.g. methods that took a text key in API 2+ - GenericArguments.bool(key)
) and any modifiers (e.g. methods that took another CommandElement
- GenericArguemts.optional(CommandElement)
). However, we provide more power than that by breaking down the different operations into different classes.
Using standard elements
A parameter created though Parameter.Builder
consists of three main elements:
- A text or string key -
setKey(String/Text)
- One
ValueParameter
-setParser(ValueParameter)
- Zero or more
ValueParameterModifier
s -modifier(ValueParameterModifier)
Most Sponge standard parameters and modifiers have shortcuts on the Parameter
and Parameter.Builder
classes, however, in order to make this easier. So, to define an integer:
Parameter.integerNumber().setKey("int").build();
Or, to define a parameter with two strings, but requires the permission.strings
permissions:
Parameter.string().setKey("strings").setRequiredPermission("permission.strings").repeated(2).build();
Or, an optional Player
, but also make sure there is only one if one exists:
Parameter.player().setKey("player").optionalWeak().onlyOne().build();
Or one of “eggs”, “bacon” or “spam”, defaulting to “spam” if nothing is chosen:
Parameter.choices("eggs", "bacon", "spam").setKey("choice").optionalOrDefault("spam").onlyOne().build();
You can also use the setParser(ValueParameter)
and modifiers(ValueParameterModifier)
functions directly should you want to. The last two examples could also be written as:
Parameter.builder()
.setParser(CatalogedValueParameters.PLAYER)
.setKey("player")
.modifier(CatalogedValueParameterModifiers.OPTIONAL_WEAK)
.modifier(CatalogedValueParameterModifiers.ONLY_ONE)
.build();
Parameter.builder()
.setParser(
VariableValueParameters.staticChoicesBuilder()
.choice("eggs").choice("bacon").choice("spam").build())
.setKey("choice")
.modifier(
VariableValueParameterModifiers.defaultValueModifierBuilder()
.setDefaultValueFunction(cause -> "spam").build())
.modifier(CatalogedValueParameterModifiers.ONLY_ONE)
.build();
You may also change the returned suggestions and usage text using setSuggestions
and setUsage
, to make your own parameter without having to create an entire new ValueParameter
.
You may have noticed that the command key has been separated from the ValueParameter
. This is so that we can use the same object for multiple parameters, hopefully reducing the footprint of your plugin.
Modifiers
It’s worth talking about how modifiers work. For the most part, when parsing an element, they work like they did in API 2-7 - they wrap around the parser. So if we called modifier(m1).modifier(m2)
, m1
is called first, which calls into m2
, then the parser. Control then returns back through the chain, through the remainder of m2
then m1
. Therefore, order is important with modifiers, though one of the benefits of moving implementation to implementation is that we can make exceptions if we need to - one might be optional
, moving that to the beginning of the chain, so its logic can execute after the parser has parsed and other modifiers have had their say.
The javadocs of ValueParameterModifer
tries to explain this a little better.
Defining custom argument parsers
Defining how to parse an argument (e.g., parse and integer) takes the form of the ValueParameter
, which consists of the following three functional interfaces:
-
ValueParser
s take arguments from aCommandArgs
and returns a value (basically,parseValue
inCommandElement
). This can be used insetParser
of the parameter builder. -
ValueCompleter
s provide tab complete suggestions for a parameter (complete
). This can be used insetSuggestions
of the parameter builder. -
ValueUsage
s define what text is sent back, if any, for the parameter when usage is requested (getUsage
). This can be used insetUsage
of the parameter builder.
So, if you wished, you could create a custom parameter in a Parameter.Builder
like so:
Parameter.builder().setKey("key")
// value parsers return an `Optional`.
.setParser((cause, commandArgs, commandContext) -> Optional.of(commandArgs.next()))
// value completers return a `List<String>`.
.setSuggestions(
(cause, commandArgs, commandContext) ->
Sponge.getServer().getOnlinePlayers().stream().map(x -> x.getName()).collect(Collectors.toList())
// key is the key set in `setKey` as a `Text`.
.setUsage((key, cause) -> Text.of("String ", key))
.build();
You can add any modifiers that you wish to this, though it’s worth noting that modifiers won’t override anything you add to setSuggestions
or setUsage
. You can use setSuggestions
and setUsage
to override those specific parts of any ValueParameter
you set in setParser
, so the above string -> string parser could be written as:
Parameter.builder().setKey("key")
.setParser(CataloguedValueParamters.STRING)
// value completers return a `List<String>`.
.setSuggestions(
(cause, commandArgs, commandContext) ->
Sponge.getServer().getOnlinePlayers().stream().map(x -> x.getName()).collect(Collectors.toList())
// key is the key set in `setKey` as a `Text`.
.setUsage((key, cause) -> Text.of("String ", key))
.build();
or even:
Parameter.string().setKey("key")
// value completers return a `List<String>`.
.setSuggestions(
(cause, commandArgs, commandContext) ->
Sponge.getServer().getOnlinePlayers().stream().map(x -> x.getName()).collect(Collectors.toList())
// key is the key set in `setKey` as a `Text`.
.setUsage((key, cause) -> Text.of("String ", key))
.build();
Defining custom argument modifiers
A modifier is an element that can manipulate the parsing of an element before or after the actual parsing, suggestion grabbing or usage text display takes place. Modifiers implement that ValueParameterModifer
class.
Assume that we have two modifiers, called though the builder using modifier(m1).modifier(m2)
. Modifier m1 will get called first:
The main method in the modifier is the onParse method.
void onParse(Text key, Cause cause, CommandArgs args, CommandContext context, ParsingContext parsingContext)
throws ArgumentParseException;
This method is called before the associated ValueParser
is called. This method is designed to wrap around to call to the parser - this method should pass control to another modifier using ParsingContext#next()
. So, the body of onParse
can be thought of like this:
void onParse(Text key, Cause cause, CommandArgs args, CommandContext context, ParsingContext parsingContext)
throws ArgumentParseException) {
// do logic before parsing
if (someErrorCondition) {
throw args.createError(Text.of(error)); // prevent parsing from continuing, command should not execute.
}
// Go to the next modifier or parser in the chain
parsingContext.next();
// Do any checks. Do not assume that anything has been parsed, another modifier may have prevented it.
if (someOtherCondition) {
throw args.createError(Text.of(error2)); // prevent parsing from continuing, command should not execute.
}
// No error, let previous handler finish up
}
ValueParameterModifier
s also have two other methods:
List<String> complete(Cause cause, CommandArgs args, CommandContext context, List<String> currentCompletions)
Text getUsage(Text key, Cause cause, Text currentUsage)
Modifiers will simply be called after the suggestion and usage is determined by the ValueCompleter/ValueUsage
. The modifiers will simply be called in order.
Catalogued parameters and modifiers
If you create a parameter or modifier and wish to make it accessibile to other plugins, you can register it in the Sponge registry. Simply extend the CatalogedValueParameter
or CatalogedValueParameterModifier
interfaces instead of the standard interfaces. Other plugins can then get your parameter from the Sponge registry, should they wish to.
Command Events
Currently, Sponge has the SendCommandEvent
. I propose to deprecate this and change this into three events, all subinterfaces of the new CommandExecutionEvent
:
-
Pre
: theSendCommandEvent
as it is today. -
Selected
: when aCommand
has been selected for processing, theCommandMapping
is available for inspection, the command can no longer be changed (but can be cancelled) -
Post
: when aCommand
has completed execution
Dispatcher: Subcommand Discovery
It’s been noted that it’s not easy to find out what subcommands that a Command
built using Command.Builder
might have. I’ve introduced a getCommandNode
on Dispatcher
s (and therefore, the CommandManager
), which allows you to get the subcommands and relating mappings by walking a command tree through the use of CommandNode
s (name subject to change).
Next Steps
Our next steps is to get this tested and gather feedback - it’s almost ready, though there are some bugs that squishing first. We hope that you like these changes, while we accept that it’ll be some work for some of you to bring your plugins inline, this will allow us to more easily make implementation changes and will allow for a stable, more featureful API for years to come. This includes being able to keep up with Brigadier, Minecraft 1.13’s new command library that will sync up usage for each player.