The Commands API - The Future


#1

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 as COMMAND_TARGET or something like that if this goes ahead): this contains what the traditional CommandSource would be. If not present, would be the Cause#first(CommandSource.class).
  • COMMAND_PERMISSION_SUBJECT: the Subject permission checks should be done on. If not present, defaults what the COMMAND_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. Calling parameter(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 as parameter(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, 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 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 a CommandArgs and returns a value (basically, parseValue in CommandElement). This can be used in setParser of the parameter builder.
  • ValueCompleters provide tab complete suggestions for a parameter (complete). This can be used in setSuggestions of the parameter builder.
  • ValueUsages define what text is sent back, if any, for the parameter when usage is requested (getUsage). This can be used in setUsage 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
}

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: the SendCommandEvent as it is today.
  • Selected: when a Command has been selected for processing, the CommandMapping is available for inspection, the command can no longer be changed (but can be cancelled)
  • Post: when a Command 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 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.


Sponge Status Update - 21st December 2017
Sponge Status Update - 25th Feb 2018
#2

My thoughts on CommandSource: There are a couple attributes/methods available in each CommandSource, and it needs to be obvious how to use each one, even if the command gets run from a sign or through /execute.

  • Display name
  • Sending messages - some messages should be relayed to other destinations, such as through /gamerule logAdminCommands.
  • Permissions
  • Associated entity (optional) - this may not be the same object as the CommandSource.
  • Location (optional) - this may not be the same as the associated entity’s location.

Any ideas how to make these obvious to plugin developers without making it inflexible?


#3

One thing that bugged me was that I can’t seem to get the used alias for a command/subcommand. While this of course should not be used to base decisions on it would be useful to do something like src.sendMessage(Text.of("Use '/", ALIAS, " disable' to turn the feature back off")); or something like that.

Also often there are very simple commands, that require no options, choices or completion, where I think parsing a descriptive string would be more convenient like:

CommandSpec.builder("/pm <Target:Player> <Message:RemainingString>")
  .permission("example.command.pm.base")
  .description("Send a private message to someone")
  .executor(...)
  .build();

In a little toolkit I’m currently writing I even simplified it to

BoxCommand.registerCommand("giveitem", "toolbox.item.cmd.give.base", (src, args)->{
      //...
      return CommandResult.success();
    });

While I’m aware that this takes away loads of depth and complexity from the command system I often found this to be enough for what I wanted to do, so I think it would be nice to have something like that in Sponge. (I’d gladly share the string parser if there’s interest)


#4

The main problem with sponge overall (which also includes your changes in Command api) is that it wont take some other complex & mature framework as an example (think of spring, or ejb).

There is a ridiculously large space which could be simplified by simple CDI-like framework

In a strogly typed & high level language such as Java you can very easily take aways lots of boilerplate tier code;

Your parameter builder is a great example.

I would be more happier with simple annotation preprocessor. Ill just leave here one simple example

@Command("/mycommand") //Command annotation is discovered once sponge lods your plugin, no need to register commands manually.
@Permission("my.permission")
public class MyAwesomeCommand { //simple bean class, every time command is executed sponge automatically creates a new instance of this class

   @Param //a parameter which should be automatically parsed and set by sponge command handler
   @Optional  //whenever its optional
   @DefaultValue(10) //if a player wont specify this argument in a command the default value will be 10 , not 0
   public void int

         @DefaultHandler
         public void handle() {
           runstuff

         }
}

You know that the argument is an int, no need to force developers to write explicit code with some kind of param builder.

How to handle sub commands?
Maybe it would be nice to either have a subclass, or an annotation @Subcommand()

How to handle tab completition?
You already know type of some arguments i wish to have. And with a little bit of brainstorming i belive you could find a way how to abstract away lots of code again.


#5

I edited post above few times, because i somehow posted accidently an uncompleted version :smiley:


#6

The intention is to extend the API once we’ve nailed down this layer, and will sit on top of it. An annotation based system will not enable complex use cases, because annotations do not make it easy to process complex data, there is no point building two separate systems for two different use cases. There is a lot more to consider than just say “blow everything away and just use annotations”.

Annotation based commands will make the system simpler for simple commands, hell, I’d go as so far as to make it a method with event filter like annotations, rather than a class, but the scaffolding needs to be there first. That way, commands will become as simple as registering an event is right now - when the command is simple enough.


#7

Yes annotations do not make it easy to process complex data,but first it does not mean its impossible, second what type of data are you talking about right now? its minecraft where users have command line on the input (:

The worst case scenario which seems to be a current trend in minecraft is to just type down a json.
In that case its quite simply to abstract away eg.: tab completition.


#8

Think about the custom command elements people have right now - for example, my homes data or warp data, where the choices are heavily contextual. The reason why the API is written as it is is to remove the need to parse strings in a command executor, for example. Dynamic tab completion would not necessarily be easy using annotations - at what point would you be using annotations for the sake of? It’s probably easier to just write a line of code pulling the data you need than making an entire json schema out of it. Probably more performant too. Annotations are not the answer to everything. Honestly, adding json into the mix is just going to add a layer of complication there doesn’t need to be.

Again, I have zero problems with adding an annotation based layer. It would make the simple commands so much easier to write, I do not argue with you there. Replacing the current offering with annotations, on the other hand, is not on the cards, because that would either involve a step back in capability, or in the more complex scenarios scattered annotations and json which I fear would confuse the junior Spongineers.

Annotations have their place. They are not, and should not be, the only solution.


#9

Annotations would work if this was C# and they were classes which could extend other classes or implement interfaces. But this is not the case. An annotation based system can only work within a very structured way, and can’t be extended with e.g. custom parameters.


#10

This is just false. Almost any command parsing library for Java that uses annotations allows custom types: Picocli does, JCommander does, aikar commands does, Intake does.

I do not want to be rude but did you even look at existing libraries before implementing your own solution?


#11

Global registration followed by annotations is moderately unwieldy, though. Save the craziness for external libraries.