Optional Arguments

Sometimes, you want to implement a command that has arguments that do not need to be entered. Take a /sayhi command for example. You may want to say "Hi" to yourself, or to another player. For that, we want this command syntax:

/sayhi          - Says "Hi!" to yourself
/sayhi <target> - Says "Hi!" to a target player

To implement these commands, the CommandAPI provides two methods to help you with that:

Argument withOptionalArguments(List<Argument<?>> args);
Argument withOptionalArguments(Argument<?>... args);

Example - /sayhi command with an optional argument

For example, say we're registering a command /sayhi:

/sayhi          - Says "Hi!" to yourself
/sayhi <target> - Says "Hi!" to a target player

For that, we are going to register a command /sayhi. To add optional arguments, we are going to use the withOptionalArguments(Argument... args) method:

new CommandAPICommand("sayhi")
    .withOptionalArguments(new PlayerArgument("target"))
    .executesPlayer((player, args) -> {
        Player target = (Player) args.get("target");
        if (target != null) {
            target.sendMessage("Hi!");
        } else {
            player.sendMessage("Hi!");
        }
    })
    .register();
CommandAPICommand("sayhi")
    .withOptionalArguments(PlayerArgument("target"))
    .executesPlayer(PlayerCommandExecutor { player, args ->
        val target: Player? = args["target"] as Player?
        if (target != null) {
            target.sendMessage("Hi!")
        } else {
            player.sendMessage("Hi!")
        }
    })
    .register()
commandAPICommand("sayhi") {
    playerArgument("target", optional = true)
    playerExecutor { player, args ->
        val target: Player? = args["target"] as Player?
        if (target != null) {
            target.sendMessage("Hi!")
        } else {
            player.sendMessage("Hi!")
        }
    }
}

This gives us the ability to run both /sayhi and /sayhi <target> with the same command name "sayhi", but have different results based on the arguments used.

You can notice two things:

  • We use the withOptionalArguments method to add an optional argument to a command
  • We use args.get("target") to get our player out of the arguments

With optional arguments, there is a possibility of them being not present in the arguments of a command. The reason we use args.get("target") is that this will just return null and you can handle what should happen.

Setting existing arguments as optional arguments

In order to set arguments as optional the CommandAPI has the method setOptional(boolean):

Argument setOptional(boolean optional);

When using the withOptionalArguments method it might be interesting to know that it calls the setOptional() method internally. This means that the following two examples are identical:

new CommandAPICommand("optional")
    .withOptionalArguments(new PlayerArgument("target"))
new CommandAPICommand("optional")
    .withArguments(new PlayerArgument("target").setOptional(true))

However, calling withOptionalArguments is safer because it makes sure that the argument is optional because of that internal call.

Avoiding null values

Previously, we've looked at how to handle null values. To make all of this easier, the CommandAPI implements multiple additional methods for CommandArguments:

Object getOrDefault(int index, Object defaultValue);
Object getOrDefault(int index, Supplier<?> defaultValue);
Object getOrDefault(String nodeName, Object defaultValue);
Object getOrDefault(String nodeName, Supplier<?> defaultValue);
Optional<Object> getOptional(int index)
Optional<Object> getOptional(String nodeName)

The examples will be using the getOptional methods but there is no downside of using the getOrDefault methods.

Example - /sayhi command while using the getOptional method

Let's register the /sayhi command from above a second time - this time using a getOptional method. We are using the exact same command syntax:

/sayhi          - Says "Hi!" to yourself
/sayhi <target> - Says "Hi!" to a target player

This is how the getOptional method is being implemented:

new CommandAPICommand("sayhi")
    .withOptionalArguments(new PlayerArgument("target"))
    .executesPlayer((player, args) -> {
        Player target = (Player) args.getOptional("target").orElse(player);
        target.sendMessage("Hi!");
    })
    .register();
CommandAPICommand("sayhi")
    .withOptionalArguments(PlayerArgument("target"))
    .executesPlayer(PlayerCommandExecutor { player, args ->
        val target: Player = args.getOptional("target").orElse(player) as Player
        target.sendMessage("Hi!")
    })
    .register()
commandAPICommand("sayhi") {
    playerArgument("target", optional = true)
    playerExecutor { player, args ->
        val target: Player = args.getOptional("target").orElse(player) as Player
        target.sendMessage("Hi!")
    }
}

Implementing required arguments after optional arguments

We've now talked about how to implement optional arguments and how to avoid null values returned by optional arguments when they aren't provided when executing the command.

