Command trees

So far in this documentation, we've described many different ways to register commands. We've described writing commands by declaring a CommandAPICommand object, using a list of arguments and providing an executor for the command. We've also described another way of registering commands with multiple "paths" using the withSubcommand method to generate a tree-like structure. As of CommandAPI 7.0.0, another method for registering commands, command trees, has been introduced.


The executes() and then() methods

The Command Tree represents command structures in a tree-like fashion, in a very similar way that Brigadier's API lets you declare commands. Command tree commands effectively revolve around two methods:

public T executes(CommandExecutor executor);

public CommandTree  then(ArgumentTree branch);
public ArgumentTree then(ArgumentTree branch);

The executes() method is the same executes() method that you have seen previously in this documentation for normal CommandAPI commands. This also includes all of the executes...() methods described in Normal command executors, but for the sake of simplicity, we'll simply refer to all of these by executes().

The then() method allows you to create new "branches" in your command "tree" data structure. If you are familiar with Brigadier's then() method for argument nodes, then you should feel right at home. Otherwise, for all intents and purposes then() lets you specify additional paths that a command can take when a user is typing their command.

Because the underlying type hierarchy of command trees is fairly complex (then() having multiple return types and taking in ArgumentTree objects), instead of trying to describe how all of that works, we'll instead describe how to make command trees by using the methods executes() and then() in practice.

Declaring a command tree

The basic syntax of a command tree is effectively identical to a normal CommandAPICommand, but instead you use the CommandTree object. For example, if we want to create a simple command which sends "Hi!" to a command sender, we declare the name of our command, make use of the executes() method, and then we use the CommandTree constructor instead of the CommandAPICommand constructor:

/sayhi
new CommandAPICommand("sayhi")
    .executes((sender, args) -> {
        sender.sendMessage("Hi!");
    })
    .register();

$$\downarrow$$

new CommandTree("sayhi")
    .executes((sender, args) -> {
        sender.sendMessage("Hi!");
    })
    .register();

Adding arguments to a command tree

Unlike the CommandAPICommand class, the CommandTree class doesn't let you add arguments using the withArguments() method. Instead, it makes use of the then() method, which allows you to provide an argument to it. This is best described with an example.

Example - Declaring a command tree with a single argument

Say we want to take our /sayhi command from above and also have an argument which lets you specify a target player. In this example, we'll have the following command syntax:

/sayhi          - Says "Hi!" to the current sender
/sayhi <target> - Says "Hi!" to a target player

We can do this by adding a PlayerArgument to our command. As described above, to add this argument, we must use the then() method:

new CommandTree("sayhi")
    .executes((sender, args) -> {
        sender.sendMessage("Hi!");
    })
    .then(new PlayerArgument("target")
        .executes((sender, args) -> {
            Player target = (Player) args.get(0);
            target.sendMessage("Hi");
        }))
    .register();
CommandTree("sayhi")
    .executes(CommandExecutor { sender, _ ->
        sender.sendMessage("Hi!")
    })
    .then(PlayerArgument("target")
        .executes(CommandExecutor { _, args ->
            val target = args[0] as Player
            target.sendMessage("Hi")
        }))
    .register()

In this example, we have our normal /sayhi command using the executes() method. We then add a new argument (a new "branch" in our "tree"), the PlayerArgument, using the then() method. We want to make this branch executable, so we also use the executes() method on the argument itself. To register the full command tree (which includes both /sayhi and /sayhi <target>), we call register() on the CommandTree object.


That's effectively all of the basics of command trees! We start by writing a normal command, use executes() to make it executable and use then() to add additional paths to our command. Finally, we finish up with register() to register our command. Below, I've included a few more examples showcasing how to design commands using command trees.

Command tree examples

Example - Sign editing plugin

Say we wanted to create a plugin to let a user edit signs. We have a single command tree /signedit, with a number of branching paths set, clear, copy and paste which represent various operations that this command can be performed on a sign:

/signedit set <line_number> <text> - Sets the text for a line on a sign
/signedit clear <line_number>      - Clears a sign's text on a specific line
/signedit copy <line_number>       - Copies the current text from a line on a sign
/signedit paste <line_number>      - Pastes the copied text onto a line on a sign
new CommandTree("signedit")
    .then(new LiteralArgument("set")
        .then(new IntegerArgument("line_number", 1, 4)
            .then(new GreedyStringArgument("text")
                .executesPlayer((player, args) -> {
                    // /signedit set <line_number> <text>
                    Sign sign = getTargetSign(player);
                    int lineNumber = (int) args.get(0);
                    String text = (String) args.get(1);
                    sign.setLine(lineNumber - 1, text);
                    sign.update(true);
                 }))))
    .then(new LiteralArgument("clear")
        .then(new IntegerArgument("line_number", 1, 4)
            .executesPlayer((player, args) -> {
                // /signedit clear <line_number>
                Sign sign = getTargetSign(player);
                int lineNumber = (int) args.get(0);
                sign.setLine(lineNumber - 1, "");
                sign.update(true);
            })))
    .then(new LiteralArgument("copy")
        .then(new IntegerArgument("line_number", 1, 4)
            .executesPlayer((player, args) -> {
                // /signedit copy <line_number>
                Sign sign = getTargetSign(player);
                int lineNumber = (int) args.get(0);
                player.setMetadata("copied_sign_text", new FixedMetadataValue(this, sign.getLine(lineNumber - 1)));
            })))
    .then(new LiteralArgument("paste")
        .then(new IntegerArgument("line_number", 1, 4)
            .executesPlayer((player, args) -> {
                // /signedit copy <line_number>
                Sign sign = getTargetSign(player);
                int lineNumber = (int) args.get(0);
                sign.setLine(lineNumber - 1, player.getMetadata("copied_sign_text").get(0).asString());
                sign.update(true);
            })))
    .register();
CommandTree("signedit")
    .then(LiteralArgument("set")
        .then(IntegerArgument("line_number", 1, 4)
            .then(GreedyStringArgument("text")
                .executesPlayer(PlayerCommandExecutor { player, args ->
                    // /signedit set <line_number> <text>
                    val sign: Sign = getTargetSign(player)
                    val line_number = args[0] as Int
                    val text = args[1] as String
                    sign.setLine(line_number - 1, text)
                    sign.update(true)
                 }))))
    .then(LiteralArgument("clear")
        .then(IntegerArgument("line_number", 1, 4)
            .executesPlayer(PlayerCommandExecutor { player, args ->
                // /signedit clear <line_number>
                val sign: Sign = getTargetSign(player)
                val line_number = args[0] as Int
                sign.setLine(line_number - 1, "")
                sign.update(true)
            })))
    .then(LiteralArgument("copy")
        .then(IntegerArgument("line_number", 1, 4)
            .executesPlayer(PlayerCommandExecutor { player, args ->
                // /signedit copy <line_number>
                val sign: Sign = getTargetSign(player)
                val line_number = args[0] as Int
                player.setMetadata("copied_sign_text", FixedMetadataValue(this, sign.getLine(line_number - 1)))
            })))
    .then(LiteralArgument("paste")
        .then(IntegerArgument("line_number", 1, 4)
            .executesPlayer(PlayerCommandExecutor { player, args ->
                // /signedit copy <line_number>
                val sign: Sign = getTargetSign(player)
                val line_number = args[0] as Int
                sign.setLine(line_number - 1, player.getMetadata("copied_sign_text")[0].asString())
                sign.update(true)
            })))
    .register()