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_TARGETor something like that if this goes ahead): this contains what the traditionalCommandSourcewould be. If not present, would be theCause#first(CommandSource.class).COMMAND_PERMISSION_SUBJECT: theSubjectpermission checks should be done on. If not present, defaults what theCOMMAND_SOURCEwould 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
setas 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 TargetedCommandExecutors
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,abcspecified three flags,-a -b -c. Now, it specifies the long flag--abc. UnknownFlagBehaviorsspecify 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
ValueParameterModifiers -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:
ValueParsers take arguments from aCommandArgsand returns a value (basically,parseValueinCommandElement). This can be used insetParserof the parameter builder.ValueCompleters provide tab complete suggestions for a parameter (complete). This can be used insetSuggestionsof the parameter builder.ValueUsages define what text is sent back, if any, for the parameter when usage is requested (getUsage). This can be used insetUsageof 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
}
ValueParameterModifiers 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: theSendCommandEventas it is today.Selected: when aCommandhas been selected for processing, theCommandMappingis available for inspection, the command can no longer be changed (but can be cancelled)Post: when aCommandhas 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 Dispatchers (and therefore, the CommandManager), which allows you to get the subcommands and relating mappings by walking a command tree through the use of CommandNodes (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.
