Custom arguments
Custom arguments are a quality-of-life feature that the CommandAPI offers which allows you to perform pre-processing on an argument in the argument instance rather than in your executes()
method for a command. They are designed to be used for multiple commands - you can define the argument once and can use it wherever you want when declaring commands.
The CustomArgument<T, B>
has the following constructor:
public CustomArgument(Argument<B> base, CustomArgumentInfoParser<T, B> parser);
This constructor takes in two parameters:
-
A "base argument", which is the argument that it'll use as the underlying parser. For example, if this is a
StringArgument
, it'll use the StringArgument's parsing rules ( alphanumeric characters (A-Z, a-z and 0-9), and the underscore character) and if this is aLocationArgument
, it'll take three numerical values. -
A "parser", which lets you process the argument based on its input. This is described in more detail below.
Type params
The custom argument requires two type parameters, <T>
and <B>
:
-
<T>
refers to the type that this argument will return when parsing the arguments for a command. For instance, if you have aCustomArgument<Player, ...>
, then when parsing the arguments for the command, you would cast it to aPlayer
object. -
<B>
refers to the type that the base argument will return. This can be found in the Argument Casting section. For example, if the base argument is aStringArgument
, you'd haveCustomArgument<..., String>
.
The CustomArgumentInfoParser class
To create a parser for a CustomArgument
, you need to provide a CustomArgumentInfoParser
function to the constructor. The CustomArgumentInfoParser
class is a functional interface which accepts CustomArgumentInfo
and returns T
, an object of your choosing:
@FunctionalInterface
public interface CustomArgumentInfoParser<T, B> {
public T apply(CustomArgumentInfo<B> info) throws CustomArgumentException;
}
The CustomArgumentInfo
record is very similar to the SuggestionInfo
record for declaring argument suggestions. This record contains the following methods:
public record CustomArgumentInfo<B> {
CommandSender sender();
Object[] previousArgs();
String input();
B currentInput();
}
These fields are as follows:
-
CommandSender sender();
sender()
represents the command sender that is typing the command. This is normally aPlayer
, but can also be a console command sender if using a Paper server. -
Object[] previousArgs();
previousArgs()
represents a list of previously declared arguments, which are parsed and interpreted as if they were being used to execute the command. -
String input();
input()
represents the current input for the custom argument that the user has typed. For example, if a user is typing/mycommand hello
and the first argument is a CustomArgument, theinput()
would return"hello"
. -
B currentInput();
currentInput()
represents the current input, as parsed by the base argument. For example, if your base argument was anIntegerArgument
, the return type ofcurrentInput()
would be anint
.
Example - World argument
Say we want to create an argument to represents the list of available worlds on the server. We want to have an argument which always returns a Bukkit World
object as the result. Here, we create a method worldArgument()
that returns our custom argument that returns a World
. First, we retrieve our String[]
of world names to be used for our suggestions. We then write our custom argument that creates a World
object from the input (in this case, we simply convert the input to a World
using Bukkit.getWorld(String)
). We perform error handling before returning our result:
// Function that returns our custom argument
public Argument<World> customWorldArgument(String nodeName) {
// Construct our CustomArgument that takes in a String input and returns a World object
return new CustomArgument<World, String>(new StringArgument(nodeName), info -> {
// Parse the world from our input
World world = Bukkit.getWorld(info.input());
if (world == null) {
throw CustomArgumentException.fromMessageBuilder(new MessageBuilder("Unknown world: ").appendArgInput());
} else {
return world;
}
}).replaceSuggestions(ArgumentSuggestions.strings(info ->
// List of world names on the server
Bukkit.getWorlds().stream().map(World::getName).toArray(String[]::new))
);
}
// Function that returns our custom argument
fun worldArgument(nodeName: String): Argument<World> {
// Construct our CustomArgument that takes in a String input and returns a World object
return CustomArgument<World, String>(StringArgument(nodeName)) { info ->
// Parse the world from our input
val world = Bukkit.getWorld(info.input())
if (world == null) {
throw CustomArgumentException.fromMessageBuilder(MessageBuilder("Unknown world: ").appendArgInput())
} else {
world
}
}.replaceSuggestions(ArgumentSuggestions.strings { _ ->
// List of world names on the server
Bukkit.getWorlds().map{ it.name }.toTypedArray()
})
}
In our error handling step, we check if the world is equal to null (since the Bukkit.getWorld(String)
is @Nullable
). To handle this case, we throw a CustomArgumentException
with an error from a MessageBuilder
. The CustomArgumentException
has various static factory methods tailored to your desired printing method, so a message builder isn't required each time:
CustomArgumentException fromBaseComponents(BaseComponent[] errorMessage);
CustomArgumentException fromString(String errorMessage);
CustomArgumentException fromAdventureComponent(Component errorMessage);
CustomArgumentException fromMessageBuilder(MessageBuilder errorMessage);
We can use our custom argument like any other argument. Say we wanted to write a command to teleport to a specific world. We will create a command of the following syntax:
/tpworld <world>
Since we have defined the method worldArgument()
which automatically generates our argument, we can use it as follows:
new CommandAPICommand("tpworld")
.withArguments(customWorldArgument("world"))
.executesPlayer((player, args) -> {
player.teleport(((World) args.get(0)).getSpawnLocation());
})
.register();
CommandAPICommand("tpworld")
.withArguments(worldArgument("world"))
.executesPlayer(PlayerCommandExecutor { player, args ->
player.teleport((args[0] as World).spawnLocation)
})
.register()
commandAPICommand("tpworld") {
worldArgument("world") // This method is actually also built into the Kotlin DSL
playerExecutor { player, args ->
player.teleport((args[0] as World).spawnLocation)
}
}
By using a CustomArgument
(as opposed to a simple StringArgument
and replacing its suggestions), we are able to provide a much more powerful form of error handling (automatically handled inside the argument), and we can reuse this argument for other commands.
Message Builders
The MessageBuilder
class is a class to easily create messages to describe errors when a sender sends a command which does not meet the expected syntax for an argument. It acts in a similar way to a StringBuilder
, where you can append content to the end of a String.
The following methods are as follows:
Method | Description |
---|---|
appendArgInput() | Appends the argument that failed that the sender submitted to the end of the builder. E.g. /foo bar will append bar |
appendFullInput() | Appends the full command that a sender submitted to the end of the builder. E.g. /foo bar will append foo bar |
appendHere() | Appends the text <--[HERE] to the end of the builder |
append(Object) | Appends an object to the end of the builder |
Example - Message builder for invalid objective argument
To create a MessageBuilder
, simply call its constructor and use whatever methods as you see fit. Unlike a StringBuilder
, you don't have to "build" it when you're done - the CommandAPI does that automatically:
new MessageBuilder("Unknown world: /").appendFullInput().appendHere();