Now we also want to talk about how to implement required arguments after optional arguments. For this, the CommandAPI implements a combineWith method for arguments:

AbstractArgument combineWith(Argument<?>... combinedArguments);

You will need to use this method if you want to have a required argument after an optional argument. In general, this is which pattern the CommandAPI follows while dealing with optional arguments:

  1. You have a CommandAPICommand and you add arguments to it.
  2. After your required arguments, you can provide optional arguments.

At this point your command is basically done. Any attempt to add a required argument will result in an OptionalArgumentException. However, this is where the combineWith method comes in. This method allows you to combine arguments. Let's say you have an optional StringArgument (here simplified to A) and you want a required PlayerArgument (here simplified to B). Argument B should only be required if argument A is given. To implement that logic, we are going to use the combineWith method so that we have this syntax:

A.combineWith(B)

This does two things:

  1. When checking optional argument constraints the argument B will be ignored so the OptionalArgumentException will not be thrown
  2. It allows you to define additional optional arguments afterwards which can only be entered if argument B has been entered

This is how you would add another optional PlayerArgument (here simplified to C):

new CommandAPICommand("mycommand")
    .withOptionalArguments(A.combineWith(B))
    .withOptionalArguments(C)

Let's say you declare your arguments like this:

new CommandAPICommand("mycommand")
    .withOptionalArguments(A.combineWith(B))
    .withArguments(C)

This would result in an OptionalArgumentException because you are declaring a required argument after an optional argument without creating that exception for argument C like you do for argument B.

Example - Required arguments after optional arguments

We want to register a command /rate with the following syntax:

/rate                           - Sends an information message
/rate <topic> <rating>          - Rates a topic with a rating and sends a message to the command sender
/rate <topic> <rating> <target> - Rates a topic with a rating and sends a message to the target

To implement that structure we make use of the combineWith method to make the argument after the optional argument <topic> required:

new CommandAPICommand("rate")
    .withOptionalArguments(new StringArgument("topic").combineWith(new IntegerArgument("rating", 0, 10)))
    .withOptionalArguments(new PlayerArgument("target"))
    .executes((sender, args) -> {
        String topic = (String) args.get("topic");
        if(topic == null) {
            sender.sendMessage(
                "Usage: /rate <topic> <rating> <player>(optional)",
                "Select a topic to rate, then give a rating between 0 and 10",
                "You can optionally add a player at the end to give the rating to"
            );
            return;
        }

        // We know this is not null because rating is required if topic is given
        int rating = (int) args.get("rating");

        // The target player is optional, so give it a default here
        CommandSender target = (CommandSender) args.getOptional("target").orElse(sender);

        target.sendMessage("Your " + topic + " was rated: " + rating + "/10");
    })
    .register();
CommandAPICommand("rate")
    .withOptionalArguments(StringArgument("topic").combineWith(IntegerArgument("rating", 0, 10)))
    .withOptionalArguments(PlayerArgument("target"))
    .executes(CommandExecutor { sender, args ->
        val topic: String? = args["topic"] as String?
        if (topic == null) {
            sender.sendMessage(
                "Usage: /rate <topic> <rating> <player>(optional)",
                "Select a topic to rate, then give a rating between 0 and 10",
                "You can optionally add a player at the end to give the rating to"
            )
            return@CommandExecutor
        }

        // We know this is not null because rating is required if topic is given
        val rating = args["rating"] as Int

        // The target player is optional, so give it a default here
        val target: CommandSender = args.getOptional("target").orElse(sender) as CommandSender

        target.sendMessage("Your $topic was rated: $rating/10")
    })
    .register()
commandAPICommand("rate") {
    argument(StringArgument("topic").setOptional(true).combineWith(IntegerArgument("rating", 0, 10)))
    playerArgument("target", optional = true)
    anyExecutor { sender, args ->
        val topic: String? = args["topic"] as String?
        if (topic == null) {
            sender.sendMessage(
                "Usage: /rate <topic> <rating> <player>(optional)",
                "Select a topic to rate, then give a rating between 0 and 10",
                "You can optionally add a player at the end to give the rating to"
            )
            return@anyExecutor
        }

        // We know this is not null because rating is required if topic is given
        val rating = args["rating"] as Int

        // The target player is optional, so give it a default here
        val target: CommandSender = args.getOptional("target").orElse(sender) as CommandSender

        target.sendMessage("Your $topic was rated: $rating/10")
    }
}