Document not found (404)
+This URL is invalid, sorry. Please use the navigation bar or search to continue.
+ +diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index b65f307b8c..3cdf2df26f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -20,6 +20,8 @@ body: description: What version of Minecraft are you using? _(If using a minor version that isn't specified, such as 1.18.1, just select the latest version below it such as 1.18)_ multiple: false options: + - "1.21" + - "1.20.6" - "1.20.5" - "1.20.4" - "1.20.3" diff --git a/Doxyfile b/Doxyfile index 4de956a6ea..09a73845fc 100644 --- a/Doxyfile +++ b/Doxyfile @@ -38,7 +38,7 @@ PROJECT_NAME = CommandAPI # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 9.5.1 +PROJECT_NUMBER = 9.5.2 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/README.md b/README.md index 60111964ae..7b88dc7d14 100644 --- a/README.md +++ b/README.md @@ -53,22 +53,23 @@ The list of what version of the CommandAPI you'll need to run on a specific vers | **1.16.2** | v4.0 - 5.12, 8.3.0 - 9.4.2 | 9.4.2 | 16 | | **1.16.3** | v4.2 - 5.12, 8.3.0 - 9.4.2 | 9.4.2 | 16 | | **1.16.4** | v5.2 - 5.12, 8.3.0 - 9.4.2 | 9.4.2 | 16 | -| **1.16.5** | v5.7 - 7.0.0, 8.3.0 - 9.5.1 | 9.5.1 | 16 | -| **1.17** | 6.0.x - 9.5.1 | 9.5.1 | 16 | -| **1.17.1** | 6.1.x - 9.5.1 | 9.5.1 | 16 | -| **1.18, 1.18.1** | 6.5.2 - 9.5.1 | 9.5.1 | 16 | -| **1.18.2** | 6.5.4 - 9.5.1 | 9.5.1 | 16 | -| **1.19** | 8.3.0 - 9.5.1 | 9.5.1 | 16 | -| **1.19.1** | 8.5.0 - 9.5.1 | 9.5.1 | 16 | -| **1.19.2** | 8.5.1 - 9.5.1 | 9.5.1 | 16 | -| **1.19.3** | 8.7.0 - 9.5.1 | 9.5.1 | 16 | -| **1.19.4** | 8.8.0 - 9.5.1 | 9.5.1 | 16 | -| **1.20** | 9.0.2 - 9.5.1 | 9.5.1 | 16 | -| **1.20.1** | 9.0.3 - 9.5.1 | 9.5.1 | 16 | -| **1.20.2** | 9.2.0 - 9.5.1 | 9.5.1 | 16 | -| **1.20.3, 1.20.4** | 9.3.0 - 9.5.1 | 9.5.1 | 16 | -| **1.20.5, 1.20.6** | 9.4.0 - 9.5.1 | 9.5.1 | 16 | -| **1.21** | 9.5.0 - 9.5.1 | 9.5.1 | 16 | +| **1.16.5** | v5.7 - 7.0.0, 8.3.0 - 9.5.2 | 9.5.2 | 16 | +| **1.17** | 6.0.x - 9.5.2 | 9.5.2 | 16 | +| **1.17.1** | 6.1.x - 9.5.2 | 9.5.2 | 16 | +| **1.18, 1.18.1** | 6.5.2 - 9.5.2 | 9.5.2 | 16 | +| **1.18.2** | 6.5.4 - 9.5.2 | 9.5.2 | 16 | +| **1.19** | 8.3.0 - 9.5.2 | 9.5.2 | 16 | +| **1.19.1** | 8.5.0 - 9.5.2 | 9.5.2 | 16 | +| **1.19.2** | 8.5.1 - 9.5.2 | 9.5.2 | 16 | +| **1.19.3** | 8.7.0 - 9.5.2 | 9.5.2 | 16 | +| **1.19.4** | 8.8.0 - 9.5.2 | 9.5.2 | 16 | +| **1.20** | 9.0.2 - 9.5.2 | 9.5.2 | 16 | +| **1.20.1** | 9.0.3 - 9.5.2 | 9.5.2 | 16 | +| **1.20.2** | 9.2.0 - 9.5.2 | 9.5.2 | 16 | +| **1.20.3, 1.20.4** | 9.3.0 - 9.5.2 | 9.5.2 | 16 | +| **1.20.5, 1.20.6** | 9.4.0 - 9.5.2 | 9.5.2 | 16 | +| **1.21** | 9.5.0 - 9.5.2 | 9.5.2 | 16 | +| **1.21.1** | 9.5.2 | 9.5.2 | 16 | ----- @@ -408,6 +409,23 @@ This is the current roadmap for the CommandAPI (as of 30th April 2024):
+CommandAPIBukkit#failWithAdventureComponent(ComponentLike)
methodThis URL is invalid, sorry. Please use the navigation bar or search to continue.
+ +The AdvancementArgument
class represents in-game advancements. As expected, the AdvancementArgument
can be casted to Bukkit's Advancement
class.
Say we want to award a player an advancement. First, we need the syntax that our command will use:
+/award <player> <advancement>
+
+Since we require a player, we will use the PlayerArgument
for this example. Given a player, we can simply get the AdvancementProgress
for that player, and then award the criteria required to fully complete the provided advancement.
new CommandAPICommand("award")
+ .withArguments(new PlayerArgument("player"))
+ .withArguments(new AdvancementArgument("advancement"))
+ .executes((sender, args) -> {
+ Player target = (Player) args.get("player");
+ Advancement advancement = (Advancement) args.get("advancement");
+
+ // Award all criteria for the advancement
+ AdvancementProgress progress = target.getAdvancementProgress(advancement);
+ for (String criteria : advancement.getCriteria()) {
+ progress.awardCriteria(criteria);
+ }
+ })
+ .register();
+
+CommandAPICommand("award")
+ .withArguments(PlayerArgument("player"))
+ .withArguments(AdvancementArgument("advancement"))
+ .executes(CommandExecutor { _, args ->
+ val target = args["player"] as Player
+ val advancement = args["advancement"] as Advancement
+
+ // Award all criteria for the advancement
+ val progress = target.getAdvancementProgress(advancement)
+ for (criteria in advancement.criteria) {
+ progress.awardCriteria(criteria)
+ }
+ })
+ .register()
+
+commandAPICommand("award") {
+ playerArgument("player")
+ advancementArgument("advancement")
+ anyExecutor { _, args ->
+ val target = args["player"] as Player
+ val advancement = args["advancement"] as Advancement
+
+ // Award all criteria for the advancement
+ val progress = target.getAdvancementProgress(advancement)
+ for (criteria in advancement.criteria) {
+ progress.awardCriteria(criteria)
+ }
+ }
+}
+
+Congratulations on making it to the end of the documentation! It's really long, but I did my best to make it the best (Bukkit/Spigot plugin) documentation in existence.
+My name is Jorel, commonly known by my Minecraft username Skepter. I started the CommandAPI in the summer holidays between my first and second year at university. On the 19th August, 2018 I made my first commit to the CommandAPI project - just a month and a day after Minecraft 1.13 was released.
+At the time, I just decided to call it "The 1.13 Command API" - it wasn't the catchiest name out there, but it sort of said what I wanted it to - it's a Command API for Minecraft 1.13, which was the update when the big overhaul to the command system was introduced.
+It all started as a simple idea that can be summarized in 3 bullet points:
+After the release of version 1.2, two days after the initial release, I received my first GitHub issue. This was quite a shock to me - version 1.2 only had 11 total downloads so it seemed odd that someone managed to stumble upon it despite the fact that I did nothing to promote the CommandAPI. Little did I know that that one issue was the main motivation to keep this API alive after its initial release.
+I would never have possible imagined in my wildest dreams that 2 years later, I would still be in contact with them and know that if I had not chosen to create this API, their server would not have survived beyond Minecraft 1.13, let alone Minecraft 1.15, two major Minecraft versions later.
+This project has been insane. Absolutely, utterly insane. At over 1000 commits and over 1,000,000 additions (that includes things such as lines of code, lines of generated HTML documentation etc.), I can say without a doubt that this is indeed my biggest project ever.
+Not only have I managed to deal with hundreds of GitHub issues, I've also had the opportunity to try all sorts of technologies that I could have only dreamt of using, such as:
+Anyway, I digress. I'd like to give credit to all of the people that have opened issues on the CommandAPI GitHub, for without these people, the CommandAPI would have only remained a shadow of what it is now. I'd like to give credit to the people that have starred the CommandAPI on its GitHub page, and all of the members of the CommandAPI Discord server.
+I would like to personally give thanks to the following people - these are people that have made a significant contribution to the project in terms of ideas or support early in the CommandAPI's development:
+I'd also like to give a special mention to the following people that have helped find bugs or have supported the project in some way early in the CommandAPI's development: aianlinb, Baka-Mitai, Checkium, Comeza, DerpNerb, DogeBogey, endrdragon, EnragedRabisu, i509VCB, KieranGateley, lifespan, Loapu, Marvmallow, MatrixTunnel, Ricky12Awesome, SHsuperCM, SpaceCheetah, The_Gavster, Tinkot, vladfrangu, zedwick.
+I'm well aware there are lots of users that have made significant contributions to the CommandAPI that aren't listed above! Over the past four years, hundreds of users have created GitHub issues, joined the CommandAPI Discord server and submitted pull requests to contribute to the CommandAPI.
+I'd like to personally give thanks to my ✨Special Contributors in the CommandAPI Discord server - these are users that have made an exceptionally significant contribution to the CommandAPI project in general, from helping new users get started with the CommandAPI, to contributing to the CommandAPI repository directly, to bringing a liveliness to the CommandAPI Discord server as a whole:
+The CommandAPI would not be where it is currently without the community that has been established over the years in the CommandAPI Discord server. As such, I'd like to give a special thanks to some of the most active CommandAPI Discord server members that make the community feel lively:
+I didn't really want to add a way for CommandAPI users to be able to contribute financially via donations because the CommandAPI is free for all and the cost of maintaining the CommandAPI is effectively negligible. That said, I'd like to give special thanks to those that have donated to the CommandAPI.
+I never really expected more than 5 or so people to use this API, so it was truly a pleasure to see everyone's responses, issues and suggestions that has made the CommandAPI what it is today.
+Thank you so much for using the CommandAPI!
+ +Aliases for commands can be added by using the withAliases()
method when registering a command. Aliases allow you to run the same command with a different 'name' from the original registered command name.
In this example, we register the command /getpos
that returns the command sender's location. We apply the aliases /getposition
, /getloc
, /getlocation
and /whereami
as well, using the withAliases()
method.
new CommandAPICommand("getpos")
+ // Declare your aliases
+ .withAliases("getposition", "getloc", "getlocation", "whereami")
+
+ // Declare your implementation
+ .executesEntity((entity, args) -> {
+ entity.sendMessage(String.format("You are at %d, %d, %d",
+ entity.getLocation().getBlockX(),
+ entity.getLocation().getBlockY(),
+ entity.getLocation().getBlockZ())
+ );
+ })
+ .executesCommandBlock((block, args) -> {
+ block.sendMessage(String.format("You are at %d, %d, %d",
+ block.getBlock().getLocation().getBlockX(),
+ block.getBlock().getLocation().getBlockY(),
+ block.getBlock().getLocation().getBlockZ())
+ );
+ })
+
+ // Register the command
+ .register();
+
+CommandAPICommand("getpos")
+ // Declare your aliases
+ .withAliases("getposition", "getloc", "getlocation", "whereami")
+
+ // Declare your implementation
+ .executesEntity(EntityCommandExecutor { entity, _ ->
+ val loc = entity.location
+ entity.sendMessage("You are at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ}")
+ })
+ .executesCommandBlock(CommandBlockCommandExecutor { block, _ ->
+ val loc = block.block.location
+ block.sendMessage("You are at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ}")
+ })
+
+ // Register the command
+ .register()
+
+commandAPICommand("getpos") {
+ // Declare your aliases
+ withAliases("getposition", "getloc", "getlocation", "whereami")
+
+ // Declare your implementation
+ entityExecutor { entity, _ ->
+ val loc = entity.location
+ entity.sendMessage("You are at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ}")
+ }
+ commandBlockExecutor { block, _ ->
+ val loc = block.block.location
+ block.sendMessage("You are at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ}")
+ }
+}
+
+This page outlines in detail the list of all annotations that the CommandAPI's annotation-based command system includes.
+@Command
("commandName")The @Command
annotation is used to declare a command. The parameter is the name of the command that will be registered.
@Command("warp")
+public class WarpCommand {
+
+@Alias({...})
The @Alias
annotation is used to declare a list of aliases for a command. The parameter is a list of aliases which can be used for the command.
@Command("teleport")
+@Alias({"tp", "tele"})
+public class TeleportCommand {
+
+@Permission("permissionNode")
The @Permission
annotation is used to add a permission node to a command. Users that want to run this command must have this permission. The parameter is the permission node required to run the command.
@Command("teleport")
+@Permission("myplugin.tp")
+class TeleportCommand {
+
+@NeedsOp
The @NeedsOp
annotation is used to indicate that a command needs to have operator privileges to run it. This annotation has no parameters.
@Command("teleport")
+@NeedsOp
+class TeleportCommand {
+
+@Help("Full description")
The @Help
annotation is used to add a help topic to a command. This annotation can take two forms:
A simple form which just uses a string which is used as the full description for a command:
+@Command("teleport")
+@Help("Teleports yourself to another location")
+class TeleportCommand {
+
+A form with two parameters value
and shortDescription
, to provide the full description (value
) and short description (shortDescription
) content for a command:
@Command("teleport")
+@Help(value = "Teleports yourself to another location", shortDescription = "TP to a location")
+class TeleportCommand {
+
+To use annotations on methods, methods must be static.
+@Default
The @Default
annotation indicates that the method is not a subcommand. This acts in a similar way to regular Bukkit commands. Commands with the @Default
annotation can be used to run the main code when the command named with the @Command
annotation is stated, such as the following:
@Default
+public static void warp(CommandSender sender) {
+ sender.sendMessage("--- Warp help ---");
+ sender.sendMessage("/warp - Show this help");
+ sender.sendMessage("/warp <warp> - Teleport to <warp>");
+ sender.sendMessage("/warp create <warpname> - Creates a warp at your current location");
+}
+
+The @Default
annotation does not mean that the command can't have arguments! Arguments can still be used and declared as shown:
@Default
+public static void warp(Player player, @AStringArgument String warpName) {
+ player.teleport(warps.get(warpName));
+}
+
+@Subcommand
The @Subcommand
simply tells the CommandAPI that the declared method is a subcommand. This acts in a similar way to the regular CommandAPI's .withSubcommand()
method. The subcommand annotation can take in a single string which is the name of the subcommand:
@Subcommand("create")
+@Permission("warps.create")
+public static void createWarp(Player player, @AStringArgument String warpName) {
+ warps.put(warpName, player.getLocation());
+}
+
+Or, it can take in a list of strings which represent the aliases that can also be used for the declared subcommand:
+@Subcommand({"teleport", "tp"})
+public static void teleport(Player player, @APlayerArgument OfflinePlayer target) {
+ if(target.isOnline() && target instanceof Player onlineTarget) {
+ player.teleport(onlineTarget);
+ }
+}
+
+@Permission
The @Permission
annotation can also be used on methods to indicate that a permission is required to execute a command.
@Subcommand("create")
+@Permission("warps.create")
+public static void createWarp(Player player, @AStringArgument String warpName) {
+ warps.put(warpName, player.getLocation());
+}
+
+@NeedsOp
The @NeedsOp
annotation can also be used on methods to indicate that the user must be an operator to run the command.
The annotations for arguments are really simple, there's just two things you need to know:
+To use an annotation argument, just add the letter A
(for 'annotation') at the beginning of it! For example:
\begin{align} +\texttt{StringArgument}&\xrightarrow{A}\texttt{@AStringArgument}\\ +\texttt{PlayerArgument}&\xrightarrow{A}\texttt{@APlayerArgument}\\ +\texttt{AdvancementArgument}&\xrightarrow{A}\texttt{@AAdvancementArgument}\\ +&\hspace{0.75em}\vdots +\end{align}
+For example, we use @AStringArgument
to indicate that this command takes a StringArgument
as its first parameter:
@Default
+public static void warp(Player player, @AStringArgument String warpName) {
+ player.teleport(warps.get(warpName));
+}
+
+warpName
.Certain argument annotations have extra parameters that can be supplied to provide additional customization:
+The following numerical arguments can take both a min
and max
value. Both of these are completely optional. This indicates the range of values (inclusive) that is valid for this argument. For example:
@Default
+public static void command(CommandSender sender,
+ @ADoubleArgument(min = 0.0, max = 10.0) double someDouble,
+ @AFloatArgument(min = 5.0f, max = 10.0f) float someFloat,
+ @AIntegerArgument(max = 100) int someInt,
+ @ALongArgument(min = -10) long someLong
+) {
+ // Command implementation here
+}
+
+Both the LiteralArgument
and MultiLiteralArgument
can be used. When these are used, the name of the variable assigned to the parameter is ignored and not used as the argument's name.
For the @ALiteralArgument
annotation, the parameter is the literal to be used for the command. For the @AMultiLiteralArgument
, the parameter can be an array of multiple literals to use:
@Default
+public static void command(CommandSender sender,
+ @ALiteralArgument("myliteral") String literal,
+ @AMultiLiteralArgument({"literal", "anotherliteral"}) String multipleLiterals
+) {
+ // Command implementation here
+}
+
+The LocationArgument
, Location2DArgument
, EntitySelectorArgument
and ScoreHolderArgument
can all take an extra parameter in their constructors. As a result, the annotation-equivalent of these arguments also allow you to provide the parameter in the annotation:
@Default
+public static void command(CommandSender sender,
+ @ALocationArgument(LocationType.BLOCK_POSITION) Location location,
+ @ALocation2DArgument(LocationType.PRECISE_POSITION) Location location2d,
+ @AEntitySelectorArgument.ManyEntities Collection<Entity> entities,
+ @AScoreHolderArgument.Multiple Collection<String> scoreHolders
+) {
+ // Command implementation here
+}
+
+
+ The CommandAPI also includes a very small lightweight annotation-based command framework. This works very differently compared to previous commands shown in this documentation and it is less feature-rich than registering commands using the other methods.
+In short, the CommandAPI's annotation-based system:
+++Developer's Note:
+Currently, the annotation framework is in its infancy, so any suggestions or improvements are heavily appreciated!
+
Developer's Note:
+As of the time of writing, annotation-based commands are not compatible with the Kotlin programming language! The CommandAPI does have the Kotlin DSL instead, which is leaner, cleaner and provides a much more Kotliny experience!
+Before we go into too much detail, let's take a look at an example of what this annotation framework looks like, and compare this to the existing method.
+Let's say we're writing a plugin with the capability to create warps to places on the server. To do this, we'll make a simple command /warp
, defined as follows:
/warp - Shows help
+/warp <warp> - Teleports a player to <warp>
+/warp create <name> - Creates a new warp <name> at the player's location
+
+Using the regular CommandAPI, this is one way we can create this command. In the code below, we use StringArguments to represent the warp names. To teleport to a warp, we also populate it with suggestions (deferred so it updates), and also use a subcommand to represent /warp create
:
Map<String, Location> warps = new HashMap<>();
+
+// /warp
+new CommandAPICommand("warp")
+ .executes((sender, args) -> {
+ sender.sendMessage("--- Warp help ---");
+ sender.sendMessage("/warp - Show this help");
+ sender.sendMessage("/warp <warp> - Teleport to <warp>");
+ sender.sendMessage("/warp create <warpname> - Creates a warp at your current location");
+ })
+ .register();
+
+// /warp <warp>
+new CommandAPICommand("warp")
+ .withArguments(new StringArgument("warp").replaceSuggestions(ArgumentSuggestions.strings(info ->
+ warps.keySet().toArray(new String[0])
+ )))
+ .executesPlayer((player, args) -> {
+ player.teleport(warps.get((String) args.get(0)));
+ })
+ .register();
+
+// /warp create <warpname>
+new CommandAPICommand("warp")
+ .withSubcommand(
+ new CommandAPICommand("create")
+ .withPermission("warps.create")
+ .withArguments(new StringArgument("warpname"))
+ .executesPlayer((player, args) -> {
+ warps.put((String) args.get(0), player.getLocation());
+ })
+ )
+ .register();
+
+Seems fairly straightforward, given everything else covered in this documentation. Now let's compare it to using annotations!
+I think it's best to show the example and the explain it afterwards:
+@Command("warp")
+public class WarpCommand {
+
+ // List of warp names and their locations
+ static Map<String, Location> warps = new HashMap<>();
+
+ @Default
+ public static void warp(CommandSender sender) {
+ sender.sendMessage("--- Warp help ---");
+ sender.sendMessage("/warp - Show this help");
+ sender.sendMessage("/warp <warp> - Teleport to <warp>");
+ sender.sendMessage("/warp create <warpname> - Creates a warp at your current location");
+ }
+
+ @Default
+ public static void warp(Player player, @AStringArgument String warpName) {
+ player.teleport(warps.get(warpName));
+ }
+
+ @Subcommand("create")
+ @Permission("warps.create")
+ public static void createWarp(Player player, @AStringArgument String warpName) {
+ warps.put(warpName, player.getLocation());
+ }
+
+}
+
+CommandAPI.registerCommand(WarpCommand.class);
+
+As we can see, the code certainly looks very different to the normal registration method. Let's take it apart piece by piece to see what exactly is going on here.
+@Command("warp")
+public class WarpCommand {
+
+Firstly, we declare our command warp
. To do this, we use the @Command
annotation and simply state the name of the command in the annotation. This annotation is attached to the class WarpCommand
, which indicates that the whole class WarpCommand
will be housing our command.
The annotation framework is designed in such a way that an entire command is represented by a single class. This provides a more modular approach to command declaration that allows you to easily contain the methods of a command in one location.
+@Default
+public static void warp(CommandSender sender) {
+ sender.sendMessage("--- Warp help ---");
+ sender.sendMessage("/warp - Show this help");
+ sender.sendMessage("/warp <warp> - Teleport to <warp>");
+ sender.sendMessage("/warp create <warpname> - Creates a warp at your current location");
+}
+
+Here, declare the main command implementation using the @Default
annotation. The @Default
annotation informs the CommandAPI that the method it is attached to does not have any subcommands. This is effectively the same as registering a regular command without using .withSubcommand()
.
Here, we simply write what happens when no arguments are run (i.e. the user just runs /warp
on its own). As such, we don't include any parameters to our method.
@Default
+public static void warp(Player player, @AStringArgument String warpName) {
+ player.teleport(warps.get(warpName));
+}
+
+We also have a second @Default
annotated method, which handles our /warp <warp>
command. Because this isn't a subcommand (the warp to teleport to is not a subcommand, it's an argument), we still using the @Default
annotation. In this method, we include an argument with this command by using the @AStringArgument
annotation. This argument uses the StringArgument
class, and the name of this argument is "warpName", which is extracted from the name of the variable. Simply put, the Annotation for an argument is A followed by the name of the argument. This is synonymous with using the following:
new StringArgument("warp")
+
+It's also very important to note the parameters for this method. The first parameter is a Player
object, which represents our command sender. The CommandAPI's annotation system uses the fact that the command sender is a Player
object and automatically ensures that anyone using the command must be a Player
. In other words, non-players (such as the console or command blocks), would be unable to execute this command.
The second argument is a String
object, which represents the result of our argument "warp". The CommandAPI's annotation system can also infer the return type of the argument that is provided to it (in this case, a StringArgument
will produce a String
) and will automatically cast and provide the result to that parameter.
@Subcommand("create")
+@Permission("warps.create")
+public static void createWarp(Player player, @AStringArgument String warpName) {
+ warps.put(warpName, player.getLocation());
+}
+
+Lastly, we declare a subcommand to allow us to run /warp create <name>
. To do this, we simply use the @Subcommand
annotation. In this example, we also apply a permission node that is required to run the command by using the @Permission
annotation. The rest is fairly straight forward - we declare an argument, in this case it's another StringArgument
, so we use @AStringArgument
and then declare everything else in a similar fashion to the default command executor.
Registering the command is fairly simple and is a one liner:
+CommandAPI.registerCommand(WarpCommand.class);
+
+This line can be placed in your onEnable()
or onLoad()
method like you were registering a normal command.
The angle argument is used to represent the yaw (horizontal) angle in degrees. The value returned from this argument range from -180.0 (inclusive) to 180 (exclusive), with -180.0 being due north:
+\begin{align} +-1&80.0 \\ +&\hspace{0.1em}N \\ +&\uparrow \\ +90.0\ W \leftarrow &\hspace{0.75em}\rightarrow E\ -90.0 \\ +&\downarrow \\ +&\hspace{0.2em}S \\ +&0.0 \\ +\end{align}
+The ~
notation can be used to specify a rotation relative to the executor's yaw angle.
The AxisArgument
class refers to the x, y and z axes. When used with the CommandAPI, it returns an EnumSet<Axis>
(You can view the documentation for EnumSet
here).
In Minecraft 1.16, they added the ability to refer to in-game biomes. The CommandAPI implements this using the BiomeArgument
. As expected, this returns Bukkit's Biome
enum when used.
When using the Biome
object, the CommandAPI will return null
if the specified Biome
could not be found, for example if a player submitted a biome from a client-side resourcepack. The CommandAPI does not return Biome.CUSTOM
from the BiomeArgument
.
Say you want to set the biome of the current chunk that a player is in. We can do this using the World.setBiome(x, y, z, biome)
method for a given world. We will use this command syntax to set the biome of our current chunk:
/setbiome <biome>
+
+And we can set the biome of the current chunk as expected:
+new CommandAPICommand("setbiome")
+ .withArguments(new BiomeArgument("biome"))
+ .executesPlayer((player, args) -> {
+ Biome biome = (Biome) args.get("biome");
+
+ Chunk chunk = player.getLocation().getChunk();
+ player.getWorld().setBiome(chunk.getX(), player.getLocation().getBlockY(), chunk.getZ(), biome);
+ })
+ .register();
+
+CommandAPICommand("setbiome")
+ .withArguments(BiomeArgument("biome"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val biome = args["biome"] as Biome
+
+ val chunk = player.location.chunk
+ player.world.setBiome(chunk.x, player.location.blockY, chunk.z, biome)
+ })
+ .register()
+
+commandAPICommand("setbiome") {
+ biomeArgument("biome")
+ playerExecutor { player, args ->
+ val biome = args["biome"] as Biome
+
+ val chunk = player.location.chunk
+ player.world.setBiome(chunk.x, player.location.blockY, chunk.z, biome)
+ }
+}
+
+The BiomeArgument
also supports returning a NamespacedKey
for custom biomes. This can be done by using the BiomeArgument.NamespacedKey
constructor instead of the normal BiomeArgument
constructor:
// Makes a BiomeArgument that returns a Biome
+new BiomeArgument("biome");
+
+// Makes a BiomeArgument that returns a NamespacedKey
+new BiomeArgument.NamespacedKey("biome");
+
+++ +Developer's Note:
+Spigot's support for custom biomes is really limited! If you have an example that lets you use custom biomes with namespaced keys, please open a GitHub issue, or reach out to us on Discord!
+
The BlockPredicateArgument
is used to represent a "test" for Minecraft blocks. This can consist of tags, such as the ones listed here on the MinecraftWiki, or individual blocks. If a block matches the tag or block, then the predicate is satisfied.
For example, if we were to use the predicate #leaves
, then the following blocks will be satisfied by that predicate: jungle_leaves
, oak_leaves
, spruce_leaves
, dark_oak_leaves
, acacia_leaves
, birch_leaves
.
When used, this argument must be casted to a Predicate<Block>
. As with other similar arguments with parameterized types, you can ignore Java's unchecked cast type safety warning.
Say you want to replace blocks in a given radius. To do this, we'll use the following command structure:
+/replace <radius> <fromBlock> <toBlock>
+
+Of course, we could simply use a BlockStateArgument
or even an ItemStackArgument
as our <fromBlock>
in order to get the material to replace, but the block predicate argument provides a test for a given block, which if satisfied, allows certain code to be executed.
First, we declare our arguments. We want to use the BlockPredicateArgument
since it also allows us to use Minecraft tags to identify blocks, as well as individual blocks. We then use BlockStateArgument
to set the block to a given type. The BlockStateArgument
also allows the user to provide any block data (e.g. contents of a chest or a stair's orientation).
Argument<?>[] arguments = new Argument<?>[] {
+ new IntegerArgument("radius"),
+ new BlockPredicateArgument("fromBlock"),
+ new BlockStateArgument("toBlock"),
+};
+
+val arguments = arrayOf<Argument<*>>(
+ IntegerArgument("radius"),
+ BlockPredicateArgument("fromBlock"),
+ BlockStateArgument("toBlock"),
+)
+
+We then register our /replace
command. First, we parse the arguments making sure to cast to Predicate<Block>
and BlockData
(and not BlockState
). After that, we use a few simple for loops to find the blocks within a radius sphere from the player.
In our most nested loop, we can then check if the block meets the requirements of our predicate. This is simply performed using predicate.test(block)
, and if satisfied, we can set the block's type.
Lastly, we register our command as normal using the register()
method.
new CommandAPICommand("replace")
+ .withArguments(arguments)
+ .executesPlayer((player, args) -> {
+
+ // Parse the arguments
+ int radius = (int) args.get("radius");
+ @SuppressWarnings("unchecked")
+ Predicate<Block> predicate = (Predicate<Block>) args.get("fromBlock");
+ BlockData blockData = (BlockData) args.get("toBlock");
+
+ // Find a (solid) sphere of blocks around the player with a given radius
+ Location center = player.getLocation();
+ for (int x = -radius; x <= radius; x++) {
+ for (int y = -radius; y <= radius; y++) {
+ for (int z = -radius; z <= radius; z++) {
+ if (Math.sqrt(((x * x) + (y * y) + (z * z))) <= radius) {
+ Block block = center.getWorld().getBlockAt(x + center.getBlockX(), y + center.getBlockY(), z + center.getBlockZ());
+
+ // If that block matches a block from the predicate, set it
+ if (predicate.test(block)) {
+ block.setType(blockData.getMaterial());
+ block.setBlockData(blockData);
+ }
+ }
+ }
+ }
+ }
+ return;
+ })
+ .register();
+
+CommandAPICommand("replace")
+ .withArguments(*arguments)
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+
+ // Parse the arguments
+ val radius = args["radius"] as Int
+ val predicate = args["fromBlock"] as Predicate<Block>
+ val blockData = args["toBlock"] as BlockData
+
+ // Find a (solid) sphere of blocks around the player with a given radius
+ val center = player.location // for (i in 1 until 11) { }
+ for (x in -radius until radius + 1) {
+ for (y in -radius until radius + 1) {
+ for (z in -radius until radius + 1) {
+ if (Math.sqrt((x * x + y * y + z * z).toDouble()) <= radius) {
+ val block = center.world.getBlockAt(x + center.blockX, y + center.blockY, z + center.blockZ)
+
+ // If that block matches a block from the predicate, set it
+ if (predicate.test(block)) {
+ block.type = blockData.material
+ block.blockData = blockData
+ }
+ }
+ }
+ }
+ }
+ })
+ .register()
+
+commandAPICommand("replace") {
+ arguments(*arguments)
+ playerExecutor { player, args ->
+ // Parse the arguments
+ val radius = args["radius"] as Int
+ val predicate = args["fromBlock"] as Predicate<Block>
+ val blockData = args["toBlock"] as BlockData
+
+ // Find a (solid) sphere of blocks around the player with a given radius
+ val center = player.location // for (i in 1 until 11) { }
+ for (x in -radius until radius + 1) {
+ for (y in -radius until radius + 1) {
+ for (z in -radius until radius + 1) {
+ if (Math.sqrt((x * x + y * y + z * z).toDouble()) <= radius) {
+ val block = center.world.getBlockAt(x + center.blockX, y + center.blockY, z + center.blockZ)
+
+ // If that block matches a block from the predicate, set it
+ if (predicate.test(block)) {
+ block.type = blockData.material
+ block.blockData = blockData
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+The BlockStateArgument
is used to represent data about blocks in the world. These refer to any blocks that have data or states, such as dispensers, signs, doors and pistons. The BlockStateArgument
creates a Bukkit BlockData
object when used.
++Developer's Note:
+Make sure to not confuse the cast type with
+BlockState
. The naming of this argument refers to the internal Minecraft vanilla argument naming convention - this argument casts toBlockData
and NOTBlockState
.
Say we want a simple command to set the block that you're looking at. We'll use the following command syntax:
+/set <block>
+
+And then we can simply set our block using setBlockData()
:
new CommandAPICommand("set")
+ .withArguments(new BlockStateArgument("block"))
+ .executesPlayer((player, args) -> {
+ BlockData blockdata = (BlockData) args.get("block");
+ Block targetBlock = player.getTargetBlockExact(256);
+
+ // Set the block, along with its data
+ targetBlock.setType(blockdata.getMaterial());
+ targetBlock.getState().setBlockData(blockdata);
+ })
+ .register();
+
+CommandAPICommand("set")
+ .withArguments(BlockStateArgument("block"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val blockdata = args["block"] as BlockData
+ val targetBlock = player.getTargetBlockExact(256)
+
+ // Set the block, along with its data
+ targetBlock?.type = blockdata.material
+ targetBlock?.state?.blockData = blockdata
+ })
+ .register()
+
+commandAPICommand("set") {
+ blockStateArgument("block")
+ playerExecutor { player, args ->
+ val blockdata = args["block"] as BlockData
+ val targetBlock = player.getTargetBlockExact(256)
+
+ // Set the block, along with its data
+ targetBlock?.type = blockdata.material
+ targetBlock?.state?.blockData = blockdata
+ }
+}
+
+++Developer's Note:
+The two following classes,
+AdventureChatComponentArgument
andAdventureChatArgument
depend on a Paper based server which has the Adventure library. If you use this class on a server without the Adventure library, it will throw aPaperAdventureNotFoundException
From Paper 1.16.5 build #473 onwards, Paper now includes Kyori's Adventure API. This library is a replacement of the BungeeCord chat API and has all of the same functionality as the BungeeCord chat API (and more!). The documentation for this API can be found here.
+Since this functions very similar to the Spigot chat arguments, this page won't reiterate everything about how it works, we'll just outline some examples of how to use these arguments instead.
+The AdventureChatColorArgument
class is used to represent a given chat color (e.g. red or green). This argument returns the NamedTextColor
object. If reset
is passed to this argument, this will return NamedTextColor.WHITE
.
Say we want to create a plugin to change the color of a player's username. We want to create a command of the following form:
+/namecolor <chatcolor>
+
+We then use the ChatColorArgument
to change the player's name color:
new CommandAPICommand("namecolor")
+ .withArguments(new AdventureChatColorArgument("chatcolor"))
+ .executesPlayer((player, args) -> {
+ NamedTextColor color = (NamedTextColor) args.get("chatcolor");
+ player.displayName(Component.text().color(color).append(Component.text(player.getName())).build());
+ })
+ .register();
+
+CommandAPICommand("namecolor")
+ .withArguments(AdventureChatColorArgument("chatcolor"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val color = args["chatcolor"] as NamedTextColor
+ player.displayName(Component.text().color(color).append(Component.text(player.name)).build())
+ })
+ .register()
+
+commandAPICommand("namecolor") {
+ chatColorArgument("chatcolor")
+ playerExecutor { player, args ->
+ val color = args["chatcolor"] as NamedTextColor
+ player.displayName(Component.text().color(color).append(Component.text(player.name)).build())
+ }
+}
+
+The AdventureChatComponentArgument
class accepts raw chat-based JSON as valid input, as declared here. This is converted into Adventure's Component
class.
In this example, we'll create a simple command which lets you show a book to a user. The syntax for our command is as follows:
+/showbook <target> <title> <author> <contents>
+
+We can construct a book using the Adventure API's Book.book(Component, Component, Component...)
method. In order to convert our strings into Component
objects, we use the Component.text(String)
method. Since Paper supports the Adventure API natively, we can then send this book to a player using the openBook(Book)
method:
new CommandAPICommand("showbook")
+ .withArguments(new PlayerArgument("target"))
+ .withArguments(new TextArgument("title"))
+ .withArguments(new StringArgument("author"))
+ .withArguments(new AdventureChatComponentArgument("contents"))
+ .executes((sender, args) -> {
+ Player target = (Player) args.get("target");
+ String title = (String) args.get("title");
+ String author = (String) args.get("author");
+ Component content = (Component) args.get("contents");
+
+ // Create a book and show it to the user (Requires Paper)
+ Book mybook = Book.book(Component.text(title), Component.text(author), content);
+ target.openBook(mybook);
+ })
+ .register();
+
+CommandAPICommand("showbook")
+ .withArguments(PlayerArgument("target"))
+ .withArguments(TextArgument("title"))
+ .withArguments(StringArgument("author"))
+ .withArguments(AdventureChatComponentArgument("contents"))
+ .executes(CommandExecutor { _, args ->
+ val target = args["target"] as Player
+ val title = args["title"] as String
+ val author = args["author"] as String
+ val content = args["contents"] as Component
+
+ // Create a book and show it to the user (Requires Paper)
+ val mybook = Book.book(Component.text(title), Component.text(author), content)
+ target.openBook(mybook)
+ })
+ .register()
+
+commandAPICommand("showbook") {
+ playerArgument("target")
+ textArgument("title")
+ stringArgument("author")
+ adventureChatComponentArgument("contents")
+ anyExecutor { _, args ->
+ val target = args["target"] as Player
+ val title = args["title"] as String
+ val author = args["author"] as String
+ val content = args["contents"] as Component
+
+ // Create a book and show it to the user (Requires Paper)
+ val mybook = Book.book(Component.text(title), Component.text(author), content)
+ target.openBook(mybook)
+ }
+}
+
+The AdventureChatArgument
class is the equivalent Adventure API class for the ChatArgument
- it represents infinitely long strings similar to the GreedyStringArgument
and allows entity selectors such as @e
, @p
and so on. The AdventureChatArgument
returns a Component
, similar to the AdventureChatComponentArgument
.
We'll take the same example from the ChatArgument
class, but using the AdventureChatArgument
instead - We want to create a personalized message broadcasted to all users using a chat component that allows entity selectors. For this command, we want the following syntax:
/pbroadcast <message>
+
+In order to broadcast an Adventure Component
to all players on the server, we have to use Paper's broadcast(Component, String)
method. This method requires a permission node which all players must have in order to receive the broadcasted message. By default, Bukkit-based servers (Spigot and Paper) use the bukkit.broadcast.user
permission, which is described here:
new CommandAPICommand("pbroadcast")
+ .withArguments(new AdventureChatArgument("message"))
+ .executes((sender, args) -> {
+ Component message = (Component) args.get("message");
+
+ // Broadcast the message to everyone with broadcast permissions.
+ Bukkit.getServer().broadcast(message, Server.BROADCAST_CHANNEL_USERS);
+ Bukkit.getServer().broadcast(message);
+ })
+ .register();
+
+CommandAPICommand("pbroadcast")
+ .withArguments(AdventureChatArgument("message"))
+ .executes(CommandExecutor { _, args ->
+ val message = args["message"] as Component
+
+ // Broadcast the message to everyone with broadcast permissions.
+ Bukkit.getServer().broadcast(message, Server.BROADCAST_CHANNEL_USERS)
+ Bukkit.getServer().broadcast(message)
+ })
+ .register()
+
+commandAPICommand("pbroadcast") {
+ adventureChatArgument("message")
+ anyExecutor { _, args ->
+ val message = args["message"] as Component
+
+ // Broadcast the message to everyone with broadcast permissions.
+ Bukkit.getServer().broadcast(message, Server.BROADCAST_CHANNEL_USERS)
+ Bukkit.getServer().broadcast(message)
+ }
+}
+
+The ChatColorArgument
class is used to represent a given chat color (e.g. red or green). This argument returns the ChatColor
object.
Say we want to create a plugin to change the color of a player's username. We want to create a command of the following form:
+/namecolor <chatcolor>
+
+We then use the ChatColorArgument
to change the player's name color:
new CommandAPICommand("namecolor")
+ .withArguments(new ChatColorArgument("chatcolor"))
+ .executesPlayer((player, args) -> {
+ ChatColor color = (ChatColor) args.get("chatcolor");
+ player.setDisplayName(color + player.getName());
+ })
+ .register();
+
+CommandAPICommand("namecolor")
+ .withArguments(ChatColorArgument("chatColor"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val color = args["chatColor"] as ChatColor
+ player.setDisplayName("$color${player.name}")
+ })
+ .register()
+
+commandAPICommand("namecolor") {
+ chatColorArgument("chatcolor")
+ playerExecutor { player, args ->
+ val color = args["chatcolor"] as ChatColor
+ player.setDisplayName("$color${player.name}")
+ }
+}
+
+++Developer's Note:
+The two following classes,
+ChatComponentArgument
andChatArgument
depend on a Spigot based server. This means that these arguments will not work on a non-Spigot based server, such as CraftBukkit. If you use this class on a non-Spigot based server, it will throw aSpigotNotFoundException
The ChatComponentArgument
class accepts raw chat-based JSON as valid input. Despite being regular JSON, it must conform to the standard declared here, which consists of JSON that has a limited subset of specific keys (In other words, you can have a JSON object that has the key text
, but not one that has the key blah
).
This is converted into Spigot's BaseComponent[]
, which can be used for the following:
Broadcasting messages to all players on the server using:
+Bukkit.getServer().spigot().broadcast(BaseComponent[]);
+
+Adding and setting pages to books using BookMeta
:
BookMeta meta = // ...
+meta.spigot().setPages(BaseComponent[]);
+
+Sending messages to Player
objects:
Player player = // ...
+player.spigot().sendMessage(BaseComponent[]);
+
+Sending messages to CommandSender
objects:
CommandSender sender = // ...
+sender.spigot().sendMessage(BaseComponent[]);
+
+Say we want to generate a book using raw JSON. For this example, we'll use the following JSON (generated from minecraftjson.com) to generate our book:
+["", {
+ "text": "Once upon a time, there was a guy call "
+}, {
+ "text": "Skepter",
+ "color": "light_purple",
+ "hoverEvent": {
+ "action": "show_entity",
+ "value": "Skepter"
+ }
+}, {
+ "text": " and he created the "
+}, {
+ "text": "CommandAPI",
+ "underlined": true,
+ "clickEvent": {
+ "action": "open_url",
+ "value": "https://github.com/JorelAli/CommandAPI"
+ }
+}]
+
+Since we're writing a book, we must ensure that all quotes have been escaped. This can also be performed on the minecraftjson.com website by selecting "book":
+["[\"\",{\"text\":\"Once upon a time, there was a guy call \"},{\"text\":\"Skepter\",\"color\":\"light_purple\",\"hoverEvent\":{\"action\":\"show_entity\",\"value\":\"Skepter\"}},{\"text\":\" and he created the \"},{\"text\":\"CommandAPI\",\"underlined\":true,\"clickEvent\":{\"action\":\"open_url\",\"value\":\"https://github.com/JorelAli/CommandAPI\"}}]"]
+
+Now let's define our command. Since book text is typically very large - too large to be entered into a chat, we'll make a command block compatible command by providing a player parameter:
+/makebook <player> <contents>
+
+Now we can create our book command. We use the player as the main target by using their name for the author field, as well as their inventory to place the book. We finally construct our book using the .setPages(BaseComponent[])
method:
new CommandAPICommand("makebook")
+ .withArguments(new PlayerArgument("player"))
+ .withArguments(new ChatComponentArgument("contents"))
+ .executes((sender, args) -> {
+ Player player = (Player) args.get("player");
+ BaseComponent[] arr = (BaseComponent[]) args.get("contents");
+
+ // Create book
+ ItemStack is = new ItemStack(Material.WRITTEN_BOOK);
+ BookMeta meta = (BookMeta) is.getItemMeta();
+ meta.setTitle("Custom Book");
+ meta.setAuthor(player.getName());
+ meta.spigot().setPages(arr);
+ is.setItemMeta(meta);
+
+ // Give player the book
+ player.getInventory().addItem(is);
+ })
+ .register();
+
+CommandAPICommand("makebook")
+ .withArguments(PlayerArgument("player"))
+ .withArguments(ChatComponentArgument("contents"))
+ .executes(CommandExecutor { _, args ->
+ val player = args["player"] as Player
+ val arr = args["contents"] as Array<BaseComponent>
+
+ // Create book
+ val item = ItemStack(Material.WRITTEN_BOOK)
+ val meta = item.itemMeta as BookMeta
+ meta.title = "Custom Book"
+ meta.author = player.name
+ meta.spigot().setPages(arr)
+ item.itemMeta = meta
+
+ // Give player the book
+ player.inventory.addItem(item)
+ })
+ .register()
+
+commandAPICommand("makebook") {
+ playerArgument("player")
+ chatComponentArgument("contents")
+ anyExecutor { _, args ->
+ val player = args["player"] as Player
+ val array = args["contents"] as Array<BaseComponent>
+
+ // Create book
+ val item = ItemStack(Material.WRITTEN_BOOK)
+ val meta = item.itemMeta as BookMeta
+ meta.title = "Custom Book"
+ meta.author = player.name
+ meta.spigot().setPages(array)
+ item.itemMeta = meta
+
+ // Give player the book
+ player.inventory.addItem(item)
+ }
+}
+
+++Note:
+The
+ChatArgument
class is an argument similar to theGreedyStringArgument
, in the sense that it has no terminator and must be defined at the end of yourList
of arguments. For more information on this, please read the section on Greedy arguments.
The ChatArgument
is identical to the GreedyStringArgument
, with the added functionality of enabling entity selectors, such as @e
, @p
and so on. The ChatArgument
also returns a BaseComponent[]
, similar to the ChatComponentArgument
.
Say we wanted to broadcast a "personalized" message to players on the server. By "personalized", we mean a command which changes its output depending on who we are sending the output to. Simply put, we want a command of the following syntax:
+/pbroadcast <message>
+
+Say we're on a server with 2 players: Bob and Michael. If I were to use the following command:
+/pbroadcast Hello @p
+
+Bob would receive the message "Hello Bob", whereas Michael would receive the message "Hello Michael". We can use the ChatArgument
to create this "personalized" broadcast:
new CommandAPICommand("pbroadcast")
+ .withArguments(new ChatArgument("message"))
+ .executes((sender, args) -> {
+ BaseComponent[] message = (BaseComponent[]) args.get("message");
+
+ // Broadcast the message to everyone on the server
+ Bukkit.getServer().spigot().broadcast(message);
+ })
+ .register();
+
+CommandAPICommand("pbroadcast")
+ .withArguments(ChatArgument("message"))
+ .executes(CommandExecutor { _, args ->
+ val message = args["message"] as Array<BaseComponent>
+
+ // Broadcast the message to everyone on the server
+ Bukkit.getServer().spigot().broadcast(*message)
+ })
+ .register()
+
+commandAPICommand("pbroadcast") {
+ chatArgument("message")
+ anyExecutor { _, args ->
+ val message = args["message"] as Array<BaseComponent>
+
+ // Broadcast the message to everyone on the server
+ Bukkit.getServer().spigot().broadcast(*message)
+ }
+}
+
+The CommandAPI provides a number of ways to interact with chat formatting in Minecraft. These are the following:
+@a
and @r
The CommandAPI implements ChatColor, Chat and ChatComponent in two separate ways: Spigot-compatible and Adventure-compatible. The differences between these and how to use them are described in their own relevant pages.
+The CommandAPI also supports Minecraft 1.19's chat preview feature. To use Minecraft 1.19's chat preview feature, information on that can be found in Chat preview.
+ +Command arguments allows users to provide an executable server command. The CommandArgument
class lets you specify:
Using the CommandArgument
will return a CommandResult
, which contains a Bukkit Command
instance representing the command to be executed, and a String[]
of command arguments.
The CommandResult
record contains the following methods:
public record CommandResult {
+ Command command();
+ String[] args();
+
+ boolean execute(CommandSender target);
+}
+
+These methods can be used to retrieve information about the command that was provided by the user:
+Command command();
+
+command()
returns the Bukkit Command
instance that the user provided. For example, if a player provided /mycommand hello world
, then command()
will represent the /mycommand
command.
String[] args();
+
+args()
returns an array of string argument inputs that were provided to the command. For example, if a player provided /mycommand hello world
, then args()
will be the following:
[ "hello", "world" ]
+
+boolean execute(CommandSender target);
+
+execute(CommandSender)
runs the Bukkit Command
using the arguments contained in the CommandResult
as the given CommandSender
. It returns true if the command dispatch succeeded, and false if it failed. Using this method is equivalent to running the following:
result.command().execute(target, result.command().getLabel(), result.args());
+
+Arbitrary commands let the user enter any command that they have permission to execute. To use arbitrary commands, you just need to use the CommandArgument
normally.
We want to create a /sudo
command which lets you execute a command as another online player.
To do this, we want to use the following command syntax:
+/sudo <target> <command>
+
+In this example, we want to be able to run any arbitrary command, so we will simply use the CommandArgument
on its own (without using suggestions). Using the CommandArgument
generates a CommandResult
and we can use the .command()
and .args()
methods above to access the command and arguments. We can make use of the Command.execute()
method to execute our command and use the target player as the command sender.
new CommandAPICommand("sudo")
+ .withArguments(new PlayerArgument("target"))
+ .withArguments(new CommandArgument("command"))
+ .executes((sender, args) -> {
+ Player target = (Player) args.get("target");
+ CommandResult command = (CommandResult) args.get("command");
+
+ command.execute(target);
+ })
+ .register();
+
+CommandAPICommand("sudo")
+ .withArguments(PlayerArgument("target"))
+ .withArguments(CommandArgument("command"))
+ .executes(CommandExecutor { _, args ->
+ val target = args["target"] as Player
+ val command = args["command"] as CommandResult
+
+ command.execute(target)
+ })
+ .register()
+
+commandAPICommand("sudo") {
+ playerArgument("target")
+ commandArgument("command")
+ anyExecutor { _, args ->
+ val target = args["target"] as Player
+ val command = args["command"] as CommandResult
+
+ command.execute(target)
+ }
+}
+
+Restricted commands allows you to restrict what commands a user is allowed to submit in the CommandArgument
. Commands can be restricted by replacing the CommandArgument
's suggestions using the replaceSuggestions()
method. For better fine-tuning of what commands a user can submit, commands can also be restricted by using suggestion branches.
To demonstrate restricting commands, let's create a command argument that allows players to enter one of the following commands:
+/tp <player> <target>
+/give <player> <item> <amount>
+
+Let's also add a restriction that the player can only use diamonds or dirt for the /give
command, and they can only specify an amount if they selected dirt. Overall, our command argument should allow players to follow this path:
\begin{gather} +\texttt{(start)}\\ +\swarrow\hspace{2cm}\searrow\\ +\swarrow\hspace{3.4cm}\searrow\\ +\texttt{tp}\hspace{4cm}\texttt{give}\\ +\swarrow\hspace{6cm}\searrow\\ +\texttt{player}\hspace{6cm}\texttt{player}\\ +\swarrow\hspace{7cm}\swarrow\hspace{2cm}\searrow\\ +\texttt{target}\hspace{5cm}\texttt{diamond}\hspace{3cm}\texttt{dirt}\\ +\hspace{6.7cm}\texttt{minecraft:diamond}\hspace{3cm}\texttt{minecraft:dirt}\\ +\hspace{7.5cm}\hspace{4cm}\downarrow\\ +\hspace{7.5cm}\hspace{4cm}\texttt{(amount)}\\ +\end{gather}
+In our diagram above, we have two main branches: /tp
and /give
. The /tp
branch has player
followed by target
, and the /give
branch has player
and then that branches off into two new sections.
We can implement our /tp
branch using the SuggestionsBranch.suggest()
method, then provide argument suggestions for our options. In this case, we have tp
and then a list of online players. We include the list of online players twice, because we need suggestions for <player>
as well as <target>
:
SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("tp"),
+ ArgumentSuggestions.strings(info -> Bukkit.getOnlinePlayers().stream().map(Player::getName).toArray(String[]::new)),
+ ArgumentSuggestions.strings(info -> Bukkit.getOnlinePlayers().stream().map(Player::getName).toArray(String[]::new))
+)
+
+SuggestionsBranch.suggest<CommandSender>(
+ ArgumentSuggestions.strings("tp"),
+ ArgumentSuggestions.strings { _ -> Bukkit.getOnlinePlayers().map{ it.name }.toTypedArray() },
+ ArgumentSuggestions.strings { _ -> Bukkit.getOnlinePlayers().map{ it.name }.toTypedArray() }
+)
+
+For the /give
branch, we can use a similar thing, but we need to tell the CommandArgument that the /give
command branches into "diamond" and "dirt" suggestions. We can do this by using the .branch()
method to add a new nested list of suggestions:
SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("give"),
+ ArgumentSuggestions.strings(info -> Bukkit.getOnlinePlayers().stream().map(Player::getName).toArray(String[]::new))
+).branch(
+ SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("diamond", "minecraft:diamond"),
+ ArgumentSuggestions.empty()
+ ),
+ SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("dirt", "minecraft:dirt"),
+ null,
+ ArgumentSuggestions.empty()
+ )
+)
+
+SuggestionsBranch.suggest<CommandSender>(
+ ArgumentSuggestions.strings("give"),
+ ArgumentSuggestions.strings { _ -> Bukkit.getOnlinePlayers().map{ it.name }.toTypedArray() }
+).branch(
+ SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("diamond", "minecraft:diamond"),
+ ArgumentSuggestions.empty()
+ ),
+ SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("dirt", "minecraft:dirt"),
+ null,
+ ArgumentSuggestions.empty()
+ )
+)
+
+Adding everything together, we get this fully completed CommandArgument:
+new CommandArgument("command")
+ .branchSuggestions(
+ SuggestionsBranch.<CommandSender>suggest(
+ ArgumentSuggestions.strings("give"),
+ ArgumentSuggestions.strings(info -> Bukkit.getOnlinePlayers().stream().map(Player::getName).toArray(String[]::new))
+ ).branch(
+ SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("diamond", "minecraft:diamond"),
+ ArgumentSuggestions.empty()
+ ),
+ SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("dirt", "minecraft:dirt"),
+ null,
+ ArgumentSuggestions.empty()
+ )
+ ),
+ SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("tp"),
+ ArgumentSuggestions.strings(info -> Bukkit.getOnlinePlayers().stream().map(Player::getName).toArray(String[]::new)),
+ ArgumentSuggestions.strings(info -> Bukkit.getOnlinePlayers().stream().map(Player::getName).toArray(String[]::new))
+ )
+ );
+
+CommandArgument("command")
+ .branchSuggestions(
+ SuggestionsBranch.suggest<CommandSender>(
+ ArgumentSuggestions.strings("give"),
+ ArgumentSuggestions.strings { _ -> Bukkit.getOnlinePlayers().map{ it.name }.toTypedArray() }
+ ).branch(
+ SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("diamond", "minecraft:diamond"),
+ ArgumentSuggestions.empty()
+ ),
+ SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("dirt", "minecraft:dirt"),
+ null,
+ ArgumentSuggestions.empty()
+ )
+ ),
+ SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("tp"),
+ ArgumentSuggestions.strings { _ -> Bukkit.getOnlinePlayers().map{ it.name }.toTypedArray() },
+ ArgumentSuggestions.strings { _ -> Bukkit.getOnlinePlayers().map{ it.name }.toTypedArray() }
+ )
+ )
+
+In the above example about restricted commands, we used null
and ArgumentSuggestions.empty()
in our SuggestionsBranch.suggest()
method. These special suggestions have specific effects when used in suggestions for the CommandArgument
.
Null suggestions ensure that the suggestions at the current position will not be overridden. In the case of the CommandArgument
, this means that the default command suggestions will be provided. For example, if we have the following null
entry in our suggestions, users are allowed to enter a value if they choose to do so, meaning that the examples below are all valid:
SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("give"),
+ null,
+ ArgumentSuggestions.empty()
+)
+
+/give dirt
+/give diamond
+/give apple
+
+Ending the command argument with nothing is also equivalent to using null
, for example the following suggestion branch allows any of the following commands:
SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("give"),
+ ArgumentSuggestions.strings("dirt", "minecraft:dirt")
+)
+
+/give dirt
+/give dirt 10
+/give dirt 10 name:Hello
+
+Empty suggestions that are provided using ArgumentSuggestions.empty()
tell the CommandArgument
to stop accepting further suggestions. This "ends" the command. Using the following example, this allows the user to enter /give diamond
and only /give diamond
- users cannot enter any other commands.
SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("give"),
+ ArgumentSuggestions.strings("diamond", "minecraft:diamond"),
+ ArgumentSuggestions.empty()
+)
+
+These commands are valid:
+/give diamond
+/give minecraft:diamond
+
+These commands are not valid:
+/give
+/give diamond 10
+
+
+ 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 a LocationArgument
, 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.
+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 a CustomArgument<Player, ...>
, then when parsing the arguments for the command, you would cast it to a Player
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 a StringArgument
, you'd have CustomArgument<..., String>
.
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();
+ CommandArguments 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 a Player
, but can also be a console command sender if using a Paper server.
CommandArguments previousArgs();
+
+previousArgs()
represents the 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, the input()
would return "hello"
.
B currentInput();
+
+currentInput()
represents the current input, as parsed by the base argument. For example, if your base argument was an IntegerArgument
, the return type of currentInput()
would be an int
.
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("world")).getSpawnLocation());
+ })
+ .register();
+
+CommandAPICommand("tpworld")
+ .withArguments(worldArgument("world"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ player.teleport((args["world"] as World).spawnLocation)
+ })
+ .register()
+
+commandAPICommand("tpworld") {
+ worldArgument("world") // This method is actually also built into the Kotlin DSL
+ playerExecutor { player, args ->
+ player.teleport((args["world"] 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.
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 |
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();
+
+The EnchantmentArgument
class lets users input a specific enchantment. As you would expect, the cast type is Bukkit's Enchantment
class.
Say we want to give a player an enchantment on the item that the player is currently holding. We will use the following command syntax:
+/enchantitem <enchantment> <level>
+
+Since most enchantment levels range between 1 and 5, we will also make use of the IntegerArgument
to restrict the level of the enchantment by usng its range constructor.
new CommandAPICommand("enchantitem")
+ .withArguments(new EnchantmentArgument("enchantment"))
+ .withArguments(new IntegerArgument("level", 1, 5))
+ .executesPlayer((player, args) -> {
+ Enchantment enchantment = (Enchantment) args.get("enchantment");
+ int level = (int) args.get("level");
+
+ // Add the enchantment
+ player.getInventory().getItemInMainHand().addEnchantment(enchantment, level);
+ })
+ .register();
+
+CommandAPICommand("enchantitem")
+ .withArguments(EnchantmentArgument("enchantment"))
+ .withArguments(IntegerArgument("level", 1, 5))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val enchantment = args["enchantment"] as Enchantment
+ val level = args["level"] as Int
+
+ // Add the enchantment
+ player.inventory.itemInMainHand.addEnchantment(enchantment, level)
+ })
+ .register()
+
+commandAPICommand("enchantitem") {
+ enchantmentArgument("enchantment")
+ integerArgument("level", 1, 5)
+ playerExecutor { player, args ->
+ val enchantment = args["enchantment"] as Enchantment
+ val level = args["level"] as Int
+
+ // Add the enchantment
+ player.inventory.itemInMainHand.addEnchantment(enchantment, level)
+ }
+}
+
+Minecraft's target selectors (e.g. @a
or @e
) are implemented using the subclasses of the EntitySelectorArgument
class. This allows you to select specific entities based on certain attributes.
There are four EntitySelectorArgument
subclasses that determine what type of data to return:
EntitySelectorArgument.OneEntity
- A single entity, which returns a Entity
object.EntitySelectorArgument.ManyEntities
- A collection of many entities, which returns a Collection<Entity>
object.EntitySelectorArgument.OnePlayer
- A single player, which returns a Player
object.EntitySelectorArgument.ManyPlayers
- A collection of players, which returns a Collection<Player>
object.The return type is the type to be cast when retrieved from the CommandArguments args
in the command declaration.
Say we want a command to remove certain types of entities. Typically, this would be implemented using a simple command like:
+/remove <player>
+/remove <mob type>
+/remove <radius>
+
+Instead, we can combine all of these into one by using the EntitySelectorArgument
. We want to be able to target multiple entities at a time, so we want to use the EntitySelectorArgument.ManyEntities
constructor. We can simply retrieve the Collection<Entity>
from this argument and iteratively remove each entity:
new CommandAPICommand("remove")
+ // Using a collective entity selector to select multiple entities
+ .withArguments(new EntitySelectorArgument.ManyEntities("entities"))
+ .executes((sender, args) -> {
+ // Parse the argument as a collection of entities (as stated above in the documentation)
+ @SuppressWarnings("unchecked")
+ Collection<Entity> entities = (Collection<Entity>) args.get("entities");
+
+ sender.sendMessage("Removed " + entities.size() + " entities");
+ for (Entity e : entities) {
+ e.remove();
+ }
+ })
+ .register();
+
+CommandAPICommand("remove")
+ // Using a collective entity selector to select multiple entities
+ .withArguments(EntitySelectorArgument.ManyEntities("entities"))
+ .executes(CommandExecutor { sender, args ->
+ // Parse the argument as a collection of entities (as stated above in the documentation)
+ val entities = args["entities"] as Collection<Entity>
+
+ sender.sendMessage("Removed ${entities.size} entities")
+ for (e in entities) {
+ e.remove()
+ }
+ })
+ .register()
+
+commandAPICommand("remove") {
+ // Using a collective entity selector to select multiple entities
+ entitySelectorArgumentManyEntities("entities")
+ anyExecutor { sender, args ->
+ // Parse the argument as a collection of entities (as stated above in the documentation)
+ val entities = args["entities"] as Collection<Entity>
+
+ sender.sendMessage("Removed ${entities.size} entities")
+ for (e in entities) {
+ e.remove()
+ }
+ }
+}
+
+We could then use this to target specific entities, for example:
+To remove all cows:
+/remove @e[type=cow]
+
+To remove the 10 furthest pigs from the command sender:
+/remove @e[type=pig,limit=10,sort=furthest]
+
+The PlayerArgument
class is very similar (almost identical) to EntitySelectorArgument.OnePlayer
. It returns a Player
object and requires the player to be online.
++Developer's Note:
+The
+PlayerArgument
internally uses theGameProfile
class from Mojang's authlib, which means that this argument has a slight performance overhead compared to usingEntitySelectorArgument.OnePlayer
When registering a PlayerArgument
you might notice that it includes Entity Selectors
(@a
, @e
, @r
, etc.). If you want to avoid those, you can use argument suggestions to only suggest the player names. For this example, let us create a /warp command:
/warp <player>
+
+To get a PlayerArgument
which only suggests the actual names, we can define it like this:
Argument<?> noSelectorSuggestions = new PlayerArgument("target")
+ .replaceSafeSuggestions(SafeSuggestions.suggest(info ->
+ Bukkit.getOnlinePlayers().toArray(new Player[0])
+ ));
+
+val noSelectorSuggestions = PlayerArgument("target")
+ .replaceSafeSuggestions(SafeSuggestions.suggest {
+ Bukkit.getOnlinePlayers().toTypedArray()
+ })
+
+Now we can define the rest of the command and include our suggestion inside of it like this:
+new CommandAPICommand("warp")
+ .withArguments(noSelectorSuggestions)
+ .executesPlayer((player, args) -> {
+ Player target = (Player) args.get("target");
+ player.teleport(target);
+ })
+ .register();
+
+CommandAPICommand("warp")
+ .withArguments(noSelectorSuggestions)
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val target = args["target"] as Player
+ player.teleport(target)
+ })
+ .register()
+
+And there we have it! One thing to note is that entity selectors are still a valid input, they are just not included in the suggestions. +
+The OfflinePlayerArgument
class is identical to the PlayerArgument
class, but instead of returning a Player
object, it returns an OfflinePlayer
object. Internally, this argument makes calls to Mojang servers (via Mojang's authlib), meaning it can be slightly slower than alternative methods (such as using a StringArgument
and suggesting a list of existing offline players).
The OfflinePlayerArgument
should be able to retrieve players that have never joined the server before.
The EntityTypeArgument
class is used to retrieve a type of entity as defined in the EntityType
enum. In other words, this is an entity type, for example a pig or a zombie.
Say we want a command to spawn a specific type of entity, similar to the /summon
command in Vanilla Minecraft, with the addition of specifying how many entities to spawn. We want to create a command of the following form:
/spawnmob <entity> <amount>
+
+Since we're trying to specify an entity type, we will use the EntityTypeArgument
as our argument type for <entity>
. We combine this with the IntegerArgument
class with a specified range of \( 1 \le \textit{amount} \le 100 \):
new CommandAPICommand("spawnmob")
+ .withArguments(new EntityTypeArgument("entity"))
+ .withArguments(new IntegerArgument("amount", 1, 100)) // Prevent spawning too many entities
+ .executesPlayer((Player player, CommandArguments args) -> {
+ for (int i = 0; i < (int) args.get("amount"); i++) {
+ player.getWorld().spawnEntity(player.getLocation(), (EntityType) args.get("entity"));
+ }
+ })
+ .register();
+
+CommandAPICommand("spawnmob")
+ .withArguments(EntityTypeArgument("entity"))
+ .withArguments(IntegerArgument("amount", 1, 100)) // Prevent spawning too many entities
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ for (i in 0 until args["amount"] as Int) {
+ player.world.spawnEntity(player.location, args["entity"] as EntityType)
+ }
+ })
+ .register()
+
+commandAPICommand("spawnmob") {
+ entityTypeArgument("entity")
+ integerArgument("amount", 1, 100) // Prevent spawning too many entities
+ playerExecutor { player, args ->
+ for (i in 0 until args["amount"] as Int) {
+ player.world.spawnEntity(player.location, args["entity"] as EntityType)
+ }
+ }
+}
+
+Note how in this example above, we have to explicitly state Player player, CommandArguments args
. This is due to a limitation of Java's type inference system which is discussed here.
The FunctionArgument
class is used to represent a function or a tag in Minecraft. When retrieving an instance of the argument, it will return a FunctionWrapper[]
, where each FunctionWrapper
consists of a Minecraft function.
Therefore, if a user supplies a single function, the FunctionWrapper[]
will be of size 1, and if the user supplies a tag which can consist of multiple functions, the FunctionWrapper[]
will consist of the array of functions as declared by that tag.
Since it's a little difficult to demonstrate a custom use for the FunctionArgument
, we will show how you can implement Vanilla Minecraft's /function
command. In this example, we want a command that uses the following syntax:
/runfunction <function>
+
+When provided with a function, it will execute that function. If instead a tag is provided, it will execute that tag (i.e. execute all functions declared in that tag).
+new CommandAPICommand("runfunction")
+ .withArguments(new FunctionArgument("function"))
+ .executes((sender, args) -> {
+ FunctionWrapper[] functions = (FunctionWrapper[]) args.get("function");
+
+ // Run all functions in our FunctionWrapper[]
+ for (FunctionWrapper function : functions) {
+ function.run();
+ }
+ })
+ .register();
+
+CommandAPICommand("runfunction")
+ .withArguments(FunctionArgument("function"))
+ .executes(CommandExecutor { _, args ->
+ val functions = args["function"] as Array<FunctionWrapper>
+
+ // Run all functions in our FunctionWrapper[]
+ for (function in functions) {
+ function.run()
+ }
+ })
+ .register()
+
+commandAPICommand("runfunction") {
+ functionArgument("function")
+ anyExecutor { _, args ->
+ val functions = args["function"] as Array<FunctionWrapper>
+
+ // Run all functions in our FunctionWrapper[]
+ for (function in functions) {
+ function.run()
+ }
+ }
+}
+
+The ItemStackArgument
class represents in-game items. As expected, this should be casted to Bukkit's ItemStack
object. The ItemStack
which is returned by the ItemStackArgument
always has a size of 1.
Say we want to create a command that gives you items. For this command, we will use the following syntax:
+/item <itemstack>
+
+With this syntax, we can easily create our command:
+new CommandAPICommand("item")
+ .withArguments(new ItemStackArgument("itemStack"))
+ .executesPlayer((player, args) -> {
+ player.getInventory().addItem((ItemStack) args.get("itemStack"));
+ })
+ .register();
+
+CommandAPICommand("item")
+ .withArguments(ItemStackArgument("itemStack"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ player.inventory.addItem(args["itemStack"] as ItemStack)
+ })
+ .register()
+
+commandAPICommand("item") {
+ itemStackArgument("itemstack")
+ playerExecutor { player, args ->
+ player.inventory.addItem(args["itemstack"] as ItemStack)
+ }
+}
+
+Similar to the BlockPredicateArgument
, the ItemStackPredicateArgument
is a way of performing predicate checks on ItemStack
objects. These can represent tags, such as the ones declared here on the MinecraftWiki, or individual items. The cast type for this argument is Predicate<ItemStack>
.
Say we wanted to remove items in your inventory (I know, the /clear
command does this, but this is the only example I could come up with). To do this, we'll use the following command syntax:
/rem <item>
+
+We implement this with a simple for loop over the player's inventory and remove items that satisfy the predicate.
+// Register our command
+new CommandAPICommand("rem")
+ .withArguments(new ItemStackPredicateArgument("items"))
+ .executesPlayer((player, args) -> {
+
+ // Get our predicate
+ @SuppressWarnings("unchecked")
+ Predicate<ItemStack> predicate = (Predicate<ItemStack>) args.get("items");
+
+ for (ItemStack item : player.getInventory()) {
+ if (predicate.test(item)) {
+ player.getInventory().remove(item);
+ }
+ }
+ })
+ .register();
+
+// Register our command
+CommandAPICommand("rem")
+ .withArguments(ItemStackPredicateArgument("items"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+
+ // Get our predicate
+ val predicate = args["items"] as Predicate<ItemStack>
+
+ for (item in player.inventory) {
+ if (predicate.test(item)) {
+ player.inventory.remove(item)
+ }
+ }
+ })
+ .register()
+
+// Register our command
+commandAPICommand("rem") {
+ itemStackPredicateArgument("items")
+ playerExecutor { player, args ->
+ // Get our predicate
+ val predicate = args["items"] as Predicate<ItemStack>
+
+ for (item in player.inventory) {
+ if (predicate.test(item)) {
+ player.inventory.remove(item)
+ }
+ }
+ }
+}
+
+List arguments allows users to provide a list of values. This argument can take on two forms:
+GreedyStringArgument
, so the greedy string argument rule applies - this argument can only be used at the end of an argument list.TextArgument
, so this argument can be used anywhere in an argument list, but its contents must be surrounded with quotes ("
).ListArgumentBuilder
Unlike other arguments, because this argument can be interpreted in various different ways, this argument can only be created using a ListArgumentBuilder
, instead of directly accessing the ListArgument
constructor. The ListArgumentBuilder
loosely follows the following format:
\begin{align} +&\quad\texttt{Create a ListArgumentBuilder} \\ +\rightarrow&\quad\texttt{(Provide the list delimiter)} \\ +\rightarrow&\quad\texttt{Provide the list to pull suggestions from} \\ +\rightarrow&\quad\texttt{Provide the mapper of the list items to a string} \\ +\rightarrow&\quad\texttt{Build the ListArgument} +\end{align}
+First, you have to create a ListArgumentBuilder
parameterized over the type that the list will generate. For example, if you want to create a list of Strings, you would use new ListArgumentBuilder<String>
.
nodeName
parameter represents the name of the node to use for the argument.delimiter
argument specifies the delimiter (separator) to use between entries. If a delimiter is not provided, a space " "
will be used as the delimiter.public ListArgumentBuilder<T>(String nodeName);
+public ListArgumentBuilder<T>(String nodeName, String delimiter);
+
+$$\downarrow$$
+++Allowing duplicates (Optional)
+If you want your users to enter duplicate entries in your list, you can use the
+allowDuplicates
method to set whether duplicates are allowed. By default, duplicates are disabled.When duplicates are enabled, items that have been entered before can be displayed again in the list of suggestions:
++ +ListArgumentBuilder.allowDuplicates(true); +
When duplicates are disabled, items that have already been entered will not appear in the list of suggestions:
++ +ListArgumentBuilder.allowDuplicates(false); +
$$\downarrow$$
+The ListArgument
requires a list that the list argument can pull suggestions and validation from. The ListArgument
does not support values which are not present in the provided list. There are three methods that can be used to provide a list for the ListArgument
:
Providing an immutable list (a list that doesn't change) using the Collection<T>
parameter:
public ListArgumentBuilder withList(Collection<T> list);
+
+Providing a list that is determined when suggestions are being displayed to the user and before the command has been executed using the Supplier<Collection<T>>
parameter:
public ListArgumentBuilder withList(Supplier<Collection<T>> list);
+
+Providing a list that is determined when suggestions are being displayed to the user and before the command has been executed, that also depends on the SuggestionInfo
present when running the command, using the Function<SuggestionInfo<CommandSender>, Collection<T>>
parameter:
public ListArgumentBuilder withList(Function<SuggestionInfo<CommandSender>, Collection<T>> list);
+
+$$\downarrow$$
+In order to display suggestions, the ListArgument
needs to know how to convert a list entry to a string. For example, a Location
may be converted into "x,y,z"
. The ListArgumentBuilder
provides three methods for providing a mapping function:
The withStringMapper()
method converts the object to a string using the object's .toString()
method. If the object is null, this method will populate it with the string "null"
:
public ListArgumentBuilder withStringMapper();
+
+The withMapper()
method requires a function that maps the object to a string:
public ListArgumentBuilder withMapper(Function<T, String> mapper);
+
+The withStringTooltipMapper()
method requires a function that maps the object to an IStringTooltip
. This allows you to also provide hover tooltips for the current item:
public ListArgumentBuilder withStringTooltipMapper(Function<T, IStringTooltip> mapper);
+
+$$\downarrow$$
+ListArgumentBuilder
To finish building the ListArgument
, call the buildGreedy()
or buildText()
method. The buildGreedy()
method will treat the list argument as a greedy string, which means you can only use this list argument at the end of the list of arguments you are declaring for the command. If you use the buildText()
instead, you can use the list argument anywhere (and multiple times), but the list must be surrounded with quotation characters ("
).
public ListArgument<T> buildGreedy();
+public ListArgument<T> buildText();
+
+Say you wanted to give yourself multiple items in a single command. For this command, we'll use the following syntax, which lets you provide the number of items to give, and a list of materials:
+/multigive <amount> <materials>
+
+To do this, we create a command with an IntegerArgument
to specify the amount (between 1 and 64), and a ListArgument
that accepts a list of Material
objects. We use the ListArgumentBuilder
to provide a list of materials as well as a mapping function that converts the material's name to a lowercase string. By default, we use a space delimiter (separator) for arguments in the list.
new CommandAPICommand("multigive")
+ .withArguments(new IntegerArgument("amount", 1, 64))
+ .withArguments(new ListArgumentBuilder<Material>("materials")
+ .withList(List.of(Material.values()))
+ .withMapper(material -> material.name().toLowerCase())
+ .buildGreedy()
+ )
+ .executesPlayer((player, args) -> {
+ int amount = (int) args.get("amount");
+ List<Material> theList = (List<Material>) args.get("materials");
+
+ for (Material item : theList) {
+ player.getInventory().addItem(new ItemStack(item, amount));
+ }
+ })
+ .register();
+
+CommandAPICommand("multigive")
+ .withArguments(IntegerArgument("amount", 1, 64))
+ .withArguments(ListArgumentBuilder<Material>("materials")
+ .withList(Material.values().toList())
+ .withMapper { material -> material.name.lowercase() }
+ .buildGreedy()
+ )
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val amount = args["amount"] as Int
+ val theList = args["materials"] as List<Material>
+
+ for (item in theList) {
+ player.inventory.addItem(ItemStack(item, amount))
+ }
+ })
+ .register()
+
+commandAPICommand("multigive") {
+ integerArgument("amount", 1, 64)
+ argument(ListArgumentBuilder<Material>("materials")
+ .withList(Material.values().toList())
+ .withMapper { material -> material.name.lowercase() }
+ .buildGreedy()
+ )
+ playerExecutor { player, args ->
+ val amount = args["amount"] as Int
+ val theList = args["materials"] as List<Material>
+
+ for (item in theList) {
+ player.inventory.addItem(ItemStack(item, amount))
+ }
+ }
+}
+
+Literal arguments are used to represent "forced options" for a command. For instance, take Minecraft's /gamemode
command. The syntax consists of the following:
/gamemode <mode>
+/gamemode <mode> <player>
+
+It consists of a gamemode, followed by an optional player argument. The list of gamemodes are as follows:
+/gamemode survival
+/gamemode creative
+/gamemode adventure
+/gamemode spectator
+
+Unlike regular commands (as those implemented by Bukkit for example), these four options are "hardcoded" - they're not "suggestions". The user can only enter one of these four examples, no other values are allowed.
+Developer's Note:
+There is a simpler alternative to the LiteralArgument
class! Instead of having to deal with arguments not being present in the array of arguments, consider using the much more intuitive MultiLiteralArgument
, which is described in more detail here!
Unlike regular arguments that are shown in this chapter, the literal argument is technically not an argument. Due to this fact, literal arguments are unlisted by default. In other words, the literal argument is not present in the CommandArguments args
for the command declaration.
To illustrate the behavior of literal arguments, we create a command of the following form:
+/mycommand <literal> <text>
+
+As an example, let's declare the literal "hello" as a valid literal for this command. When we retrieve the result from args.get(0)
, it returns the value of the TextArgument
, as opposed to the literal "hello":
new CommandAPICommand("mycommand")
+ .withArguments(new LiteralArgument("hello"))
+ .withArguments(new TextArgument("text"))
+ .executes((sender, args) -> {
+ // This gives the variable "text" the contents of the TextArgument, and not the literal "hello"
+ String text = (String) args.get(0);
+ })
+ .register();
+
+CommandAPICommand("mycommand")
+ .withArguments(LiteralArgument("hello"))
+ .withArguments(TextArgument("text"))
+ .executes(CommandExecutor { _, args ->
+ // This gives the variable "text" the contents of the TextArgument, and not the literal "hello"
+ val text = args[0] as String
+ })
+ .register()
+
+commandAPICommand("mycommand") {
+ literalArgument("hello")
+ textArgument("text")
+ anyExecutor { _, args ->
+ // This gives the variable "text" the contents of the TextArgument, and not the literal "hello"
+ val text = args[0] as String
+ }
+}
+
+The LiteralArgument
class also provides the LiteralArgument.of()
and LiteralArgument.literal()
helper methods which can be used as an alternative way to declare literal arguments:
new CommandAPICommand("mycommand")
+ .withArguments(LiteralArgument.of("hello"))
+ .withArguments(new TextArgument("text"))
+ .executes((sender, args) -> {
+ // This gives the variable "text" the contents of the TextArgument, and not the literal "hello"
+ String text = (String) args.get(0);
+ })
+ .register();
+
+new CommandAPICommand("mycommand")
+ .withArguments(LiteralArgument.literal("hello"))
+ .withArguments(new TextArgument("text"))
+ .executes((sender, args) -> {
+ // This gives the variable "text" the contents of the TextArgument, and not the literal "hello"
+ String text = (String) args.get(0);
+ })
+ .register();
+
+CommandAPICommand("mycommand")
+ .withArguments(LiteralArgument.of("hello"))
+ .withArguments(TextArgument("text"))
+ .executes(CommandExecutor { _, args ->
+ val text = args[0] as String
+ })
+ .register()
+
+CommandAPICommand("mycommand")
+ .withArguments(LiteralArgument.literal("hello"))
+ .withArguments(TextArgument("text"))
+ .executes(CommandExecutor { _, args ->
+ val text = args[0] as String
+ })
+ .register()
+
+commandAPICommand("mycommand") {
+ argument(LiteralArgument.of("hello"))
+ textArgument("text")
+ anyExecutor { _, args ->
+ val text = args[0] as String
+ }
+}
+
+commandAPICommand("mycommand") {
+ argument(LiteralArgument.literal("hello"))
+ textArgument("text")
+ anyExecutor { _, args ->
+ val text = args[0] as String
+ }
+}
+
+If I were to run the following command:
+/mycommand hello goodbye
+
+The value of text
in the code above would be "goodbye".
This is a demonstration of how you could create a command similar to Minecraft's /gamemode
command by using literal arguments. To do this, we are effectively registering 4 separate commands, each called /gamemode
, but with different literal arguments.
// Create a map of gamemode names to their respective objects
+HashMap<String, GameMode> gamemodes = new HashMap<>();
+gamemodes.put("adventure", GameMode.ADVENTURE);
+gamemodes.put("creative", GameMode.CREATIVE);
+gamemodes.put("spectator", GameMode.SPECTATOR);
+gamemodes.put("survival", GameMode.SURVIVAL);
+
+// Iterate over the map
+for(Entry<String, GameMode> entry : gamemodes.entrySet()) {
+
+ // Register the command as usual
+ new CommandAPICommand("changegamemode")
+ .withArguments(new LiteralArgument(entry.getKey()))
+ .executesPlayer((player, args) -> {
+ // Retrieve the object from the map via the key and NOT the args[]
+ player.setGameMode(entry.getValue());
+ })
+ .register();
+}
+
+// Create a map of gamemode names to their respective objects
+val gamemodes = mapOf(
+ "adventure" to GameMode.ADVENTURE,
+ "creative" to GameMode.CREATIVE,
+ "spectator" to GameMode.SPECTATOR,
+ "survival" to GameMode.SURVIVAL
+)
+
+// Iterate over the map
+for ((key, _) in gamemodes) {
+
+ // Register the command as usual
+ CommandAPICommand("changegamemode")
+ .withArguments(LiteralArgument(key))
+ .executesPlayer(PlayerCommandExecutor { player, _ ->
+ // Retrieve the object from the map via the key and NOT the args[]
+ player.gameMode = gamemodes[key]!!
+ })
+ .register()
+}
+
+// Create a map of gamemode names to their respective objects
+val gamemodes = mapOf(
+ "adventure" to GameMode.ADVENTURE,
+ "creative" to GameMode.CREATIVE,
+ "spectator" to GameMode.SPECTATOR,
+ "survival" to GameMode.SURVIVAL
+)
+
+// Iterate over the map
+for ((key, _) in gamemodes) {
+
+ // Register the command as usual
+ commandAPICommand("changegamemode") {
+ literalArgument(key)
+ playerExecutor { player, args ->
+ // Retrieve the object from the map via the key and NOT the args[]
+ player.gameMode = gamemodes[key]!!
+ }
+ }
+
+}
+
+Note how, since we don't have access to the literal from args
, we must access the provided gamemode from elsewhere.
Literal arguments require a string in the constructor. If the literal is an empty String or is null, the CommandAPI will throw a BadLiteralException
.
Because literal arguments are "hardcoded", each literal is effectively mapped to a single command. This is shown when using the configuration option create-dispatcher-json: true
which shows the JSON result of registered commands. For instance, take the /defaultgamemode
command:
"defaultgamemode": {
+ "type": "literal",
+ "children": {
+ "adventure": {
+ "type": "literal",
+ "executable": true
+ },
+ "creative": {
+ "type": "literal",
+ "executable": true
+ },
+ "spectator": {
+ "type": "literal",
+ "executable": true
+ },
+ "survival": {
+ "type": "literal",
+ "executable": true
+ }
+ }
+},
+
+Each option produces a new "command" in the tree of commands. This means that having exceptionally large lists of literals, or nested literals (e.g. /command <literal1> <literal2>
) can cause very large trees which cannot be sent to the clients (it can cause clients to crash).
++ +Developer's Note:
+Take care when using literal arguments. If your list of arguments is exceptionally large, or contains many nested arguments, the server may be unable to send the command information to the client. If many command argument choices are required, consider using a
+StringArgument
and using.replaceSuggestions()
to create your own list of required arguments.
In the CommandAPI, there are two arguments used to represent location. The LocationArgument
argument, which represents a 3D location \( (x, y, z) \) and the Location2DArgument
, which represents 2D location \( (x, z) \).
The LocationArgument
class is used to specify a location in the command sender's current world, returning a Bukkit Location
object. It allows the user to enter three numbers as coordinates, or use relative coordinates (i.e. the ~
and ^
operators).
The LocationArgument
constructor requires a LocationType
, which specifies the type of location that is accepted by the command. The LocationType
enum consists of two values:
LocationType.BLOCK_POSITION
BLOCK_POSITION
refers to integer block coordinates. When in-game as a player, the suggested location is the coordinates of block you are looking at when you type the command.
LocationType.PRECISE_POSITION
PRECISE_PRECISION
uses exact coordinates, using the double
primitive type. When in-game as a player, the suggested location is the exact coordinates of where your cursor is pointing at when you type the command.
If no LocationType
is provided, the LocationArgument
will use PRECISE_POSITION
by default.
The LocationArgument
constructor can also accept a boolean centerPosition
. If set to true
, when using LocationType.PRECISE_POSITION
, if an integer is provided in the value, it will add 0.5 to the x and z coordinates to center the position within a block. If set to false
, the integer value will be provided as is.
If no centerPosition
parameter is provided, the LocationArgument
will use centerPosition = true
by default.
Say you use the following constructor, which sets centerPosition
to true
:
new LocationArgument("location", LocationType.PRECISE_POSITION, true);
+
+LocationArgument("location", LocationType.PRECISE_POSITION, true)
+
+Integer positions are centered
+Let's also say you use the following location using this location argument in a command:
+10 20 30
+
+The resulting location will be the following, which centers the position of the x and z coordinates. This does not change the y coordinate:
+10.5 20 30.5
+
+Non-integer positions remain as normal
+If you use the following location using this location argument in a command:
+10.2 20.2 30.2
+
+The resulting location will be the following, which does not change the x and z coordinates, because the positions are not integers:
+10.2 20.2 30.2
+
+Say you use the following constructor, which sets centerPosition
to false
:
new LocationArgument("location", LocationType.PRECISE_POSITION, false);
+
+LocationArgument("location", LocationType.PRECISE_POSITION, false)
+
+Integer positions are not centered
+Let's also say you use the following location using this location argument in a command:
+10 20 30
+
+The resulting location will be the following, which does not modify the position of the x and z coordinates:
+10 20 30
+
+We can declare a simple command to break a block:
+/break <location>
+
+Simply put, given the coordinates provided to the command, "break" the block by setting it's type to Material.AIR
. For this example, we're referring to block specific coordinates, so we want to use LocationType.BLOCK_POSITION
:
new CommandAPICommand("break")
+ // We want to target blocks in particular, so use BLOCK_POSITION
+ .withArguments(new LocationArgument("block", LocationType.BLOCK_POSITION))
+ .executesPlayer((player, args) -> {
+ Location location = (Location) args.get("block");
+ location.getBlock().setType(Material.AIR);
+ })
+ .register();
+
+CommandAPICommand("break")
+ // We want to target blocks in particular, so use BLOCK_POSITION
+ .withArguments(LocationArgument("block", LocationType.BLOCK_POSITION))
+ .executesPlayer(PlayerCommandExecutor { _, args ->
+ (args["block"] as Location).block.type = Material.AIR
+ })
+ .register()
+
+commandAPICommand("break") {
+ // We want to target blocks in particular, so use BLOCK_POSITION
+ locationArgument("block", LocationType.BLOCK_POSITION)
+ playerExecutor { _, args ->
+ (args["block"] as Location).block.type = Material.AIR
+ }
+}
+
+The Location2DArgument
is pretty much identical in use to the LocationArgument
for 3D coordinates, except instead of returning a Location
object, it instead returns a Location2D
object that extends Location
(thus, being compatible anywhere you would normally be able to use Location
).
The LootTableArgument
class can be used to get a Bukkit LootTable
object.
Say we wanted to write a command that populates a chest with some loot table contents. For this example, we'll use the following command:
+/giveloottable <loottable> <location>
+
+We ensure that the location provided is a container (such as a chest or shulkerbox) and then update the contents of that container:
+new CommandAPICommand("giveloottable")
+ .withArguments(new LootTableArgument("lootTable"))
+ .withArguments(new LocationArgument("location", LocationType.BLOCK_POSITION))
+ .executes((sender, args) -> {
+ LootTable lootTable = (LootTable) args.get("lootTable");
+ Location location = (Location) args.get("location");
+
+ BlockState state = location.getBlock().getState();
+
+ // Check if the input block is a container (e.g. chest)
+ if (state instanceof Container container && state instanceof Lootable lootable) {
+ // Apply the loot table to the chest
+ lootable.setLootTable(lootTable);
+ container.update();
+ }
+ })
+ .register();
+
+CommandAPICommand("giveloottable")
+ .withArguments(LootTableArgument("lootTable"))
+ .withArguments(LocationArgument("location", LocationType.BLOCK_POSITION))
+ .executes(CommandExecutor { _, args ->
+ val lootTable = args["lootTable"] as LootTable
+ val location = args["location"] as Location
+
+ val state = location.block.state
+
+ // Check if the input block is a container (e.g. chest)
+ if (state is Container && state is Lootable) {
+ // Apply the loot table to the chest
+ state.lootTable = lootTable
+ state.update()
+ }
+ })
+ .register()
+
+commandAPICommand("giveloottable") {
+ lootTableArgument("loottable")
+ locationArgument("location", LocationType.BLOCK_POSITION)
+ anyExecutor { _, args ->
+ val lootTable = args["loottable"] as LootTable
+ val location = args["location"] as Location
+
+ val state = location.block.state
+
+ // Check if the input block is a container (e.g. chest)
+ if (state is Container && state is Lootable) {
+ // Apply the loot table to the chest
+ state.lootTable = lootTable
+ state.update()
+ }
+ }
+}
+
+A MapArgument
can be used to provide a map of values. This argument uses an underlying GreedyStringArgument
which means that this argument can only be used at the end of the argument list.
+It returns a LinkedHashMap
object.
MapArgumentBuilder
Similar to the ListArgument
, this argument also uses a builder class to construct it.
\begin{align} +&\quad\texttt{Create a MapArgumentBuilder and possibly provide the delimiter or separator} \\ +\rightarrow&\quad\texttt{Provide the mapper from a string to an object of the provided key type} \\ +\rightarrow&\quad\texttt{Provide the mapper from a string to an object of the provided value type} \\ +\rightarrow&\quad\texttt{Provide the list with keys to pull suggestions from} \\ +\rightarrow&\quad\texttt{Provide the list with values to pull suggestions from} \\ +\rightarrow&\quad\texttt{Build the MapArgument} +\end{align}
+MapArgument
To start building the argument, you first have to construct a MapArgumentBuilder
parameterized over the types the key and the value are supposed to have.
+If you wanted to construct a MapArgument
that returns a LinkedHashMap<String, Integer>
you would construct the MapArgumentBuilder
like this:
new MapArgumentBuilder<String, Integer>
+
+The MapArgumentBuilder
has three possible constructors:
public MapArgumentBuilder<K, V>(String nodeName);
+public MapArgumentBuilder<K, V>(String nodeName, char delimiter);
+public MapArgumentBuilder<K, V>(String nodeName, char delimiter, String separator)
+
+nodeName
: This parameter determines the node name of the MapArgument
delimiter
: This parameter determines the delimiter. This separates a key from a value (key:value
). When not provided, it defaults to a colon (:
)separator
: This parameter determines the separator. This separates one key-value pair from another (key:value key:value
). When not provided, it defaults to a space (
)$$\downarrow$$
+The mapper functions are used to parse the argument when entered. Because a GreedyStringArgument
+returns a String
, we need a way to convert a String
into an object specified by the type parameters.
When providing mappers, you first need to provide the key mapper:
+public MapArgumentBuilder withKeyMapper(StringParser<K>);
+
+You then have to provide the value mapper:
+public MapArgumentBuilder withValueMapper(StringParser<V>);
+
+StringParser
is a functional interface with the following definition:
@FunctionalInterface
+public interface StringParser<T> {
+ /**
+ * A method that turns a String into an object of type T.
+ *
+ * @param s The String to parse
+ * @return The resulting parsed object
+ * @throws WrapperCommandSyntaxException If there is a problem with the syntax of the String that prevents it from being turned into an object of type T.
+ */
+ T parse(String s) throws WrapperCommandSyntaxException;
+}
+
+This signature allows you to throw exceptions using the CommandAPI.fail...
methods if the given String cannot be parsed (see Handling command failures).
$$\downarrow$$
+When providing suggestions you have the choice whether players are allowed to enter any key/value pair or only key/value pairs specified by the MapArgument
.
+To accomplish this the MapArgumentBuilder
provides different methods.
Similar to the mappers, you first have to provide the key suggestions:
+public MapArgumentBuilder withKeyList(List<String> keyList);
+
+public MapArgumentBuilder withoutKeyList();
+
+Next, you have to provide the value suggestions. In addition to the two possibilities presented for the key suggestions, here you also have the possibility to define +whether a value can be written multiple times.
+public MapArgumentBuilder withValueList(List<String> valueList);
+
+public MapArgumentBuilder withValueList(List<String> valueList, boolean allowValueDuplicates);
+
+public MapArgumentBuilder withoutValueList();
+
+public MapArgumentBuilder withoutValueList(boolean allowDuplicates)
+
+If you choose to allow a value to be written multiple times you have to set allowValueDuplicates
to true
. By default, it is set to false
and
+does not allow values to be written multiple times.
$$\downarrow$$
+MapArgument
To finish building the MapArgument
, you have to call the build()
method. This will return a new MapArgument
object.
public MapArgument<K, V> build();
+
+Let's say we want to create a command that we can execute to send multiple players messages without typing the command more than once. For that, we create a command with the following syntax:
+/sendmessage <message>
+
+To implement that, we create a command that uses a MapArgument
and use Player
objects as keys and String
objects as values:
new CommandAPICommand("sendmessage")
+ // Parameter 'delimiter' is missing, delimiter will be a colon
+ // Parameter 'separator' is missing, separator will be a space
+ .withArguments(new MapArgumentBuilder<Player, String>("message")
+
+ // Providing a key mapper to convert a String into a Player
+ .withKeyMapper(Bukkit::getPlayer)
+
+ // Providing a value mapper to leave the message how it was sent
+ .withValueMapper(s -> s)
+
+ // Providing a list of player names to be used as keys
+ .withKeyList(Bukkit.getOnlinePlayers().stream().map(Player::getName).toList())
+
+ // Don't provide a list of values so messages can be chosen without restrictions
+ // Allow duplicates in case the same message should be sent to different players
+ .withoutValueList(true)
+
+ // Build the MapArgument
+ .build()
+ )
+ .executesPlayer((player, args) -> {
+ // The MapArgument returns a LinkedHashMap
+ LinkedHashMap<Player, String> map = (LinkedHashMap<Player, String>) args.get("message");
+
+ // Sending the messages to the players
+ for (Entry<Player, String> messageRecipients : map.entrySet()) {
+ messageRecipients.getKey().sendMessage(messageRecipients.getValue());
+ }
+ })
+ .register();
+
+CommandAPICommand("sendmessage")
+ // Parameter 'delimiter' is missing, delimiter will be a colon
+ // Parameter 'separator' is missing, separator will be a space
+ .withArguments(MapArgumentBuilder<Player, String>("message")
+
+ // Providing a key mapper to convert a String into a Player
+ .withKeyMapper { s: String -> Bukkit.getPlayer(s) }
+
+ // Providing a value mapper to leave the message how it was sent
+ .withValueMapper { s: String -> s }
+
+ // Providing a list of player names to be used as keys
+ .withKeyList(Bukkit.getOnlinePlayers().map { player: Player -> player.name }.toList())
+
+ // Don't provide a list of values so messages can be chosen without restrictions
+ // Allow duplicates in case the same message should be sent to different players
+ .withoutValueList(true)
+
+ // Build the MapArgument
+ .build()
+ )
+ .executesPlayer(PlayerCommandExecutor { _, args ->
+ // The MapArgument returns a LinkedHashMap
+ val map: LinkedHashMap<Player, String> = args["message"] as LinkedHashMap<Player, String>
+
+ // Sending the messages to the players
+ for (messageRecipient in map.keys) {
+ messageRecipient.sendMessage(map[messageRecipient]!!)
+ }
+ })
+ .register()
+
+commandAPICommand("sendmessage") {
+ // Parameter 'delimiter' is missing, delimiter will be a colon
+ // Parameter 'separator' is missing, separator will be a space
+ argument(MapArgumentBuilder<Player, String>("message")
+
+ // Providing a key mapper to convert a String into a Player
+ .withKeyMapper { s: String -> Bukkit.getPlayer(s) }
+
+ // Providing a value mapper to leave the message how it was sent
+ .withValueMapper { s: String -> s }
+
+ // Providing a list of player names to be used as keys
+ .withKeyList(Bukkit.getOnlinePlayers().map { player: Player -> player.name }.toList())
+
+ // Don't provide a list of values so messages can be chosen without restrictions
+ // Allow duplicates in case the same message should be sent to different players
+ .withoutValueList(true)
+
+ // Build the MapArgument
+ .build()
+ )
+ playerExecutor { _, args ->
+ // The MapArgument returns a LinkedHashMap
+ val map: LinkedHashMap<Player, String> = args["message"] as LinkedHashMap<Player, String>
+
+ // Sending the messages to the players
+ for (messageRecipient in map.keys) {
+ messageRecipient.sendMessage(map[messageRecipient]!!)
+ }
+ }
+}
+
+Developer's Note:
+The MapArgument
is very strict and doesn't have room for any errors. A key must always be followed by the delimiter, then a value. One value and the next key must always be separated by the separator. Both keys and values also have the option to be surrounded by quotes.
For example, let's say you are on a server with two players, Player1
and Player2
. We want to send both of them the message Hello, <playerName>!
+To do that, we use the previously declared sendmessage
command like this:
/sendmessage Player1:"Hello, Player1!" Player2:"Hello, Player2!"
+
+A colon is used as the delimiter and a space as the separator because those are the defaults, and neither was specified in the MapArgumentBuilder
constructor. Since the separator was a space, the messages were surrounded by quotes to avoid the spaces inside them from being misinterpreted as the start of the next key-value pair.
The CommandAPI's MathOperationArgument
is used to represent the Minecraft scoreboard arithmetic operation to alter scoreboard scores. Since there is no default representation in the Bukkit API, the CommandAPI provides the MathOperation
class to represent each operation:
Symbol (in Minecraft) | MathOperation enum value |
---|---|
\(+=\) | MathOperation.ADD |
\(-=\) | MathOperation.SUBTRACT |
\(*=\) | MathOperation.MULTIPLY |
\(/=\) | MathOperation.DIVIDE |
\(\%=\) | MathOperation.MOD |
\(=\) | MathOperation.ASSIGN |
\(<\) | MathOperation.MIN |
\(>\) | MathOperation.MAX |
\(><\) | MathOperation.SWAP |
The MathOperation
also has two methods:
public int apply(int val1, int val2);
+public float apply(float val1, float val2);
+
+These methods are used to provide a basic implementation of these math operations on a given input. Given the values val1
and val2
, these are the operation that the apply(val1, val2)
method performs:
MathOperation enum value | Result |
---|---|
MathOperation.ADD | val1 + val2 |
MathOperation.SUBTRACT | val1 - val2 |
MathOperation.MULTIPLY | val1 * val2 |
MathOperation.DIVIDE | val1 / val2 |
MathOperation.MOD | val1 % val2 |
MathOperation.ASSIGN | val2 |
MathOperation.MIN | Math.min(val1, val2) |
MathOperation.MAX | Math.max(val1, val2) |
MathOperation.SWAP | val2 |
Say we wanted to create a player's level. Typically, this is implemented in the following manner:
+/xp set <player> <level>
+/xp add <player> <levels>
+
+Using the MathOperationArgument
, we can extend the functionality of adding and setting a player's level by allowing the user to choose what operation they desire. To do this, we'll use the following syntax:
/changelevel <player> <operation> <value>
+
+As with any command, we declare our arguments, cast them properly and then we write our main code. In this example, we use the apply(int, int)
method from our MathOperation
to calculate the player's new level.
new CommandAPICommand("changelevel")
+ .withArguments(new PlayerArgument("player"))
+ .withArguments(new MathOperationArgument("operation"))
+ .withArguments(new IntegerArgument("value"))
+ .executes((sender, args) -> {
+ Player target = (Player) args.get("player");
+ MathOperation op = (MathOperation) args.get("operation");
+ int value = (int) args.get("value");
+
+ target.setLevel(op.apply(target.getLevel(), value));
+ })
+ .register();
+
+CommandAPICommand("changelevel")
+ .withArguments(PlayerArgument("player"))
+ .withArguments(MathOperationArgument("operation"))
+ .withArguments(IntegerArgument("value"))
+ .executes(CommandExecutor { _, args ->
+ val target = args["player"] as Player
+ val op = args["operation"] as MathOperation
+ val value = args["value"] as Int
+
+ target.level = op.apply(target.level, value)
+ })
+ .register()
+
+commandAPICommand("changelevel") {
+ playerArgument("player")
+ mathOperationArgument("operation")
+ integerArgument("value")
+ anyExecutor { _, args ->
+ val target = args["player"] as Player
+ val op = args["operation"] as MathOperation
+ val value = args["value"] as Int
+
+ target.level = op.apply(target.level, value)
+ }
+}
+
+There are various applications for the changelevel
command based on what the user inputs. For example:
To set the player Notch to level 10:
+/changelevel Notch = 10
+
+To double the player Notch's level:
+/changelevel Notch *= 2
+
+To set the player Notch's level to 20, or keep it as their current level if it is higher than 20:
+/changelevel Notch > 20
+
+So far, we've described normal arguments and literal arguments. We've described the nuances with literal arguments and how they're not really "arguments", so they don't appear in the CommandArguments args
for commands.
Now forget all of that. Multi literal arguments are the same as literal arguments but they do appear in the CommandArguments args
for commands (i.e. they are listed). Multi literal arguments are just a way better alternative to literal arguments. The multi literal argument constructor allows you to provide a String nodeName
and a String... literals
of possible values which you can use for your command declaration.
The multi literal argument has all of the same benefits of a regular literal argument - they are hardcoded options that the user must enter - they don't allow other values.
+Developer's Note:
+For 9.1.0, all previously existing MultiLiteralArgument
constructors have been deprecated! They will be removed in a future version.
The new constructor looks like this:
+public MultiLiteralArgument(String nodeName, String... literals)
+
+In this example, we'll show how to use multi literals to declare Minecraft's /gamemode
command. As you can see from the example code below, the argument declaration and command declaration is the same as if you were declaring any normal argument or command.
new CommandAPICommand("gamemode")
+ .withArguments(new MultiLiteralArgument("gamemodes", "adventure", "creative", "spectator", "survival"))
+ .executesPlayer((player, args) -> {
+ // The literal string that the player enters IS available in the args[]
+ switch ((String) args.get("gamemodes")) {
+ case "adventure":
+ player.setGameMode(GameMode.ADVENTURE);
+ break;
+ case "creative":
+ player.setGameMode(GameMode.CREATIVE);
+ break;
+ case "spectator":
+ player.setGameMode(GameMode.SPECTATOR);
+ break;
+ case "survival":
+ player.setGameMode(GameMode.SURVIVAL);
+ break;
+ default:
+ player.sendMessage("Invalid gamemode: " + args.get("gamemodes"));
+ break;
+ }
+ })
+ .register();
+
+CommandAPICommand("gamemode")
+ .withArguments(MultiLiteralArgument("gamemodes", "adventure", "creative", "spectator", "survival"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ // The literal string that the player enters IS available in the args[]
+ when (args["gamemodes"] as String) {
+ "adventure" -> player.gameMode = GameMode.ADVENTURE
+ "creative" -> player.gameMode = GameMode.CREATIVE
+ "spectator" -> player.gameMode = GameMode.SPECTATOR
+ "survival" -> player.gameMode = GameMode.SURVIVAL
+ }
+ })
+ .register()
+
+commandAPICommand("gamemode") {
+ multiLiteralArgument(nodeName = "gamemodes", "adventure", "creative", "spectator", "survival") // Adding this for now, needed because ambiguous methods exist
+ playerExecutor { player, args ->
+ // The literal string that the player enters IS available in the args[]
+ when (args["gamemodes"] as String) {
+ "adventure" -> player.gameMode = GameMode.ADVENTURE
+ "creative" -> player.gameMode = GameMode.CREATIVE
+ "spectator" -> player.gameMode = GameMode.SPECTATOR
+ "survival" -> player.gameMode = GameMode.SURVIVAL
+ }
+ }
+}
+
+An important thing to note is that we don't have to implement a default
case for the above switch
statements, because the CommandAPI will only permit valid options of a MultiLiteralArgument
to reach the executor. If the user enters an invalid option, the command doesn't run.
NamespacedKey arguments represent Minecraft's resource locations, or namespaced keys. This argument is casted to Bukkit's NamespacedKey
object.
Namespaced keys are typically of the form namespace:key
. If no namespace is provided, the default namespace (minecraft
) will be used.
The CommandAPI includes support for NBT compound arguments using an NBT API. The usage for the NBTCompoundArgument
depends on whether you are using the CommandAPI plugin (using a CommandAPI.jar
file in your plugins/
folder), or are shading the CommandAPI (including the compiled CommandAPI code in your own plugin).
By default, the CommandAPI plugin includes a copy of the NBT API by tr7zw in dev.jorel.commandapi.nbtapi
. No additional set up is required and it can be used directly out the box.
In order to use the NBTCompoundArgument
, you will have to use an NBT API that can create an NBT Compound object from an Object
(ideally a net.minecraft.nbt.NBTTagCompound
object). Examples of NBT APIs that can do this are (these are not sponsored in any way):
new NBTContainer(Object)
constructorBefore the NBTCompoundArgument
can be used, the CommandAPI needs to know what implementation of an NBT Compound object you're going to use. This is specified in the onLoad()
sequence, where your CommandAPI's config is set up, by using the following method:
<T> CommandAPIConfig initializeNBTAPI(Class<T> nbtContainerClass, Function<Object, T> nbtContainerConstructor);
+
+The initializeNBTAPI(Class<T>, Function<Object, T>)
takes in two arguments:
Class<T>
- The class that will be your NBT Compound implementation. This is also the type that the CommandAPI will return when the NBTCompoundArgument
is used.
Function<Object, T>
- A function that takes in an object and returns the specified NBT Compound implementation. This could be a constructor or a static method, for example.
Say we want to use the NBT API as our implementation of NBT compounds. First, we have to shade the NBT API into our project (view the official documentation for how to do this for Maven or Gradle).
+Now, we can configure the CommandAPI using the CommandAPI.onLoad()
method to use the NBTContainer
class, and the NBTContainer
constructor that takes in an Object
:
@Override
+public void onLoad() {
+ CommandAPI.onLoad(new CommandAPIBukkitConfig(this)
+ .initializeNBTAPI(NBTContainer.class, NBTContainer::new)
+ );
+}
+
+override fun onLoad() {
+ CommandAPI.onLoad(CommandAPIBukkitConfig(this)
+ .initializeNBTAPI(NBTContainer::class.java, ::NBTContainer)
+ )
+}
+
+++Confused with the
+::new
syntax? Read more about method references to a constructor here.
We're now able to use the NBTContainer
as our implemented type for the NBTCompoundArgument
!
Since the underlying implementation of the NBTCompoundArgument
can change (e.g. NBTContainer
if you're using the NBT API), the type of your NBT compound implementation has to be declared in angle brackets.
new CommandAPICommand("award")
+ .withArguments(new NBTCompoundArgument<NBTContainer>("nbt"))
+ .executes((sender, args) -> {
+ NBTContainer nbt = (NBTContainer) args.get("nbt");
+
+ // Do something with "nbt" here...
+ })
+ .register();
+
+CommandAPICommand("award")
+ .withArguments(NBTCompoundArgument<NBTContainer>("nbt"))
+ .executes(CommandExecutor { _, args ->
+ val nbt = args["nbt"] as NBTContainer
+
+ // Do something with "nbt" here...
+ })
+ .register()
+
+commandAPICommand("award") {
+ nbtCompoundArgument<NBTContainer>("nbt")
+ anyExecutor { _, args ->
+ val nbt = args["nbt"] as NBTContainer
+
+ // Do something with "nbt" here...
+ }
+}
+
+++ +Developer's Note:
+If you believe you can supply a suitable example for this page, feel free to send an example on the CommandAPI issues page!
+
In the CommandAPI, objectives are split into two classes:
+ObjectiveArgument
class, which represents objectives as a wholeObjectiveCriteriaArgument
class, which represents objective criteriaThe objective argument refers to a single scoreboard objective.
+As an example, let's create a command to move an objective to a player's sidebar. To do this, we will use the following command syntax:
+/sidebar <objective>
+
+new CommandAPICommand("sidebar")
+ .withArguments(new ObjectiveArgument("objective"))
+ .executes((sender, args) -> {
+ Objective objective = (Objective) args.get("objective");
+
+ // Set display slot
+ objective.setDisplaySlot(DisplaySlot.SIDEBAR);
+ })
+ .register();
+
+CommandAPICommand("sidebar")
+ .withArguments(ObjectiveArgument("objective"))
+ .executes(CommandExecutor { _, args ->
+ val objective = args["objective"] as Objective
+
+ // Set display slot
+ objective.displaySlot = DisplaySlot.SIDEBAR
+ })
+ .register()
+
+commandAPICommand("sidebar") {
+ objectiveArgument("objective")
+ anyExecutor { _, args ->
+ val objective = args["objective"] as Objective
+
+ // Set display slot
+ objective.displaySlot = DisplaySlot.SIDEBAR
+ }
+}
+
+The ObjectiveCriteriaArgument
is fairly straight forward - it represents the criteria for an objective. Similar to Bukkit, the objective criteria is simply represented as a String
, so it must be casted to a String
when being used.
Say we wanted to create a command to unregister all objectives based on a given criteria. Let's create a command with the following form:
+/unregisterall <objective critera>
+
+To do this, we're going to take advantage of Bukkit's Scoreboard.getObjectivesByCriteria(String)
method
new CommandAPICommand("unregisterall")
+ .withArguments(new ObjectiveCriteriaArgument("objective criteria"))
+ .executes((sender, args) -> {
+ String objectiveCriteria = (String) args.get("objective criteria");
+ Set<Objective> objectives = Bukkit.getScoreboardManager().getMainScoreboard().getObjectivesByCriteria(objectiveCriteria);
+
+ // Unregister the objectives
+ for (Objective objective : objectives) {
+ objective.unregister();
+ }
+ })
+ .register();
+
+CommandAPICommand("unregisterall")
+ .withArguments(ObjectiveCriteriaArgument("objective criteria"))
+ .executes(CommandExecutor { _, args ->
+ val objectiveCriteria = args["objective criteria"] as String
+ val objectives = Bukkit.getScoreboardManager().mainScoreboard.getObjectivesByCriteria(objectiveCriteria)
+
+ // Unregister the objectives
+ for (objective in objectives) {
+ objective.unregister()
+ }
+ })
+ .register()
+
+commandAPICommand("unregisterall") {
+ objectiveCriteriaArgument("objective criteria")
+ anyExecutor { _, args ->
+ val objectiveCriteria = args["objective criteria"] as String
+ val objectives = Bukkit.getScoreboardManager().mainScoreboard.getObjectivesByCriteria(objectiveCriteria)
+
+ // Unregister the objectives
+ for (objective in objectives) {
+ objective.unregister()
+ }
+ }
+}
+
+The particle argument requires additional data for a particle depending on what the particle is. The following particles have additional data required to display them:
+ +Bukkit Particle | +Argument syntax | +
---|---|
BLOCK |
+
+ block{block_state:{Name:"block_name"}}+
|
+
BLOCK_MARKER |
+
+ block_marker{block_state:{Name:"block_name"}}+
|
+
DUST |
+
+ dust{color:[red,green,blue],scale:scale}+
|
+
DUST_COLOR_TRANSITION |
+
+ dust_color_transition{from_color:[red,green,blue],+
|
+
DUST_PILLAR |
+
+ dust_pillar{block_state:{Name:"block_name"}}+
|
+
ENTITY_EFFECT |
+
+ entity_effect{color:[red,green,blue,alpha]}+
|
+
FALLING_DUST |
+
+ falling_dust{block_state:{Name:"block_name"}}+
|
+
ITEM |
+
+ item{item:"item"}+
|
+
SCULK_CHARGE |
+
+ sculk_charge{roll:angle}+
|
+
SHRIEK |
+
+ shriek{delay:delay}+
|
+
VIBRATION |
+
+ vibration{destination:{type:"block",pos:[x,y,z]},+
|
+
Because certain particles (in the table above) require additional data, it is not recommended to spawn a particle without its corresponding data. This can result in particles not showing due to missing requirements.
+Say we wanted to have a command that displayed particles at a player's location. We will use the following command syntax:
+/showparticle <particle>
+
+With this, we can simply spawn the particle using the World.spawnParticle(Particle, Location, int)
method:
new CommandAPICommand("showparticle")
+ .withArguments(new ParticleArgument("particle"))
+ .executesPlayer((player, args) -> {
+ ParticleData<?> particleData = (ParticleData<?>) args.get("particle");
+ player.getWorld().spawnParticle(particleData.particle(), player.getLocation(), 1);
+ })
+ .register();
+
+CommandAPICommand("showparticle")
+ .withArguments(ParticleArgument("particle"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val particleData = args["particle"] as ParticleData<Any>
+ player.world.spawnParticle(particleData.particle(), player.location, 1)
+ })
+ .register()
+
+commandAPICommand("showparticle") {
+ particleArgument("particle")
+ playerExecutor { player, args ->
+ val particleData = args["particle"] as ParticleData<Any>
+ player.world.spawnParticle(particleData.particle(), player.location, 1)
+ }
+}
+
+Running this can result in errors due to missing requirements. If you provide a particle that has additional requirements, Bukkit will throw an error and the particle will not be displayed. Instead, the example below should be used.
+We can fix the issues with the example above by providing the data of the argument using the ParticleData
record:
/showparticle <particle>
+
+In this case, we'll use the World.spawnParticle(Particle particle, Location location, int count, T data)
method which accepts some particle data:
new CommandAPICommand("showparticle")
+ .withArguments(new ParticleArgument("particle"))
+ .executesPlayer((player, args) -> {
+ ParticleData<?> particleData = (ParticleData<?>) args.get("particle");
+ player.getWorld().spawnParticle(particleData.particle(), player.getLocation(), 1, particleData.data());
+ })
+ .register();
+
+CommandAPICommand("showparticle")
+ .withArguments(ParticleArgument("particle"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val particleData = args["particle"] as ParticleData<Any>
+ player.world.spawnParticle(particleData.particle(), player.location, 1, particleData.data())
+ })
+ .register()
+
+commandAPICommand("showparticle") {
+ particleArgument("particle")
+ playerExecutor { player, args ->
+ val particleData = args["particle"] as ParticleData<Any>
+ player.world.spawnParticle(particleData.particle(), player.location, 1, particleData.data())
+ }
+}
+
+This can be used with commands such as:
+/showparticle minecraft:dust_color_transition{from_color:[0.0,0.0,0.0],scale:20.0,to_color:[1.0,0.0,0.0]}
+/showparticle minecraft:block_marker{block_state:{Name:"diamond_block"}}
+
+The vibration
particle will return a particle data of the Bukkit Vibration
class. In the Vibration
class, you can access the destination location using the Vibration.getDestination()
method, which returns a Vibration.Destination
instance. The CommandAPI will always return a Vibration.Destination.BlockDestination
instance, and will never return a Vibration.Destination.EntityDestination
instance. An example of accessing the location can be found below:
ParticleData<Vibration> particleData; // The particle data you get from your argument
+Location destination = ((BlockDestination) particleData.data().getDestination()).getLocation();
+
+
+ The particle argument requires additional data for a particle depending on what the particle is. Information about this can be found on the Argument types page on the MinecraftWiki. The following particles have additional data required to display them:
+Bukkit Particle | Minecraft particle | Arguments |
---|---|---|
BLOCK_CRACK | block | block block_id block block_id[block_state=value] |
BLOCK_MARKER | block_marker | block_marker block_id block_marker block_id[block_state=value] |
REDSTONE | dust | dust red green blue size |
DUST_COLOR_TRANSITION | dust_color_transition | dust_color_transition red1 green1 blue1 size red2 green2 blue2 |
FALLING_DUST | falling_dust | falling_dust block_id falling_dust block_id[block_state=value] |
ITEM_CRACK | item | item item_id item item_id{NBT} |
SCULK_CHARGE | sculk_charge | sculk_charge angle |
SHRIEK | shriek | shriek delay |
VIBRATION | vibration | vibration x y z ticks |
Because certain particles (in the table above) require additional data, it is not recommended to spawn a particle without its corresponding data. This can result in particles not showing due to missing requirements.
+Say we wanted to have a command that displayed particles at a player's location. We will use the following command syntax:
+/showparticle <particle>
+
+With this, we can simply spawn the particle using the World.spawnParticle(Particle, Location, int)
method:
new CommandAPICommand("showparticle")
+ .withArguments(new ParticleArgument("particle"))
+ .executesPlayer((player, args) -> {
+ ParticleData<?> particleData = (ParticleData<?>) args.get("particle");
+ player.getWorld().spawnParticle(particleData.particle(), player.getLocation(), 1);
+ })
+ .register();
+
+CommandAPICommand("showparticle")
+ .withArguments(ParticleArgument("particle"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val particleData = args["particle"] as ParticleData<Any>
+ player.world.spawnParticle(particleData.particle(), player.location, 1)
+ })
+ .register()
+
+commandAPICommand("showparticle") {
+ particleArgument("particle")
+ playerExecutor { player, args ->
+ val particleData = args["particle"] as ParticleData<Any>
+ player.world.spawnParticle(particleData.particle(), player.location, 1)
+ }
+}
+
+Running this can result in errors due to missing requirements. If you provide a particle that has additional requirements, Bukkit will throw an error and the particle will not be displayed. Instead, the example below should be used.
+We can fix the issues with the example above by providing the data of the argument using the ParticleData
record:
/showparticle <particle>
+
+In this case, we'll use the World.spawnParticle(Particle particle, Location location, int count, T data)
method which accepts some particle data:
new CommandAPICommand("showparticle")
+ .withArguments(new ParticleArgument("particle"))
+ .executesPlayer((player, args) -> {
+ ParticleData<?> particleData = (ParticleData<?>) args.get("particle");
+ player.getWorld().spawnParticle(particleData.particle(), player.getLocation(), 1, particleData.data());
+ })
+ .register();
+
+CommandAPICommand("showparticle")
+ .withArguments(ParticleArgument("particle"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val particleData = args["particle"] as ParticleData<Any>
+ player.world.spawnParticle(particleData.particle(), player.location, 1, particleData.data())
+ })
+ .register()
+
+commandAPICommand("showparticle") {
+ particleArgument("particle")
+ playerExecutor { player, args ->
+ val particleData = args["particle"] as ParticleData<Any>
+ player.world.spawnParticle(particleData.particle(), player.location, 1, particleData.data())
+ }
+}
+
+This can be used with commands such as:
+/showparticle minecraft:dust_color_transition 0 0 0 20 1 0 0
+/showparticle minecraft:block_marker diamond_block
+
+The vibration
particle will return a particle data of the Bukkit Vibration
class. In the Vibration
class, you can access the destination location using the Vibration.getDestination()
method, which returns a Vibration.Destination
instance. The CommandAPI will always return a Vibration.Destination.BlockDestination
instance, and will never return a Vibration.Destination.EntityDestination
instance. An example of accessing the location can be found below:
ParticleData<Vibration> particleData; // The particle data you get from your argument
+Location destination = ((BlockDestination) particleData.data().getDestination()).getLocation();
+
+
+ The ParticleArgument
class represents Minecraft particles. This is casted to the CommandAPI's ParticleData
class.
ParticleData
classThe ParticleData
class is a record that contains two values:
Particle particle
, which is the Bukkit enum Particle
representation of what particle was providedT data
, which represents any additional particle data which was provided.public record ParticleData<T>(Particle particle, T data);
+
+The T data
can be used in Bukkit's World.spawnParticle(Particle particle, Location location, int count, T data)
method.
Particle data depends on your version of Minecraft. In 1.20.5, Minecraft and Spigot updated their particle API and they are no longer compatible with each other. Information about how the CommandAPI uses particle data can be found using the links below:
+ + +The PotionEffectArgument
class represents Minecraft potion effects. When used, this argument is casted to Bukkit's PotionEffectType
class, or alternatively a NamespacedKey
object if the PotionEffectArgument.NamespacedKey
argument is used to create a PotionEffectArgument
.
Say we wanted to have a command that gives a player a potion effect. For this command, we'll use the following syntax:
+/potion <target> <potion> <duration> <strength>
+
+In this example, we utilize some of the other arguments that we've described earlier, such as the PlayerArgument
and TimeArgument
. Since duration for the PotionEffect
constructor is in ticks, this is perfectly fit for the TimeArgument
, which is represented in ticks.
new CommandAPICommand("potion")
+ .withArguments(new PlayerArgument("target"))
+ .withArguments(new PotionEffectArgument("potion"))
+ .withArguments(new TimeArgument("duration"))
+ .withArguments(new IntegerArgument("strength"))
+ .executes((sender, args) -> {
+ Player target = (Player) args.get("target");
+ PotionEffectType potion = (PotionEffectType) args.get("potion");
+ int duration = (int) args.get("duration");
+ int strength = (int) args.get("strength");
+
+ // Add the potion effect to the target player
+ target.addPotionEffect(new PotionEffect(potion, duration, strength));
+ })
+ .register();
+
+new CommandAPICommand("potion")
+ .withArguments(new PlayerArgument("target"))
+ .withArguments(new PotionEffectArgument.NamespacedKey("potion"))
+ .withArguments(new TimeArgument("duration"))
+ .withArguments(new IntegerArgument("strength"))
+ .executes((sender, args) -> {
+ Player target = (Player) args.get("target");
+ NamespacedKey potionKey = (NamespacedKey) args.get("potion");
+ int duration = (int) args.get("duration");
+ int strength = (int) args.get("strength");
+
+ PotionEffectType potion = PotionEffectType.getByKey(potionKey);
+
+ // Add the potion effect to the target player
+ target.addPotionEffect(new PotionEffect(potion, duration, strength));
+ })
+ .register();
+
+CommandAPICommand("potion")
+ .withArguments(PlayerArgument("target"))
+ .withArguments(PotionEffectArgument("potion"))
+ .withArguments(TimeArgument("duration"))
+ .withArguments(IntegerArgument("strength"))
+ .executes(CommandExecutor { _, args ->
+ val target = args["target"] as Player
+ val potion = args["potion"] as PotionEffectType
+ val duration = args["duration"] as Int
+ val strength = args["strength"] as Int
+
+ // Add the potion effect to the target player
+ target.addPotionEffect(PotionEffect(potion, duration, strength))
+ })
+ .register()
+
+CommandAPICommand("potion")
+ .withArguments(PlayerArgument("target"))
+ .withArguments(PotionEffectArgument.NamespacedKey("potion"))
+ .withArguments(TimeArgument("duration"))
+ .withArguments(IntegerArgument("strength"))
+ .executes(CommandExecutor { _, args ->
+ val target = args["target"] as Player
+ val potionKey = args["potion"] as NamespacedKey
+ val duration = args["duration"] as Int
+ val strength = args["strength"] as Int
+
+ val potion = PotionEffectType.getByKey(potionKey)!!
+
+ // Add the potion effect to the target player
+ target.addPotionEffect(PotionEffect(potion, duration, strength))
+ })
+ .register()
+
+commandAPICommand("potion") {
+ playerArgument("target")
+ potionEffectArgument("potion")
+ timeArgument("duration")
+ integerArgument("strength")
+ anyExecutor { _, args ->
+ val target = args["target"] as Player
+ val potion = args["potion"] as PotionEffectType
+ val duration = args["duration"] as Int
+ val strength = args["strength"] as Int
+
+ // Add the potion effect to the target player
+ target.addPotionEffect(PotionEffect(potion, duration, strength))
+ }
+}
+
+commandAPICommand("potion") {
+ playerArgument("target")
+ potionEffectArgument("potion", true)
+ timeArgument("duration")
+ integerArgument("strength")
+ anyExecutor { _, args ->
+ val target = args["target"] as Player
+ val potionKey = args["potion"] as NamespacedKey
+ val duration = args["duration"] as Int
+ val strength = args["strength"] as Int
+
+ val potion = PotionEffectType.getByKey(potionKey)!!
+
+ // Add the potion effect to the target player
+ target.addPotionEffect(PotionEffect(potion, duration, strength))
+ }
+}
+
+Primitive arguments are arguments that represent Java primitive types, such as int
, float
, double
, boolean
and long
. These arguments are defined in their respective classes:
Primitive type | CommandAPI class |
---|---|
int | IntegerArgument |
float | FloatArgument |
double | DoubleArgument |
long | LongArgument |
boolean | BooleanArgument |
These arguments simply cast to their primitive type and don't need any extra work.
+The BooleanArgument
class represents the Boolean values true
and false
.
Say we want to create a plugin that lets you edit its own config.yml
file using a command. To do this, let's create a command with the following syntax:
/editconfig <config-key> <value>
+
+We first retrieve the keys from the configuration file using the typical Bukkit API. We construct our List
to hold our arguments, with the first parameter being a String key (in the form of a TextArgument
, overridden with an array of suggestions). Finally, we register our command and update the config, ensuring that we cast the BooleanArgument
to boolean
:
// Load keys from config file
+String[] configKeys = getConfig().getKeys(true).toArray(new String[0]);
+
+// Register our command
+new CommandAPICommand("editconfig")
+ .withArguments(new TextArgument("config-key").replaceSuggestions(ArgumentSuggestions.strings(info -> configKeys)))
+ .withArguments(new BooleanArgument("value"))
+ .executes((sender, args) -> {
+ // Update the config with the boolean argument
+ getConfig().set((String) args.get("config-key"), (boolean) args.get("value"));
+ })
+ .register();
+
+// Load keys from config file
+val configKeys: Array<String> = config.getKeys(true).toTypedArray()
+
+// Register our command
+CommandAPICommand("editconfig")
+ .withArguments(TextArgument("config-key").replaceSuggestions(ArgumentSuggestions.strings { _ -> configKeys }))
+ .withArguments(BooleanArgument("value"))
+ .executes(CommandExecutor { _, args ->
+ // Update the config with the boolean argument
+ config.set(args["config-key"] as String, args["value"] as Boolean)
+ })
+ .register()
+
+// Load keys from config file
+val configKeys: Array<String> = getConfig().getKeys(true).toTypedArray()
+
+// Register our command
+commandAPICommand("editconfig") {
+ argument(TextArgument("config-key").replaceSuggestions(ArgumentSuggestions.strings { configKeys }))
+ booleanArgument("value")
+ anyExecutor { _, args ->
+ // Update the config with the boolean argument
+ getConfig().set(args["config-key"] as String, args["value"] as Boolean)
+ }
+}
+
+Numbers are represented using the designated number classes:
+Class | Description |
---|---|
IntegerArgument | Whole numbers between Integer.MIN_VALUE and Integer.MAX_VALUE |
LongArgument | Whole numbers between Long.MIN_VALUE and Long.MAX_VALUE |
DoubleArgument | Double precision floating point numbers |
FloatArgument | Single precision floating point numbers |
Each numerical argument can have ranges applied to them, which restricts the user to only entering numbers from within a certain range. This is done using the constructor, and the range specified:
+Constructor | Description |
---|---|
new IntegerArgument() | Any range |
new IntegerArgument(min) | Values greater than or equal to min |
new IntegerArgument(min, max) | Values greater than or equal to min and less than or equal to max |
Each range is inclusive, so it includes the number given to it. If the minimum value provided is larger than the maximum value, an InvalidRangeException
is thrown.
Ranged arguments allow players to provide a range between two numbers, all within a single argument. The CommandAPI provides two ranged arguments, IntegerRangeArgument
for ranges with only integer values, and FloatRangeArgument
for ranged with potential floating point values.
These consist of values such as:
+Input | What it means |
---|---|
5 | The number 5 |
5..10 | Numbers between 5 and 10, including 5 and 10 |
5.. | Numbers greater than or equal to 5 (bounded by Java's max number size) |
..5 | Numbers less than or equal to 5 (bounded by Java's min number size) |
This allows you to let users define a range of values, which can be used to limit a value, such as the number of players in a region or for a random number generator.
+The CommandAPI returns an IntegerRange
from the IntegerRangeArgument
, and a FloatRange
from the FloatRangeArgument
, which represents the upper and lower bounds of the numbers provided by the command sender, as well as a method to check if a number is within that range.
The IntegerRange
class has the following methods:
class IntegerRange {
+ public int getLowerBound();
+ public int getUpperBound();
+ public boolean isInRange(int);
+}
+
+The FloatRange
class has the following methods:
class FloatRange {
+ public float getLowerBound();
+ public float getUpperBound();
+ public boolean isInRange(float);
+}
+
+Say you're working on a plugin for server administrators to help them find restricted items. A method of doing so would be to search chests in a given radius for certain items. As such, we can use the following syntax:
+/searchchests <range> <item>
+
+Now, we simply create our arguments using IntegerRangeArgument
for our range and ItemStackArgument
as the item to search for. We can then find all chests in a given area and determine if it is within the range provided by the command sender by using range.isInRange(distance)
:
new CommandAPICommand("searchrange")
+ .withArguments(new IntegerRangeArgument("range")) // Range argument
+ .withArguments(new ItemStackArgument("item")) // The item to search for
+ .executesPlayer((player, args) -> {
+ // Retrieve the range from the arguments
+ IntegerRange range = (IntegerRange) args.get("range");
+ ItemStack itemStack = (ItemStack) args.get("item");
+
+ // Store the locations of chests with certain items
+ List<Location> locations = new ArrayList<>();
+
+ // Iterate through all chunks, and then all tile entities within each chunk
+ for (Chunk chunk : player.getWorld().getLoadedChunks()) {
+ for (BlockState blockState : chunk.getTileEntities()) {
+
+ // The distance between the block and the player
+ int distance = (int) blockState.getLocation().distance(player.getLocation());
+
+ // Check if the distance is within the specified range
+ if (range.isInRange(distance)) {
+
+ // Check if the tile entity is a chest
+ if (blockState instanceof Chest chest) {
+
+ // Check if the chest contains the item specified by the player
+ if (chest.getInventory().contains(itemStack.getType())) {
+ locations.add(chest.getLocation());
+ }
+ }
+ }
+
+ }
+ }
+
+ // Output the locations of the chests, or whether no chests were found
+ if (locations.isEmpty()) {
+ player.sendMessage("No chests were found");
+ } else {
+ player.sendMessage("Found " + locations.size() + " chests:");
+ locations.forEach(location -> {
+ player.sendMessage(" Found at: "
+ + location.getX() + ", "
+ + location.getY() + ", "
+ + location.getZ());
+ });
+ }
+ })
+ .register();
+
+CommandAPICommand("searchrange")
+ .withArguments(IntegerRangeArgument("range")) // Range argument
+ .withArguments(ItemStackArgument("item")) // The item to search for
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ // Retrieve the range from the arguments
+ val range = args["range"] as IntegerRange
+ val itemStack = args["item"] as ItemStack
+
+ // Store the locations of chests with certain items
+ val locations = mutableListOf<Location>()
+
+ // Iterate through all chunks, and then all tile entities within each chunk
+ for (chunk in player.world.loadedChunks) {
+ for (blockState in chunk.tileEntities) {
+
+ // The distance between the block and the player
+ val distance = blockState.location.distance(player.location).toInt()
+
+ // Check if the distance is within the specified range
+ if (range.isInRange(distance)) {
+
+ // Check if the tile entity is a chest
+ if (blockState is Chest) {
+
+ // Check if the chest contains the item specified by the player
+ if (blockState.inventory.contains(itemStack.type)) {
+ locations.add(blockState.location)
+ }
+ }
+ }
+
+ }
+ }
+
+ // Output the locations of the chests, or whether no chests were found
+ if (locations.isEmpty()) {
+ player.sendMessage("No chests were found")
+ } else {
+ player.sendMessage("Found ${locations.size} chests:")
+ locations.forEach {
+ player.sendMessage(" Found at: ${it.x}, ${it.y}, ${it.z}")
+ }
+ }
+ })
+ .register()
+
+commandAPICommand("searchrange") {
+ integerRangeArgument("range") // Range argument
+ itemStackArgument("item") // The item to search for
+ playerExecutor { player, args ->
+ // Retrieve the range from the arguments
+ val range = args["range"] as IntegerRange
+ val itemStack = args["item"] as ItemStack
+
+ // Store the locations of chests with certain items
+ val locations = mutableListOf<Location>()
+
+ // Iterate through all chunks, and then all tile entities within each chunk
+ for (chunk in player.world.loadedChunks) {
+ for (blockState in chunk.tileEntities) {
+
+ // The distance between the block and the player
+ val distance = blockState.location.distance(player.location).toInt()
+
+ // Check if the distance is within the specified range
+ if (range.isInRange(distance)) {
+
+ // Check if the tile entity is a chest
+ if (blockState is Chest) {
+
+ // Check if the chest contains the item specified by the player
+ if (blockState.inventory.contains(itemStack.type)) {
+ locations.add(blockState.location)
+ }
+ }
+ }
+
+ }
+ }
+
+ // Output the locations of the chests, or whether no chests were found
+ if (locations.isEmpty()) {
+ player.sendMessage("No chests were found")
+ } else {
+ player.sendMessage("Found ${locations.size} chests:")
+ locations.forEach {
+ player.sendMessage(" Found at: ${it.x}, ${it.y}, ${it.z}")
+ }
+ }
+ }
+}
+
+The RecipeArgument
class lets you retrieve Bukkit's ComplexRecipe
object.
Say we want to give yourself the result of a specific recipe. Since Bukkit's Recipe
class contains the getResult()
method, we will use that in our example. We want to create the following command:
/giverecipe <recipe>
+
+As such, we easily implement it by specifying the RecipeArgument
, casting it and adding it to the player's inventory:
new CommandAPICommand("giverecipe")
+ .withArguments(new RecipeArgument("recipe"))
+ .executesPlayer((player, args) -> {
+ ComplexRecipe recipe = (ComplexRecipe) args.get("recipe");
+ player.getInventory().addItem(recipe.getResult());
+ })
+ .register();
+
+CommandAPICommand("giverecipe")
+ .withArguments(RecipeArgument("recipe"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val recipe = args["recipe"] as ComplexRecipe
+ player.inventory.addItem(recipe.result)
+ })
+ .register()
+
+commandAPICommand("giverecipe") {
+ recipeArgument("recipe")
+ playerExecutor { player, args ->
+ val recipe = args["recipe"] as ComplexRecipe
+ player.inventory.addItem(recipe.result)
+ }
+}
+
+In this example, we'll use the ComplexRecipe
's getKey()
method to write an example to to unlock a recipe for a player. For this command, we'll use the following syntax:
/unlockrecipe <player> <recipe>
+
+This is then implemented trivially as follows:
+new CommandAPICommand("unlockrecipe")
+ .withArguments(new PlayerArgument("player"))
+ .withArguments(new RecipeArgument("recipe"))
+ .executes((sender, args) -> {
+ Player target = (Player) args.get("player");
+ ComplexRecipe recipe = (ComplexRecipe) args.get("recipe");
+
+ target.discoverRecipe(recipe.getKey());
+ })
+ .register();
+
+CommandAPICommand("unlockrecipe")
+ .withArguments(PlayerArgument("player"))
+ .withArguments(RecipeArgument("recipe"))
+ .executes(CommandExecutor { _, args ->
+ val target = args["player"] as Player
+ val recipe = args["recipe"] as ComplexRecipe
+
+ target.discoverRecipe(recipe.key)
+ })
+ .register()
+
+commandAPICommand("unlockrecipe") {
+ playerArgument("player")
+ recipeArgument("recipe")
+ anyExecutor { _, args ->
+ val target = args["player"] as Player
+ val recipe = args["recipe"] as ComplexRecipe
+
+ target.discoverRecipe(recipe.key)
+ }
+}
+
+The RotationArgument
allows users to specify a pair of pitch and yaw coordinates. By default (using the ~
symbol), this refers to the player's current pitch and yaw of where they are looking at.
The RotationArgument
class returns a Rotation
object, which consists of the following methods:
Method name | What it does |
---|---|
float getPitch() | Returns a player's pitch (up and down rotation) |
float getYaw() | Returns a player's yaw (left and right rotation) |
float getNormalizedPitch() | Returns a player's pitch between -90 and 90 degrees |
float getNormalizedYaw() | Returns a player's yaw between -180 and 180 degrees |
Say we want to make an armor stand look in a certain direction. To do this, we'll use the following command:
+/rotate <rotation> <target>
+
+To do this, we'll use the rotation from the RotationArgument
and select an entity using the EntitySelectorArgument.OneEntity
class. We then check if our entity is an armor stand and if so, we set its head pose to the given rotation.
new CommandAPICommand("rotate")
+ .withArguments(new RotationArgument("rotation"))
+ .withArguments(new EntitySelectorArgument.OneEntity("target"))
+ .executes((sender, args) -> {
+ Rotation rotation = (Rotation) args.get("rotation");
+ Entity target = (Entity) args.get("target");
+
+ if (target instanceof ArmorStand armorStand) {
+ armorStand.setHeadPose(new EulerAngle(Math.toRadians(rotation.getPitch()), Math.toRadians(rotation.getYaw() - 90), 0));
+ }
+ })
+ .register();
+
+CommandAPICommand("rotate")
+ .withArguments(RotationArgument("rotation"))
+ .withArguments(EntitySelectorArgument.OneEntity("target"))
+ .executes(CommandExecutor { _, args ->
+ val rotation = args["rotation"] as Rotation
+ val target = args["target"] as Entity
+
+ if (target is ArmorStand) {
+ target.headPose = EulerAngle(Math.toRadians(rotation.pitch.toDouble()), Math.toRadians(rotation.yaw.toDouble() - 90), 0.0)
+ }
+ })
+ .register()
+
+commandAPICommand("rotate") {
+ rotationArgument("rotation")
+ entitySelectorArgumentOneEntity("target")
+ anyExecutor { _, args ->
+ val rotation = args["rotation"] as Rotation
+ val target = args["target"] as Entity
+
+ if (target is ArmorStand) {
+ target.headPose = EulerAngle(Math.toRadians(rotation.pitch.toDouble()), Math.toRadians(rotation.yaw.toDouble() - 90), 0.0)
+ }
+ }
+}
+
+Note how the head pose requires an EulerAngle
as opposed to a pitch and yaw. To account for this, we convert our rotation (which is in degrees) into an EulerAngle
in radians.
The CommandAPI uses two classes to provide information about a scoreboard:
+ScoreHolderArgument
class represents score holder - a player's name or an entity's UUID that has scores in an objective. This is described in more detail on the Minecraft Wiki.ScoreboardSlotArgument
class represents a display slot (sidebar, list or belowName) as well as the team color if the display is the sidebar. This is described in more detail on the Minecraft Wiki.The score holder argument can accept either a single entity or a collection of multiple entities. In order to specify which one to use, you must use the ScoreHolderArgument.Single
or ScoreHolderArgument.Multiple
constructor respectively:
new ScoreHolderArgument.Single(nodeName);
+new ScoreHolderArgument.Multiple(nodeName);
+
+Depending on which constructor is used, the cast type changes. If you use ScoreHolderArgument.Single
, the argument must be casted to a String
. Otherwise, if you use ScoreHolderArgument.Multiple
, the argument must be casted to a Collection<String>
.
Say we want to reward all players that fit a certain criteria. We want a command with the following syntax:
+/reward <players>
+
+Since we could have multiple players that fit a certain criterion, we want to use ScoreHolderArgument.Multiple
constructor.
To give this example a bit more context, let's say we want to reward all players that have died less than 10 times in the server. To do this, we will use the following command:
+/reward @e[type=player,scores={deaths=..9}]
+
+Note how we use ..9
to represent 9 or less deaths (since ranges are inclusive). Also note how we restrict our input to players via the command using type=player
. We can now implement our command:
new CommandAPICommand("reward")
+ // We want multiple players, so we use ScoreHolderType.MULTIPLE in the constructor
+ .withArguments(new ScoreHolderArgument.Multiple("players"))
+ .executes((sender, args) -> {
+ // Get player names by casting to Collection<String>
+ @SuppressWarnings("unchecked")
+ Collection<String> players = (Collection<String>) args.get("players");
+
+ for (String playerName : players) {
+ Bukkit.getPlayer(playerName).getInventory().addItem(new ItemStack(Material.DIAMOND, 3));
+ }
+ })
+ .register();
+
+CommandAPICommand("reward")
+ // We want multiple players, so we use the ScoreHolderArgument.Multiple constructor
+ .withArguments(ScoreHolderArgument.Multiple("players"))
+ .executes(CommandExecutor { _, args ->
+ // Get player names by casting to Collection<String>
+ val players = args["players"] as Collection<String>
+
+ for (playerName in players) {
+ Bukkit.getPlayer(playerName)?.inventory!!.addItem(ItemStack(Material.DIAMOND, 3))
+ }
+ })
+ .register()
+
+commandAPICommand("reward") {
+ // We want multiple players, so we use the scoreHolderArgumentMultiple method
+ scoreHolderArgumentMultiple("player")
+ anyExecutor { _, args ->
+ // Get player names by casting to Collection<String>
+ val players = args["player"] as Collection<String>
+
+ for (playerName in players) {
+ Bukkit.getPlayer(playerName)?.inventory!!.addItem(ItemStack(Material.DIAMOND, 3))
+ }
+ }
+}
+
+++Developer's Note:
+In the example above, we have our user use the
+@e[type=player]
entity selector to restrict theCollection<String>
so it only returns player names, which allows us to useBukkit.getPlayer(playerName)
. In practice, we cannot guarantee that such a selector will be used, so we could update the code to accept both entities and players. For example, we can differentiate between players and entities by using theUUID.fromString(String)
method:+Collection<String> entitiesAndPlayers = (Collection<String>) args.get(0); +for(String str : entitiesAndPlayers) { + try { + UUID uuid = UUID.fromString(str); + // Is a UUID, so it must by an entity + Bukkit.getEntity(uuid); + } catch(IllegalArgumentException exception) { + // Not a UUID, so it must be a player name + Bukkit.getPlayer(str); + } +} +
The ScoreboardSlotArgument
represents where scoreboard information is displayed. Since the Bukkit scoreboard DisplaySlot
is not able to represent the case where team colors are provided, the CommandAPI uses the ScoreboardSlot
wrapper class as the representation of the ScoreboardSlotArgument
.
ScoreboardSlot
wrapperThe ScoreboardSlot
wrapper class has 3 main methods:
enum ScoreboardSlot {
+
+ PLAYER_LIST,
+ SIDEBAR, // Unused, use the other SIDEBAR_TEAM_### values below
+ BELOW_NAME,
+ SIDEBAR_TEAM_BLACK,
+ SIDEBAR_TEAM_DARK_BLUE,
+ SIDEBAR_TEAM_DARK_GREEN,
+ SIDEBAR_TEAM_DARK_AQUA,
+ SIDEBAR_TEAM_DARK_RED,
+ SIDEBAR_TEAM_DARK_PURPLE,
+ SIDEBAR_TEAM_GOLD,
+ SIDEBAR_TEAM_GRAY,
+ SIDEBAR_TEAM_DARK_GRAY,
+ SIDEBAR_TEAM_BLUE,
+ SIDEBAR_TEAM_GREEN,
+ SIDEBAR_TEAM_AQUA,
+ SIDEBAR_TEAM_RED,
+ SIDEBAR_TEAM_LIGHT_PURPLE,
+ SIDEBAR_TEAM_YELLOW,
+ SIDEBAR_TEAM_WHITE;
+
+ public DisplaySlot getDisplaySlot();
+ public ChatColor getTeamColor();
+ public boolean hasTeamColor();
+
+ public NamespacedKey getKey();
+}
+
+The getDisplaySlot()
method returns the display slot that was chosen. Sidebar scoreboard colors can be accessed via ScoreboardSlot.SIDEBAR_TEAM_###
. You can also retrieve the color using the getTeamColor()
method.
Say we want to clear all objectives in a specific scoreboard slot. In this example, we will use the main server scoreboard, which is accessed using Bukkit.getScoreboardManager.getMainScoreboard()
. We want a command with the following syntax:
/clearobjectives <slot>
+
+We implement this simply by using the ScoreboardSlotArgument
as our argument, and then we can clear the slot using the scoreboard clearSlot(DisplaySlot)
method.
new CommandAPICommand("clearobjectives")
+ .withArguments(new ScoreboardSlotArgument("slot"))
+ .executes((sender, args) -> {
+ Scoreboard scoreboard = Bukkit.getScoreboardManager().getMainScoreboard();
+ DisplaySlot slot = ((ScoreboardSlot) args.get("slot")).getDisplaySlot();
+ scoreboard.clearSlot(slot);
+ })
+ .register();
+
+CommandAPICommand("clearobjectives")
+ .withArguments(ScoreboardSlotArgument("slot"))
+ .executes(CommandExecutor { _, args ->
+ val scoreboard = Bukkit.getScoreboardManager().mainScoreboard
+ val slot = (args["slot"] as ScoreboardSlot).displaySlot
+ scoreboard.clearSlot(slot)
+ })
+ .register()
+
+commandAPICommand("clearobjectives") {
+ scoreboardSlotArgument("slot")
+ anyExecutor { _, args ->
+ val scoreboard = Bukkit.getScoreboardManager().mainScoreboard
+ val slot = (args["slot"] as ScoreboardSlot).displaySlot
+ scoreboard.clearSlot(slot)
+ }
+}
+
+The SoundArgument
class allows a command sender to retrieve the Bukkit Sound
or NamespacedKey
object to represent in-game sound effects (such as mob sounds or ambient sound effects), as well as in-game music.
The SoundArgument
can return a Sound
or NamespacedKey
object. To return a Sound
object, simply use the SoundArgument
as normal. To return a NamespacedKey
object, use the SoundArgument.NamespacedKey
constructor instead:
// Makes a SoundArgument that returns a Sound
+new SoundArgument("sound");
+
+// Makes a SoundArgument that returns a NamespacedKey
+new SoundArgument.NamespacedKey("sound");
+
+When using the Sound
object, the CommandAPI will return null
if the specified Sound
could not be found. For this reason, it's recommended to use the NamespacedKey
object for optimal compatibility with client-side resourcepacks.
Say we want a simple command that plays a specific sound at your location. To do this, we will make the following command:
+/sound <sound>
+
+This command simply plays the provided sound to the current player:
+new CommandAPICommand("sound")
+ .withArguments(new SoundArgument("sound"))
+ .executesPlayer((player, args) -> {
+ player.getWorld().playSound(player.getLocation(), (Sound) args.get("sound"), 100.0f, 1.0f);
+ })
+ .register();
+
+new CommandAPICommand("sound")
+ .withArguments(new SoundArgument.NamespacedKey("sound"))
+ .executesPlayer((player, args) -> {
+ player.getWorld().playSound(player.getLocation(), ((NamespacedKey) args.get("sound")).asString(), 100.0f, 1.0f);
+ })
+ .register();
+
+CommandAPICommand("sound")
+ .withArguments(SoundArgument("sound"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ player.world.playSound(player.location, args["sound"] as Sound, 100.0f, 1.0f)
+ })
+ .register()
+
+CommandAPICommand("sound")
+ .withArguments(SoundArgument.NamespacedKey("sound"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ player.world.playSound(player.location, (args["sound"] as NamespacedKey).asString(), 100.0f, 1.0f)
+ })
+ .register()
+
+commandAPICommand("sound") {
+ soundArgument("sound")
+ playerExecutor { player, args ->
+ player.world.playSound(player.location, args["sound"] as Sound, 100.0f, 1.0f)
+ }
+}
+
+commandAPICommand("sound") {
+ soundArgument("sound", true)
+ playerExecutor { player, args ->
+ player.world.playSound(player.location, (args["sound"] as NamespacedKey).asString(), 100.0f, 1.0f)
+ }
+}
+
+There are three types of arguments that return Java's String
object. Each have their own unique set of features which make them suitable for specific needs.
The StringArgument
class is used to represent a single word. These words can only contain alphanumeric characters (A-Z, a-z and 0-9) and the underscore (_), plus (+), minus (-) and period (.) characters.
Accepted StringArgument
values:
Hello
+123
+hello123
+hello-123
+hello.WORLD
+Hello_world
+
+Rejected StringArgument
values:
hello@email.com
+yesn't
+
+The TextArgument
acts similar to any String in Java. These can be single words, like the StringArgument
, or have additional characters (e.g. spaces, symbols) if surrounded by quotes. To type quotation marks, you can use \"
(as similar to Java) to escape these special characters.
Accepted TextArgument
values:
hello
+"hello world!"
+"hello@gmail.com"
+"this has \" <<-- speech marks! "
+
+Rejected TextArgument
values:
hello world
+私
+"speech marks: ""
+
+++Greedy Arguments:
+The
+GreedyStringArgument
, similar to theChatArgument
uses the entire argument array from its current position. This means that it never ends, therefore if it is used, it must be the last element of yourList
of arguments.For example, if you have a command
+/message <message> <target>
, it would not be able to determine where the message ends and the<target>
argument begins.If a
+GreedyStringArgument
orChatArgument
is not declared at the end of theList
of arguments, or multiple of these arguments are used in the sameList
, the CommandAPI throws aGreedyArgumentException
.
The GreedyStringArgument
takes the TextArgument
a step further. Any characters and symbols are allowed and quotation marks are not required.
Say we have a simple message command of the following form:
+/message <target> <message>
+
+This would be ideal for a greedy string, since it can consume all text after the player's name:
+new CommandAPICommand("message")
+ .withArguments(new PlayerArgument("target"))
+ .withArguments(new GreedyStringArgument("message"))
+ .executes((sender, args) -> {
+ ((Player) args.get("target")).sendMessage((String) args.get("message"));
+ })
+ .register();
+
+CommandAPICommand("message")
+ .withArguments(PlayerArgument("target"))
+ .withArguments(GreedyStringArgument("message"))
+ .executes(CommandExecutor { _, args ->
+ (args["target"] as Player).sendMessage(args["message"] as String)
+ })
+ .register()
+
+commandAPICommand("message") {
+ playerArgument("target")
+ greedyStringArgument("message")
+ anyExecutor { _, args ->
+ (args["target"] as Player).sendMessage(args["message"] as String)
+ }
+}
+
+Any text entered after the <target>
argument would be sent to the player. For example, the command could be used as follows:
/message Skepter This is some incredibly long string with "symbols" and $p3c!aL characters~
+
+Note how this only works if the greedy string argument is at the end. If, say, the command was /message <message> <target>
, it would not be able to determine where the <message>
argument ends and the <target>
argument begins.
The TeamArgument
class interacts with the Minecraft scoreboard and represents a team.
Let's say we want to create a command to toggle the state of friendly fire in a team. We want a command of the following form
+/togglepvp <team>
+
+To do this, given a team we want to use the setAllowFriendlyFire(boolean)
function.
new CommandAPICommand("togglepvp")
+ .withArguments(new TeamArgument("team"))
+ .executes((sender, args) -> {
+ Team team = (Team) args.get("team");
+
+ // Toggle pvp
+ team.setAllowFriendlyFire(team.allowFriendlyFire());
+ })
+ .register();
+
+CommandAPICommand("togglepvp")
+ .withArguments(TeamArgument("team"))
+ .executes(CommandExecutor { _, args ->
+ val team = args["team"] as Team
+
+ // Toggle pvp
+ team.setAllowFriendlyFire(team.allowFriendlyFire())
+ })
+ .register()
+
+commandAPICommand("togglepvp") {
+ teamArgument("team")
+ anyExecutor { _, args ->
+ val team = args["team"] as Team
+
+ // Toggle pvp
+ team.setAllowFriendlyFire(team.allowFriendlyFire())
+ }
+}
+
+The TimeArgument
class represents in-game time, in the number of in-game ticks. This allows command senders to specify a certain number of ticks in a simpler way, by including the characters d
to specify the numbers of days, s
to specify the number of seconds or t
to specify a number of ticks.
The CommandAPI converts the inputs provided by the command sender into a number of ticks as an integer.
+++Developer's Note:
+The
+TimeArgument
provides inputs such as2d
(2 in-game days),10s
(10 seconds) and20t
(20 ticks), but does not let you combine them, such as2d10s
.
Say we have a command bigmsg
that displays a title message to all players for a certain duration:
/bigmsg <duration> <message>
+
+new CommandAPICommand("bigmsg")
+ .withArguments(new TimeArgument("duration"))
+ .withArguments(new GreedyStringArgument("message"))
+ .executes((sender, args) -> {
+ // Duration in ticks
+ int duration = (int) args.get("duration");
+ String message = (String) args.get("message");
+
+ for (Player player : Bukkit.getOnlinePlayers()) {
+ // Display the message to all players, with the default fade in/out times (10 and 20).
+ player.sendTitle(message, "", 10, duration, 20);
+ }
+ })
+ .register();
+
+CommandAPICommand("bigmsg")
+ .withArguments(TimeArgument("duration"))
+ .withArguments(GreedyStringArgument("message"))
+ .executes(CommandExecutor { _, args ->
+ // Duration in ticks
+ val duration = args["duration"] as Int
+ val message = args["message"] as String
+
+ for (player in Bukkit.getOnlinePlayers()) {
+ // Display the message to all players, with the default fade in/out times (10 and 20).
+ player.sendTitle(message, "", 10, duration, 20)
+ }
+ })
+ .register()
+
+commandAPICommand("bigmsg") {
+ timeArgument("duration")
+ greedyStringArgument("message")
+ anyExecutor { _, args ->
+ // Duration in ticks
+ val duration = args["duration"] as Int
+ val message = args["message"] as String
+
+ for (player in Bukkit.getOnlinePlayers()) {
+ // Display the message to all players, with the default fade in/out times (10 and 20).
+ player.sendTitle(message, "", 10, duration, 20)
+ }
+ }
+}
+
+The UUID argument is used to uniquely identify players, entities and attribute modifiers. As a result, its cast type is Java's UUID
.
The WorldArgument
class allows a command sender to refer to a loaded Bukkit World
.
Say we want to unload a world on our Minecraft server. We want to create a command with the following syntax:
+/unloadworld <world>
+
+Using the world from the WorldArgument
, we can then unload the world safely using Bukkit.getServer().unloadWorld()
and passing true
(to save chunks):
new CommandAPICommand("unloadworld")
+ .withArguments(new WorldArgument("world"))
+ .executes((sender, args) -> {
+ World world = (World) args.get("world");
+
+ // Unload the world (and save the world's chunks)
+ Bukkit.getServer().unloadWorld(world, true);
+ })
+ .register();
+
+CommandAPICommand("unloadworld")
+ .withArguments(WorldArgument("world"))
+ .executes(CommandExecutor { sender, args ->
+ val world = args["world"] as World
+
+ // Unload the world (and save the world's chunks)
+ Bukkit.getServer().unloadWorld(world, true)
+ })
+ .register()
+
+commandAPICommand("unloadworld") {
+ worldArgument("world")
+ anyExecutor { sender, args ->
+ val world = args["world"] as World
+
+ // Unload the world (and save the world's chunks)
+ Bukkit.getServer().unloadWorld(world, true)
+ }
+}
+
+Arguments in the CommandAPI are registered by using an Argument[]
or List<Argument>
object. There are two things you need to keep in mind when creating arguments:
By definition of a List
, the order of the elements inserted into it are preserved, meaning the order you add arguments to the List
will be the resulting order of which arguments are presented to the user when they run that command.
Adding arguments for registration is simple:
+// Create a List
+List<Argument> arguments = new ArrayList<>();
+
+// Add an argument with the node "target", which is a PlayerArgument
+arguments.add(new PlayerArgument("target"));
+
+The String value is the node that is registered into Minecraft's internal command graph. This name is also used as a prompt that is shown to a player when they are entering the command.
+The CommandAPI is very flexible when it comes to registering arguments, and lets you use a number of different methods to suit your preference:
+new CommandAPICommand("mycommand")
+ .withArguments(new StringArgument("arg0"))
+ .withArguments(new StringArgument("arg1"))
+ .withArguments(new StringArgument("arg2"))
+ // And so on
+
+CommandAPICommand("mycommand")
+ .withArguments(StringArgument("arg0"))
+ .withArguments(StringArgument("arg1"))
+ .withArguments(StringArgument("arg2"))
+ // And so on
+
+commandAPICommand("mycommand") {
+ stringArgument("arg0")
+ stringArgument("arg1")
+ stringArgument("arg2")
+ // And so on
+}
+
+new CommandAPICommand("mycommand")
+ .withArguments(new StringArgument("arg0"), new StringArgument("arg1"), new StringArgument("arg2"))
+ // And so on
+
+CommandAPICommand("mycommand")
+ .withArguments(StringArgument("arg0"), StringArgument("arg1"), StringArgument("arg2"))
+ // And so on
+
+commandAPICommand("mycommand") {
+ arguments(StringArgument("arg0"), StringArgument("arg1"), StringArgument("arg2"))
+ // And so on
+}
+
+List<Argument<?>> arguments = new ArrayList<>();
+arguments.add(new StringArgument("arg0"));
+arguments.add(new StringArgument("arg1"));
+arguments.add(new StringArgument("arg2"));
+
+new CommandAPICommand("mycommand")
+ .withArguments(arguments)
+ // And so on
+
+val arguments = listOf(
+ StringArgument("arg0"),
+ StringArgument("arg1"),
+ StringArgument("arg2")
+)
+
+CommandAPICommand("mycommand")
+ .withArguments(arguments)
+ // And so on
+
+val arguments = listOf(
+ StringArgument("arg0"),
+ StringArgument("arg1"),
+ StringArgument("arg2")
+)
+
+commandAPICommand("mycommand") {
+ arguments(*arguments.toTypedArray())
+ // And so on
+}
+
+To access arguments, they have to be casted to the type that the argument represents. The order of the arguments in the CommandArguments args
is the same as the order in which the arguments were declared.
List<Argument<?>> commandArguments = new ArrayList<>();
+commandArguments.add(new StringArgument("arg0"));
+commandArguments.add(new PotionEffectArgument("arg1"));
+commandArguments.add(new LocationArgument("arg2"));
+
+new CommandAPICommand("cmd")
+ .withArguments(commandArguments)
+ .executes((sender, args) -> {
+ String stringArg = (String) args.get("arg0");
+ PotionEffectType potionArg = (PotionEffectType) args.get("arg1");
+ Location locationArg = (Location) args.get("arg2");
+ })
+ .register();
+
+val commandArguments = listOf(
+ StringArgument("arg0"),
+ PotionEffectArgument("arg1"),
+ LocationArgument("arg2")
+)
+
+CommandAPICommand("cmd")
+ .withArguments(commandArguments)
+ .executes(CommandExecutor { _, args ->
+ val stringArg = args["arg0"] as String
+ val potionArg = args["arg1"] as PotionEffectType
+ val locationArg = args["arg2"] as Location
+ })
+ .register()
+
+val args = listOf(
+ StringArgument("arg0"),
+ PotionEffectArgument("arg1"),
+ LocationArgument("arg2")
+)
+
+commandAPICommand("cmd") {
+ arguments(*args.toTypedArray())
+ anyExecutor { _, args ->
+ val stringArg = args["arg0"] as String
+ val potionArg = args["arg1"] as PotionEffectType
+ val locationArg = args["arg2"] as Location
+ }
+}
+
+The type to cast each argument (declared in the dev.jorel.commandapi.arguments
package) is listed below:
Argument class | Data type |
---|---|
AngleArgument | float |
AdvancementArgument | org.bukkit.advancement.Advancement |
AdventureChatArgument | net.kyori.adventure.text.Component |
AdventureChatColorArgument | net.kyori.adventure.text.format.NamedTextColor |
AdventureChatComponentArgument | net.kyori.adventure.text.Component |
AxisArgument | java.util.EnumSet<org.bukkit.Axis> |
BiomeArgument | org.bukkit.block.Biome |
BiomeArgument.NamespacedKey | org.bukkit.NamespacedKey |
BlockPredicateArgument | java.util.function.Predicate <org.bukkit.block.Block> |
BlockStateArgument | org.bukkit.block.data.BlockData |
BooleanArgument | boolean |
ChatArgument | net.md_5.bungee.api.chat.BaseComponent[] |
ChatColorArgument | org.bukkit.ChatColor |
ChatComponentArgument | net.md_5.bungee.api.chat.BaseComponent[] |
CommandArgument | dev.jorel.commandapi.wrappers.CommandResult |
CustomArgument<T, B> | T |
DoubleArgument | double |
EnchantmentArgument | org.bukkit.enchantments.Enchantment |
EntitySelectorArgument.ManyEntities | Collection<org.bukkit.entity.Entity> |
EntitySelectorArgument.ManyPlayers | Collection<org.bukkit.entity.Player> |
EntitySelectorArgument.OneEntity | org.bukkit.entity.Entity |
EntitySelectorArgument.OnePlayer | org.bukkit.entity.Player |
EntityTypeArgument | org.bukkit.entity.EntityType |
FloatArgument | float |
FloatRangeArgument | dev.jorel.commandapi.wrappers.FloatRange |
FunctionArgument | dev.jorel.commandapi.wrappers.FunctionWrapper[] |
GreedyStringArgument | String |
IntegerArgument | int |
IntegerRangeArgument | dev.jorel.commandapi.wrappers.IntegerRange |
ItemStackArgument | org.bukkit.inventory.ItemStack |
ItemStackPredicateArgument | java.util.function.Predicate <org.bukkit.inventory.ItemStack> |
ListArgument | java.util.Collection<T> |
LiteralArgument | N/A |
Location2DArgument | dev.jorel.commandapi.wrappers.Location2D |
LocationArgument | org.bukkit.Location |
LongArgument | long |
LootTableArgument | org.bukkit.loot.LootTable |
MapArgument | java.util.LinkedHashMap |
MathOperationArgument | dev.jorel.commandapi.wrappers.MathOperation |
MultiLiteralArgument | String |
NamespacedKeyArgument | org.bukkit.NamespacedKey |
NBTCompoundArgument<T> | The cast type changes depending on whether you're shading the CommandAPI or using the CommandAPI as a plugin:
|
ObjectiveArgument | org.bukkit.scoreboard.Objective |
ObjectiveCriteriaArgument | String |
OfflinePlayerArgument | org.bukkit.OfflinePlayer |
ParticleArgument | dev.jorel.commandapi.wrappers.ParticleData |
PlayerArgument | org.bukkit.entity.Player |
PotionEffectArgument | org.bukkit.potion.PotionEffectType |
PotionEffectArgument.NamespacedKey | org.bukkit.NamespacedKey |
RecipeArgument | The cast type changes depending on your Minecraft version:
|
RotationArgument | dev.jorel.commandapi.wrappers.Rotation |
ScoreboardSlotArgument | dev.jorel.commandapi.wrappers.ScoreboardSlot |
ScoreHolderArgument.Single | String |
ScoreHolderArgument.Multiple | Collection<String> |
SoundArgument | org.bukkit.Sound |
SoundArgument.NamespacedKey | org.bukkit.NamespacedKey |
StringArgument | String |
TeamArgument | org.bukkit.scoreboard.Team |
TextArgument | String |
TimeArgument | int |
UUIDArgument | java.util.UUID |
WorldArgument | org.bukkit.World |
Sometimes, you want to modify the list of suggestions that are provided by an argument. To handle this, CommandAPI arguments have two methods:
+Argument replaceSuggestions(ArgumentSuggestions suggestions);
+Argument includeSuggestions(ArgumentSuggestions suggestions);
+
+The replaceSuggestions
method replaces all suggestions with the provided list of suggestions, whereas the includeSuggestions
method will include the provided suggestions with the suggestions already present by the argument.
Because argument suggestions are arguably the most powerful feature that the CommandAPI offers, I've split this section into a number of subsections. To give an overview on what CommandAPI argument suggestions can do:
+ArgumentSuggestions
interfaceThe two methods above require an ArgumentSuggestions
object, which is a functional interface that takes in a SuggestionInfo
record and the current Brigadier SuggestionsBuilder
and returns a CompletableFuture<Suggestions>
object. This may sound a bit complicated, but this allows you to implement very powerful suggestions using a combination of the CommandAPI and raw Brigadier API methods. More information about using Brigadier-level suggestions can be found in the brigadier suggestions section.
To simplify this, the CommandAPI provides a number of methods to generate suggestions:
+ArgumentSuggestions strings(String... suggestions);
+ArgumentSuggestions strings(Collection<String>);
+ArgumentSuggestions strings(Function<SuggestionInfo, String[]> suggestions);
+ArgumentSuggestions stringCollection(Function<SuggestionInfo<CommandSender>, Collection<String>>);
+ArgumentSuggestions stringsAsync(Function<SuggestionInfo, CompletableFuture<String[]>> suggestions);
+ArgumentSuggestions stringCollectionAsync(Function<SuggestionInfo<CommandSender>, CompletableFuture<Collection<String>>>);
+
+ArgumentSuggestions stringsWithTooltips(IStringTooltip... suggestions);
+ArgumentSuggestions stringsWithTooltips(Collection<IStringTooltip>);
+ArgumentSuggestions stringsWithTooltips(Function<SuggestionInfo, IStringTooltip[]> suggestions);
+ArgumentSuggestions stringsWithTooltipsCollection(Function<SuggestionInfo<CommandSender>, Collection<IStringTooltip>>);
+ArgumentSuggestions stringsWithTooltipsAsync(Function<SuggestionInfo, CompletableFuture<IStringTooltip[]>> suggestions);
+ArgumentSuggestions stringsWithTooltipsCollectionAsync(Function<SuggestionInfo<CommandSender>, CompletableFuture<Collection<IStringTooltip>>>);
+
+
+ For more information on argument casting types, see Argument Casting. This section is primarily about use cases for each argument and any implementation notes.
+ +In addition to normal suggestions, safely-typed suggestions and suggestions with tooltips, the CommandAPI can support computing the list of suggestions to send to a player asynchronously. This allows you to perform suggestions independent of the main server thread, which is useful for slow operations, such as:
+The asynchronous methods for suggestions are similar in format to normal suggestions, except for two things: the name of the method ends with Async
, and the return type for the inner function is a CompletableFuture
:
ArgumentSuggestions stringsAsync(Function<SuggestionInfo, CompletableFuture<String[]>> suggestions);
+ArgumentSuggestions stringsWithTooltipsAsync(Function<SuggestionInfo, CompletableFuture<IStringTooltip[]>> suggestions);
+
+SafeSuggestions<T> suggestAsync(Function<SuggestionInfo, CompletableFuture<T[]>> suggestions);
+SafeSuggestions<T> tooltipsAsync(Function<SuggestionInfo, CompletableFuture<Tooltip<T>[]>> suggestions);
+
+The easiest way to create a CompleteableFuture
for asynchronous suggestions is to use Java's CompletableFuture.supplyAsync()
method. If you have a simple array of string suggestions (String[]
), these can be turned into a completable future (CompletableFuture<String[]>
) using this method:
new String[] { "dirt", "grass", "cobblestone", };
+
+$$\downarrow$$
+CompletableFuture.supplyAsync(() -> {
+ return new String[] { "dirt", "grass", "cobblestone", };
+});
+
+++Developer's Note:
+As with all asynchronous operations running in a Minecraft server, you should not try to access the Bukkit API within an asynchronous block. If you want to run code which accesses Bukkit's API while inside an asynchronous block, you can schedule a synchronous task using the Bukkit scheduler, for example:
++CompletableFuture.supplyAsync(() -> { + + Bukkit.getScheduler().scheduleAsyncDelayedTask(plugin, () -> + // Your code here + ); + + return new String[] { "dirt", "grass", "cobblestone", }; +}); +
Say you wanted to write a command to modify your plugin's config file. Since the config file is an external file, you ideally want to access the file in a separate thread to the main server thread to retain performance for players on the server. We have the following command syntax:
+/setconfig <key> <value>
+
+We make use of the ArgumentSuggestions.stringsAsync
method to provide asynchronous suggestions. In our completable future implementation, we access the keys from the plugin configuration.
new CommandAPICommand("setconfig")
+ .withArguments(new StringArgument("key").replaceSuggestions(ArgumentSuggestions.stringsAsync(info -> {
+ return CompletableFuture.supplyAsync(() -> {
+ return plugin.getConfig().getKeys(false).toArray(new String[0]);
+ });
+ })))
+ .withArguments(new TextArgument("value"))
+ .executes((sender, args) -> {
+ String key = (String) args.get("key");
+ String value = (String) args.get("value");
+ plugin.getConfig().set(key, value);
+ })
+ .register();
+
+CommandAPICommand("setconfig")
+ .withArguments(StringArgument("key").replaceSuggestions(ArgumentSuggestions.stringsAsync { _ ->
+ CompletableFuture.supplyAsync { plugin.config.getKeys(false).toTypedArray() }
+ } ))
+ .withArguments(TextArgument("value"))
+ .executes(CommandExecutor { _, args ->
+ val key = args["key"] as String
+ val value = args["value"] as String
+ plugin.config.set(key, value)
+ })
+ .register()
+
+So far, we've been using only the CommandAPI to register commands. As a result, this makes the CommandAPI's features limited by whatever the CommandAPI has implemented. To push past these limits, the CommandAPI includes some extra methods to help with invoking brigadier methods. Of course, to use these methods, brigadier is required. The brigadier dependency's installation instructions can be found here.
+++Developer's Note:
+For those that are unaware, brigadier is Mojang's command parser and dispatching framework. This is what the CommandAPI wraps around and is the main underlying source of its functionality.
+
The CommandAPI has been designed in such a way that you shouldn't have to access NMS in order to make use of the more "advanced" arguments and features - if you find that NMS is required to do something, please make a new issue!
+The CommandAPI offers the following methods in the dev.jorel.commandapi.Brigadier
class:
public static CommandDispatcher getCommandDispatcher();
+public static RootCommandNode getRootNode();
+public static LiteralArgumentBuilder fromLiteralArgument(LiteralArgument literalArgument);
+public static RedirectModifier fromPredicate(BiPredicate<CommandSender, Object[]> predicate, List<Argument> args);
+public static Command fromCommand(CommandAPICommand command);
+public static RequiredArgumentBuilder fromArgument(List<Argument> args, Argument<?> argument);
+public static RequiredArgumentBuilder fromArgument(Argument argument);
+public static SuggestionProvider toSuggestions(Argument<?> argument, List<Argument> args);
+public static Object[] parseArguments(CommandContext cmdCtx, List<Argument> args);
+public static Object getBrigadierSourceFromCommandSender(CommandSender sender);
+public static CommandSender getBukkitCommandSenderFromContext(CommandContext cmdCtx);
+
+Briefly, here's what each of these functions do (you can view the JavaDocs for more information):
+Method | Description |
---|---|
getCommandDispatcher | Returns the Minecraft command dispatcher graph |
getRootNode | Returns the root node of the command dispatcher. This is equivalent to using getCommandDispatcher().getRoot(); |
fromLiteralArgument | Creates a LiteralArgumentBuilder from a LiteralArgument |
fromPredicate | Converts a predicate and some arguments into a RedirectModifier . This can be used for the fork method in brigadier's ArgumentBuilder |
fromCommand | Converts a CommandAPICommand into a brigadier Command object |
fromArgument | Converts an argument, or a list of arguments, into a RequiredArgumentBuilder |
toSuggestions | Converts an argument's suggestions into brigadier's SuggestionProvider , with a list of previously declared arguments |
parseArguments | Parses a list of CommandAPI arguments into their respective objects for a provided CommandContext |
getBrigadierSourceFromCommandSender | Converts a Bukkit CommandSender into the NMS command sender source object |
getBukkitCommandSenderFromContext | Converts a Brigadier CommandContext into a Bukkit CommandSender |
I hope these examples help understand how the CommandAPI can help with registering more "powerful" commands with the use of brigadier as well! Please bear with with it - these examples can be long, but I'm certain that they've been explained well and will be useful!
+Say we wanted to add a predicate to the /execute
command. In this example, we'll create a predicate which handles random chances. To illustrate this, we want to be able to run commands such as:
/execute if randomchance 1 4 run say Hello!
+
+In this scenario, if we ran this command, we would expect "Hello!" to appear in the chat with a \(\frac{1}{4}\) chance. In particular, this is what we're trying to achieve:
+We want to create a predicate (true/false value) for the following syntax:
+randomchance <numerator> <denominator>
+
+We also want this predicate to come after execute if
:
\begin{gather} +\texttt{execute}\\ +\downarrow\\ +\texttt{if}\\ +\downarrow\\ +\texttt{randomchance <numerator}\texttt{> <denominator}\texttt{>} +\end{gather}
+After entering our predicate, we want to route back to execute
(because the argument after execute
is run
, which is used in our example command above):
\begin{gather} +\texttt{execute}\\ +\downarrow\\ +\texttt{if}\\ +\downarrow\\ +\texttt{randomchance <numerator}\texttt{> <denominator}\texttt{>}\\ +\downarrow\\ +\texttt{execute} +\end{gather}
+Now that we've established what we want, we can finally begin writing the code! First we want to create a literal randomchance
. It's a literal because literal values don't change (similar to say run
or if
from the /execute
command). To create a literal, we'll use the fromLiteralArgument
method described above, and then build it using the .build()
method:
// Register literal "randomchance"
+LiteralCommandNode randomChance = Brigadier.fromLiteralArgument(new LiteralArgument("randomchance")).build();
+
+// Register literal "randomchance"
+val randomChance: LiteralCommandNode<Any> = Brigadier.fromLiteralArgument(LiteralArgument("randomchance")).build()
+
+With that completed, we can now create our "argument" to this predicate. To do this, we'll use the regular declaration of arguments that we would normally use for commands. In this example, because we're computing \(\frac{numerator}{denominator}\), we want our numerator to be 0 or greater and our denominator to be 1 or greater (we don't want any negative numbers or division by zero!):
+// Declare arguments like normal
+Argument<Integer> numeratorArgument = new IntegerArgument("numerator", 0);
+Argument<Integer> denominatorArgument = new IntegerArgument("denominator", 1);
+
+List<Argument> arguments = new ArrayList<>();
+arguments.add(numeratorArgument);
+arguments.add(denominatorArgument);
+
+// Declare arguments like normal
+val numeratorArgument = IntegerArgument("numerator", 0)
+val denominatorArgument = IntegerArgument("denominator", 1)
+
+val arguments = listOf<Argument<*>>(numeratorArgument, denominatorArgument)
+
+Now we're going to get into the very nitty-gritty part - the predicate declaration. First, we'll create some variables numerator
and denominator
to represent the brigadier instances of these arguments. This can be handled by using the Brigadier.argBuildOf
function:
ArgumentBuilder numerator = Brigadier.fromArgument(numeratorArgument);
+ArgumentBuilder denominator = Brigadier.fromArgument(denominatorArgument)
+
+val numerator = Brigadier.fromArgument(numeratorArgument)
+val denominator = Brigadier.fromArgument(denominatorArgument)
+
+Now we'll define our predicate. Since this is sort of a "meta-command" (it directly affects the outcome of the run
command), we need to use the ArgumentBuilder
's fork
method. Remember that after we run this predicate, we want to link back to execute
again, so our first argument is the CommandNode
for execute
, which we can get using Brigadier.getRootNode().getChild("execute")
. Then, we can simply use Brigadier.fromPredicate
to finish our declaration:
ArgumentBuilder denominator = Brigadier.fromArgument(denominatorArgument)
+ // Fork redirecting to "execute" and state our predicate
+ .fork(Brigadier.getRootNode().getChild("execute"), Brigadier.fromPredicate((sender, args) -> {
+ // Parse arguments like normal
+ int num = (int) args[0];
+ int denom = (int) args[1];
+
+ // Return boolean with a num/denom chance
+ return Math.ceil(Math.random() * denom) <= num;
+ }, arguments));
+
+val denominator = Brigadier.fromArgument(denominatorArgument)
+ // Fork redirecting to "execute" and state our predicate
+ .fork(Brigadier.getRootNode().getChild("execute"), Brigadier.fromPredicate( { _: CommandSender, args ->
+ // Parse arguments like normal
+ val num = (args[0] as Int).toDouble()
+ val denom = (args[1] as Int).toDouble()
+
+ // Return boolean with a num/denom chance
+ Math.ceil(Math.random() * denom) <= num
+ }, arguments))
+
+Finally, we can now link everything up. We know that numerator
comes first, then denominator
, so we have to have numerator.then(denominator)
. We also know that these arguments are the children of the randomChance
literal, so we use the following code to state all of this:
// Add <numerator> <denominator> as a child of randomchance
+randomChance.addChild(numerator.then(denominator).build());
+
+// Add <numerator> <denominator> as a child of randomchance
+randomChance.addChild(numerator.then(denominator).build())
+
+Finally, we "register" the command. In this case, we're actually just adding the randomChance
node under \(\texttt{execute}\rightarrow\texttt{if}\), which we can add using the following code:
// Add (randomchance <numerator> <denominator>) as a child of (execute -> if)
+Brigadier.getRootNode().getChild("execute").getChild("if").addChild(randomChance);
+
+// Add (randomchance <numerator> <denominator>) as a child of (execute -> if)
+Brigadier.getRootNode().getChild("execute").getChild("if").addChild(randomChance)
+
+So, hopefully that wasn't too confusing! If you're still lost, here's the whole code that we wrote:
+// Register literal "randomchance"
+LiteralCommandNode randomChance = Brigadier.fromLiteralArgument(new LiteralArgument("randomchance")).build();
+
+// Declare arguments like normal
+Argument<Integer> numeratorArgument = new IntegerArgument("numerator", 0);
+Argument<Integer> denominatorArgument = new IntegerArgument("denominator", 1);
+
+List<Argument> arguments = new ArrayList<>();
+arguments.add(numeratorArgument);
+arguments.add(denominatorArgument);
+
+// Get brigadier argument objects
+ArgumentBuilder numerator = Brigadier.fromArgument(numeratorArgument);
+ArgumentBuilder denominator = Brigadier.fromArgument(denominatorArgument)
+ // Fork redirecting to "execute" and state our predicate
+ .fork(Brigadier.getRootNode().getChild("execute"), Brigadier.fromPredicate((sender, args) -> {
+ // Parse arguments like normal
+ int num = (int) args[0];
+ int denom = (int) args[1];
+
+ // Return boolean with a num/denom chance
+ return Math.ceil(Math.random() * denom) <= num;
+ }, arguments));
+
+// Add <numerator> <denominator> as a child of randomchance
+randomChance.addChild(numerator.then(denominator).build());
+
+// Add (randomchance <numerator> <denominator>) as a child of (execute -> if)
+Brigadier.getRootNode().getChild("execute").getChild("if").addChild(randomChance);
+
+// Register literal "randomchance"
+val randomChance: LiteralCommandNode<Any> = Brigadier.fromLiteralArgument(LiteralArgument("randomchance")).build()
+
+// Declare arguments like normal
+val numeratorArgument = IntegerArgument("numerator", 0)
+val denominatorArgument = IntegerArgument("denominator", 1)
+
+val arguments = listOf<Argument<*>>(numeratorArgument, denominatorArgument)
+
+// Get brigadier argument objects
+val numerator = Brigadier.fromArgument(numeratorArgument)
+val denominator = Brigadier.fromArgument(denominatorArgument)
+ // Fork redirecting to "execute" and state our predicate
+ .fork(Brigadier.getRootNode().getChild("execute"), Brigadier.fromPredicate( { _: CommandSender, args ->
+ // Parse arguments like normal
+ val num = (args[0] as Int).toDouble()
+ val denom = (args[1] as Int).toDouble()
+
+ // Return boolean with a num/denom chance
+ Math.ceil(Math.random() * denom) <= num
+ }, arguments))
+
+// Add <numerator> <denominator> as a child of randomchance
+randomChance.addChild(numerator.then(denominator).build())
+
+// Add (randomchance <numerator> <denominator>) as a child of (execute -> if)
+Brigadier.getRootNode().getChild("execute").getChild("if").addChild(randomChance)
+
+As described in The ArgumentSuggestions interface, the ArgumentSuggestions
interface has the following default method:
@FunctionalInterface
+public interface ArgumentSuggestions<CommandSender> {
+
+ /**
+ * Create a {@link CompletableFuture} resolving onto a brigadier {@link Suggestions} object.
+ * @param info The suggestions info
+ * @param builder The Brigadier {@link SuggestionsBuilder} object
+ * @return a {@link CompletableFuture} resolving onto a brigadier {@link Suggestions} object.
+ *
+ * @throws CommandSyntaxException if there is an error making suggestions
+ */
+ CompletableFuture<Suggestions> suggest(SuggestionInfo<CommandSender> info, SuggestionsBuilder builder)
+ throws CommandSyntaxException;
+
+}
+
+This allows you to use Brigadier's SuggestionsBuilder
and Suggestions
classes to create more powerful suggestions beyond the basic capabilities of the CommandAPI.
In order to use this, you will need the Brigadier dependency, which you can find under the Brigadier installation instructions.
+Say we want to let users broadcast a message, but also allow them to enter emojis into the message they're typing:
+ +For this command, we'll use a GreedyStringArgument
as if we were making a generic broadcasted message. We create a map of emojis to their descriptions to use as tooltips and then we use Brigadier to display the suggestions at the end of the message where the cursor is.
Map<String, String> emojis = new HashMap<>();
+emojis.put("☻", "smile");
+emojis.put("❤", "heart");
+emojis.put("🔥", "fire");
+emojis.put("★", "star");
+emojis.put("☠", "death");
+emojis.put("⚠", "warning");
+emojis.put("☀", "sun");
+emojis.put("☺", "smile");
+emojis.put("☹", "frown");
+emojis.put("✉", "mail");
+emojis.put("☂", "umbrella");
+emojis.put("✘", "cross");
+emojis.put("♪", "music note (eighth)");
+emojis.put("♬", "music note (beamed sixteenth)");
+emojis.put("♩", "music note (quarter)");
+emojis.put("♫", "music note (beamed eighth)");
+emojis.put("☄", "comet");
+emojis.put("✦", "star");
+emojis.put("🗡", "sword");
+emojis.put("🪓", "axe");
+emojis.put("🔱", "trident");
+emojis.put("🎣", "fishing rod");
+emojis.put("🏹", "bow");
+emojis.put("⛏", "pickaxe");
+emojis.put("🍖", "food");
+
+Argument<String> messageArgument = new GreedyStringArgument("message")
+ .replaceSuggestions((info, builder) -> {
+ // Only display suggestions at the very end character
+ builder = builder.createOffset(builder.getStart() + info.currentArg().length());
+
+ // Suggest all the emojis!
+ for (Entry<String, String> str : emojis.entrySet()) {
+ builder.suggest(str.getKey(), new LiteralMessage(str.getValue()));
+ }
+
+ return builder.buildFuture();
+ });
+
+new CommandAPICommand("emoji")
+ .withArguments(messageArgument)
+ .executes((sender, args) -> {
+ Bukkit.broadcastMessage((String) args.get("message"));
+ })
+ .register();
+
+val emojis = mapOf(
+ "☻" to "smile",
+ "❤" to "heart",
+ "🔥" to "fire",
+ "★" to "star",
+ "☠" to "death",
+ "⚠" to "warning",
+ "☀" to "sun",
+ "☺" to "smile",
+ "☹" to "frown",
+ "✉" to "mail",
+ "☂" to "umbrella",
+ "✘" to "cross",
+ "♪" to "music note (eighth)",
+ "♬" to "music note (beamed sixteenth)",
+ "♩" to "music note (quarter)",
+ "♫" to "music note (beamed eighth)",
+ "☄" to "comet",
+ "✦" to "star",
+ "🗡" to "sword",
+ "🪓" to "axe",
+ "🔱" to "trident",
+ "🎣" to "fishing rod",
+ "🏹" to "bow",
+ "⛏" to "pickaxe",
+ "🍖" to "food"
+)
+
+val messageArgument = GreedyStringArgument("message")
+ .replaceSuggestions { info, builder ->
+ // Only display suggestions at the very end character
+ val newBuilder = builder.createOffset(builder.start + info.currentArg().length)
+
+ // Suggest all the emojis!
+ emojis.forEach { (emoji, description) ->
+ newBuilder.suggest(emoji, LiteralMessage(description))
+ }
+
+ newBuilder.buildFuture()
+ }
+
+CommandAPICommand("emoji")
+ .withArguments(messageArgument)
+ .executes(CommandExecutor { _, args ->
+ Bukkit.broadcastMessage(args["message"] as String)
+ })
+ .register()
+
+In this example, we simply create the GreedyStringArgument
and use replaceSuggestions()
to specify our suggestion rules. We create an offset using the current builder to make suggestions start at the last character (the current builder start builder.getStart()
and the current length of what the user has already typed info.currentArg().length()
). Finally, we build the suggestions with builder.buildFuture()
and then register our command as normal.
++Developer's Note:
+This example has been superseded by the Command argument. This example is still present as it gives an example of much more complicated brigadier suggestions which may be useful for readers!
+
Courtesy of 469512345, the following example shows how using Brigadier's suggestions and parser can be combined with the CommandAPI to create an argument which suggests valid Minecraft commands. This could be used for example as a sudo
command, to run a command as another player.
For this command, we'll use a GreedyStringArgument
because that allows users to enter any combination of characters (which therefore, allows users to enter any command). First, we start by defining the suggestions that we'll use for the GreedyStringArgument
. We'll use the ArgumentSuggestions
functional interface described above:
ArgumentSuggestions<CommandSender> commandSuggestions = (info, builder) -> {
+ // The current argument, which is a full command
+ String arg = info.currentArg();
+
+ // Identify the position of the current argument
+ int start;
+ if (arg.contains(" ")) {
+ // Current argument contains spaces - it starts after the last space and after the start of this argument.
+ start = builder.getStart() + arg.lastIndexOf(' ') + 1;
+ } else {
+ // Input starts at the start of this argument
+ start = builder.getStart();
+ }
+
+ // Parse command using brigadier
+ ParseResults<?> parseResults = Brigadier.getCommandDispatcher()
+ .parse(info.currentArg(), Brigadier.getBrigadierSourceFromCommandSender(info.sender()));
+
+ // Intercept any parsing errors indicating an invalid command
+ if(!parseResults.getExceptions().isEmpty()) {
+ CommandSyntaxException exception = parseResults.getExceptions().values().iterator().next();
+ // Raise the error, with the cursor offset to line up with the argument
+ throw new CommandSyntaxException(exception.getType(), exception.getRawMessage(), exception.getInput(), exception.getCursor() + start);
+ }
+
+ return Brigadier
+ .getCommandDispatcher()
+ .getCompletionSuggestions(parseResults)
+ .thenApply(suggestionsObject -> {
+ // Brigadier's suggestions
+ Suggestions suggestions = (Suggestions) suggestionsObject;
+
+ return new Suggestions(
+ // Offset the index range of the suggestions by the start of the current argument
+ new StringRange(start, start + suggestions.getRange().getLength()),
+ // Copy the suggestions
+ suggestions.getList()
+ );
+ });
+};
+
+val commandSuggestions: ArgumentSuggestions<CommandSender> = ArgumentSuggestions { info, builder ->
+ // The current argument, which is a full command
+ val arg: String = info.currentArg()
+
+ // Identify the position of the current argument
+ var start = if (arg.contains(" ")) {
+ // Current argument contains spaces - it starts after the last space and after the start of this argument.
+ builder.start + arg.lastIndexOf(' ') + 1
+ } else {
+ // Input starts at the start of this argument
+ builder.start
+ }
+
+ // Parse command using brigadier
+ val parseResults: ParseResults<*> = Brigadier.getCommandDispatcher()
+ .parse(info.currentArg(), Brigadier.getBrigadierSourceFromCommandSender(info.sender))
+
+ // Intercept any parsing errors indicating an invalid command
+ for ((_, exception) in parseResults.exceptions) {
+ // Raise the error, with the cursor offset to line up with the argument
+ throw CommandSyntaxException(exception.type, exception.rawMessage, exception.input, exception.cursor + start)
+ }
+
+ val completableFutureSuggestions: CompletableFuture<Suggestions> =
+ Brigadier.getCommandDispatcher().getCompletionSuggestions(parseResults) as CompletableFuture<Suggestions>
+
+ completableFutureSuggestions.thenApply { suggestions: Suggestions ->
+ Suggestions(
+ // Offset the index range of the suggestions by the start of the current argument
+ StringRange(start, start + suggestions.range.length),
+ // Copy the suggestions
+ suggestions.list
+ )
+ }
+}
+
+There's a lot to unpack there, but it's generally split up into 4 key sections:
+Finding the start of the argument. We find the start of the argument so we know where the beginning of our command suggestion is. This is done easily using builder.getStart()
, but we also have to take into account any spaces if our command argument contains spaces.
Parsing the command argument. We make use of Brigadier's parse()
method to parse the argument and generate some ParseResults
.
Reporting parsing errors. This is actually an optional step, but in general it's good practice to handle exceptions stored in ParseResults
. While Brigadier doesn't actually handle suggestion exceptions, this has been included in this example to showcase exception handling.
Generating suggestions from parse results. We use our parse results with Brigadier's getCompletionSuggestions()
method to generate some suggestions based on the parse results and the suggestion string range.
Now that we've declared our arguments suggestions, we can then create our simple command with the following syntax:
+/commandargument <command>
+
+We use the command suggestions declared above by using the replaceSuggestions
method in our GreedyStringArgument
, and write a simple executor which runs the command that the user provided:
new CommandAPICommand("commandargument")
+ .withArguments(new GreedyStringArgument("command").replaceSuggestions(commandSuggestions))
+ .executes((sender, args) -> {
+ // Run the command using Bukkit.dispatchCommand()
+ Bukkit.dispatchCommand(sender, (String) args.get("command"));
+ }).register();
+
+CommandAPICommand("commandargument")
+ .withArguments(GreedyStringArgument("command").replaceSuggestions(commandSuggestions))
+ .executes(CommandExecutor { sender, args ->
+ // Run the command using Bukkit.dispatchCommand()
+ Bukkit.dispatchCommand(sender, args["command"] as String)
+ })
+ .register()
+
+The scoreboard arguments that the CommandAPI provides allows you to interact with various scoreboard elements, such as objectives, teams and score holders.
+ +Chat preview is a feature introduced in Minecraft 1.19 that allows the server to display a preview of a chat message to the client before the client sends their message to the server. This chat preview feature is also compatible with /say
and /msg
, as well as the ChatArgument
and AdventureChatArgument
classes.
The chat preview feature is only present in Minecraft versions 1.19, 1.19.1 and 1.19.2. Chat preview was removed in 1.19.3, so this feature is unfortunately no longer usable in Minecraft 1.19.3 and beyond.
+To use chat preview, your server must have previews-chat
set to true
in the server.properties
file:
...
+previews-chat=true
+...
+
+For players that want to use chat preview, they must have Chat Preview
enabled in Options > Chat Settings...
The ChatArgument
and AdventureChatArgument
classes include a method, withPreview
:
public T withPreview(PreviewableFunction preview);
+
+The method withPreview(PreviewableFunction preview)
lets you generate a preview to send to the client. This method takes in the PreviewableFunction
functional interface, which is a function that takes in a PreviewInfo
and returns either a BaseComponent[]
(for ChatArgument
) or a Component
(for AdventureChatArgument
):
public T generatePreview(PreviewInfo info) throws WrapperCommandSyntaxException;
+
+The PreviewInfo
class is a record containing the following:
public record PreviewInfo<T> {
+ Player player();
+ String input();
+ String fullInput();
+ T parsedInput();
+}
+
+The following methods are as follows:
+Player player();
+
+player()
is the player that is currently typing a chat preview.
String input();
+
+input()
is the current input for the current ChatArgument
or AdventureChatArgument
. If a user is typing /mycommand hellowor¦
and the command syntax is /mycommand <ChatArgument>
, the result of input()
would be "hellowor"
.
String fullInput();
+
+fullInput()
is the full input that the player has typed, including the leading /
symbol which is required to start a command. If a user is typing /mycommand hellowor¦
, the result of fullInput()
would be "/mycommand hellowor"
.
T parsedInput();
+
+parsedInput()
is similar to input()
, except it has been parsed by the CommandAPI's argument parser. This is a representation of what the argument in the executor would look like. For a ChatArgument
the return type is BaseComponent[]
, and for AdventureChatArgument
the return type is Component
.
The ChatArgument
and AdventureChatArgument
classes also include a method, usePreview
:
public T usePreview(boolean usePreview);
+
+The usePreview(boolean usePreview)
method lets you specify whether you would like the previewing function to be used as the argument's value during execution. If set to true
, when the command's .executes()
method is called, the argument value (e.g. arg[0]
) will be the same as the content generated by the function provided to withPreview()
.
Say we wanted to make our own /broadcast
command that allowed the user to use &
chat colors. We can use chat preview to show users what the result of their /broadcast
command would look like before running the command. We'll use the following command syntax:
/broadcast <message>
+
+Because the ChatArgument
and AdventureChatArgument
can support entity selectors (such as @p
), it's best to use the info.parsedInput()
method to handle parsed entity selectors. In our code, we use the .withPreview()
method and take the parsed input and convert it to plain text. We then convert the plain text with &
characters into component text to be displayed to the user.
For execution, we do the same procedure, because the text that the user enters still has &
characters that need to be converted into a component.
new CommandAPICommand("broadcast")
+ .withArguments(new ChatArgument("message").withPreview(info -> {
+ // Convert parsed BaseComponent[] to plain text
+ String plainText = BaseComponent.toPlainText(info.parsedInput());
+
+ // Translate the & in plain text and generate a new BaseComponent[]
+ return TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', plainText));
+ }))
+ .executesPlayer((player, args) -> {
+ // The user still entered legacy text. We need to properly convert this
+ // to a BaseComponent[] by converting to plain text then to BaseComponent[]
+ String plainText = BaseComponent.toPlainText((BaseComponent[]) args.get("message"));
+ Bukkit.spigot().broadcast(TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', plainText)));
+ })
+ .register();
+
+new CommandAPICommand("broadcast")
+ .withArguments(new AdventureChatArgument("message").withPreview(info -> {
+ // Convert parsed Component to plain text
+ String plainText = PlainTextComponentSerializer.plainText().serialize(info.parsedInput());
+
+ // Translate the & in plain text and generate a new Component
+ return LegacyComponentSerializer.legacyAmpersand().deserialize(plainText);
+ }))
+ .executesPlayer((player, args) -> {
+ // The user still entered legacy text. We need to properly convert this
+ // to a Component by converting to plain text then to Component
+ String plainText = PlainTextComponentSerializer.plainText().serialize((Component) args.get("broadcast"));
+ Bukkit.broadcast(LegacyComponentSerializer.legacyAmpersand().deserialize(plainText));
+ })
+ .register();
+
+CommandAPICommand("broadcast")
+ .withArguments(ChatArgument("message").withPreview { info ->
+ // Convert parsed BaseComponent[] to plain text
+ val plainText: String = BaseComponent.toPlainText(*info.parsedInput() as Array<BaseComponent>)
+
+ // Translate the & in plain text and generate a new BaseComponent[]
+ TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', plainText))
+ } )
+ .executesPlayer(PlayerCommandExecutor { _, args ->
+ // The user still entered legacy text. We need to properly convert this
+ // to a BaseComponent[] by converting to plain text then to BaseComponent[]
+ val plainText: String = BaseComponent.toPlainText(*args["message"] as Array<BaseComponent>)
+ val baseComponents: Array<BaseComponent> = TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', plainText))
+ Bukkit.spigot().broadcast(*baseComponents)
+ })
+ .register()
+
+CommandAPICommand("broadcast")
+ .withArguments(AdventureChatArgument("message").withPreview { info ->
+ // Convert parsed Component to plain text
+ val plainText: String = PlainTextComponentSerializer.plainText().serialize(info.parsedInput() as Component)
+
+ // Translate the & in plain text and generate a new Component
+ LegacyComponentSerializer.legacyAmpersand().deserialize(plainText)
+ } )
+ .executesPlayer(PlayerCommandExecutor { _, args ->
+ // The user still entered legacy text. We need to properly convert this
+ // to a Component by converting to plain text then to Component
+ val plainText: String = PlainTextComponentSerializer.plainText().serialize(args["message"] as Component)
+ Bukkit.broadcast(LegacyComponentSerializer.legacyAmpersand().deserialize(plainText))
+ })
+ .register()
+
+usePreview()
Extending on the example above where we created a /broadcast
command with chat preview support, we can simplify the code by using .usePreview(true)
to use the preview function as the value of our argument in our executor function. We'll use the same command syntax as the previous example:
/broadcast <message>
+
+By using .usePreview(true)
, we don't have to re-translate &
formatting codes into their corresponding components because that has already been done by the preview function specified in .withPreview()
method.
new CommandAPICommand("broadcast")
+ .withArguments(new ChatArgument("message").usePreview(true).withPreview(info -> {
+ // Convert parsed BaseComponent[] to plain text
+ String plainText = BaseComponent.toPlainText(info.parsedInput());
+
+ // Translate the & in plain text and generate a new BaseComponent[]
+ return TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', plainText));
+ }))
+ .executesPlayer((player, args) -> {
+ Bukkit.spigot().broadcast((BaseComponent[]) args.get("message"));
+ })
+ .register();
+
+new CommandAPICommand("broadcast")
+ .withArguments(new AdventureChatArgument("message").usePreview(true).withPreview(info -> {
+ // Convert parsed Component to plain text
+ String plainText = PlainTextComponentSerializer.plainText().serialize(info.parsedInput());
+
+ // Translate the & in plain text and generate a new Component
+ return LegacyComponentSerializer.legacyAmpersand().deserialize(plainText);
+ }))
+ .executesPlayer((player, args) -> {
+ Bukkit.broadcast((Component) args.get("message"));
+ })
+ .register();
+
+CommandAPICommand("broadcast")
+ .withArguments(ChatArgument("message").usePreview(true).withPreview { info ->
+ // Convert parsed BaseComponent[] to plain text
+ val plainText = BaseComponent.toPlainText(*info.parsedInput() as Array<BaseComponent>)
+
+ // Translate the & in plain text and generate a new BaseComponent[]
+ TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', plainText))
+ } )
+ .executesPlayer(PlayerCommandExecutor { _, args ->
+ Bukkit.spigot().broadcast(*args["message"] as Array<BaseComponent>)
+ })
+ .register()
+
+CommandAPICommand("broadcast")
+ .withArguments(AdventureChatArgument("message").usePreview(true).withPreview { info ->
+ // Convert parsed Component to plain text
+ val plainText = PlainTextComponentSerializer.plainText().serialize(info.parsedInput() as Component)
+
+ // Translate the & in plain text and generate a new Component
+ LegacyComponentSerializer.legacyAmpersand().deserialize(plainText)
+ } )
+ .executesPlayer(PlayerCommandExecutor { _, args ->
+ Bukkit.broadcast(args["message"] as Component)
+ })
+ .register()
+
+The CommandArguments
class was introduced in CommandAPI 9.0.0 and provides a much more powerful way of accessing arguments than just an array of arguments which existed until 9.0.0.
While the argument array just gives the possibility to access the arguments via the array notation (args[0]
), the CommandArguments
class offers much more, including:
To access the inner structure of the CommandArguments
class directly, it provides various methods which you can learn about below:
Get the argument array
+Object[] args();
+
+This returns the array of arguments as defined when creating your command.
+Get the arguments mapped to their node name
+Map<String, Object> argsMap();
+
+This returns an unmodifiable map which contains the arguments mapped to their node names.
+Get the raw argument array
+String[] rawArgs();
+
+This returns the array of raw arguments. An explanation of what raw arguments are can be found in the section about accessing raw arguments.
+Get the raw arguments mapped to their node name
+Map<String, String> rawArgsMap();
+
+This returns an unmodifiable map which contains the raw arguments mapped to their node names. An explanation of what raw arguments are can be found in the section about accessing raw arguments.
+Other useful methods
+String fullInput(); // Returns the full command input (including the / character)
+int count(); // Returns the amount of arguments
+
+The CommandArguments
class provides its arguments in a way similar to how a List
or Map
let you access their contents. When using these methods, you need to cast the arguments to their respective type. The CommandArguments
class also provides a way to access unsafe arguments.
You can choose to access arguments by their node name or by their index.
+Accessing arguments by their node name is the recommended way of accessing arguments.
+There are four methods you can use to access arguments by their node name:
+Object get(String nodeName);
+Object getOrDefault(String nodeName, Object defaultValue);
+Object getOrDefault(String nodeName, Supplier<?> defaultValue);
+Optional<Object> getOptional(String nodeName);
+
+Accessing arguments by their index is the original way of accessing arguments. However, we recommend to access arguments by node name.
+Similar to the four methods of accessing arguments by their node name, there also are four methods you can use to access arguments by their index:
+Object get(int index);
+Object getOrDefault(int index, Object defaultValue);
+Object getOrDefault(int index, Supplier<?> defaultValue);
+Optional<Object> getOptional(int index);
+
+To demonstrate the different ways of accessing arguments, we want to register a command /mycommand
like this:
/mycommand <name> <amount>
+/mycommand <name> <amount> <player>
+/mycommand <name> <amount> <player> <target>
+/mycommand <name> <amount> <player> <target> <message>
+
+This is how these commands are implemented:
+new CommandAPICommand("mycommand")
+ .withArguments(new StringArgument("name"))
+ .withArguments(new IntegerArgument("amount"))
+ .withOptionalArguments(new PlayerArgument("player"))
+ .withOptionalArguments(new PlayerArgument("target"))
+ .withOptionalArguments(new GreedyStringArgument("message"))
+ .executesPlayer((player, args) -> {
+ String name = (String) args.get(0); // Access arguments by index
+ int amount = (int) args.get("amount"); // Access arguments by node name
+ Player p = (Player) args.getOrDefault("player", player); // Access arguments using the getOrDefault(String, Object) method
+ Player target = (Player) args.getOrDefault("target", () -> player); // Access arguments using the getOrDefault(String, Supplier<?>) method
+ String message = (String) args.getOptional("message").orElse("Hello!"); // Access arguments using the getOptional(String) method
+
+ // Do whatever with these values
+ })
+ .register();
+
+CommandAPICommand("mycommand")
+ .withArguments(StringArgument("name"))
+ .withArguments(IntegerArgument("amount"))
+ .withOptionalArguments(PlayerArgument("player"))
+ .withOptionalArguments(PlayerArgument("target"))
+ .withOptionalArguments(GreedyStringArgument("message"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val name = args[0] as String // Access arguments by index
+ val amount = args["amount"] as Int // Access arguments by node name
+ val p = args.getOrDefault("player", player) as Player // Access arguments using the getOrDefault(String, Object) method
+ val target = args.getOrDefault("target") { player } as Player // Access arguments using the getOrDefault(String, Supplier<?>) method
+ val message = args.getOptional("message").orElse("Hello!") as String // Access arguments using the getOptional(String) method
+
+ // Do whatever with these values
+ })
+ .register();
+
+A "raw argument" is the String
form of an argument as written in a command. For example:
A user defines a command /mycommand
that accepts a double
as the first argument and an entity selector as the second argument. It could be executed with the values 15.3
as the double
value and @e
as the entity selector:
/mycommand 15.3 @e
+
+When accessing the raw arguments of this command there are 15.3
and @e
available as String
s.
However, when accessing the arguments of this command there is 15.3
available as double
and @e
available as Collection<Entity>
.
Raw arguments are accessed basically the same way you would access arguments. You can access them by their node name and their index in the argument array.
+Accessing raw arguments by their node name is the recommended way of doing it.
+To access raw arguments by their node name, you can use these methods:
+String getRaw(String nodeName);
+String getOrDefaultRaw(String nodeName, String defaultValue);
+String getOrDefaultRaw(String nodeName, Supplier<String> defaultValue);
+Optional<String> getRawOptional(String nodeName);
+
+Of course, if you don't want to access raw arguments by their node name, we also provide the option to access them by index with these methods:
+String getRaw(int index);
+String getOrDefaultRaw(int index, String defaultValue);
+String getOrDefaultRaw(int index, Supplier<String> defaultValue);
+Optional<String> getRawOptional(int index);
+
+To demonstrate how to access raw arguments, we are going to implement the /mycommand
again, this time with the following syntax:
/mycommand <entities>
+
+We want to find out which entity selector is being used when the command is executed.
+new CommandAPICommand("mycommand")
+ .withArguments(new EntitySelectorArgument.ManyEntities("entities"))
+ .executesPlayer((player, args) -> {
+ String entitySelector = args.getRaw("entities"); // Access the raw argument with getRaw(String)
+
+ // Do whatever with the entity selector
+ })
+ .register();
+
+CommandAPICommand("mycommand")
+ .withArguments(EntitySelectorArgument.ManyEntities("entities"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val entitySelector = args.getRaw("entities")!! // Access the raw argument with getRaw(String)
+
+ // Do whatever with the entity selector
+ })
+ .register();
+
+When accessing arguments you need to cast the Object
returned by these methods to the type the argument returns. More about casting arguments here.
Unsafe arguments provide the ability to access an argument without needing to cast it to the argument's type. When not using unsafe arguments, your code looks like this:
+String name = (String) args.get("name");
+
+When using unsafe arguments you can make your code look like this:
+String name = args.getUnchecked("name");
+
+Unsafe arguments can also be accessed by their node names and their indices.
+Unsafe arguments can also be accessed by node name which, again, is the recommended way of doing it.
+Use these methods when accessing unsafe arguments by their node name:
+T getUnchecked(String nodeName);
+T getOrDefaultUnchecked(String nodeName, T defaultValue);
+T getOrDefaultUnchecked(String nodeName, Supplier<T> defaultValue);
+Optional<T> getOptionalUnchecked(String nodeName);
+
+If you want to access unsafe arguments by index, you can do that by using these methods:
+T getUnchecked(int index);
+T getOrDefaultUnchecked(int index, T defaultValue);
+T getOrDefaultUnchecked(int index, Supplier<T> defaultValue);
+Optional<T> getOptionalUnchecked(int index);
+
+Finally, we want to implement the /mycommand
again. This time we use this syntax:
/mycommand <player>
+
+Here, we don't actually want to cast the argument, so we use unsafe arguments to remove that cast:
+new CommandAPICommand("mycommand")
+ .withArguments(new PlayerArgument("player"))
+ .executesPlayer((player, args) -> {
+ Player p = args.getUnchecked("player");
+
+ // Do whatever with the player
+ })
+ .register();
+
+CommandAPICommand("mycommand")
+ .withArguments(PlayerArgument("player"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val p: Player = args.getUnchecked("player")!!
+
+ // Do whatever with the player
+ })
+ .register();
+
+Developer's Note:
+The following methods cannot be used to access a value returned by a CustomArgument
as its return type depends on the base argument for it.
Lastly, the CommandArguments class offers you a way to access your arguments in a more safe way by using internal casts. Again, methods are offered to access arguments by their +index or their node name:
+T getByClass(String nodeName, Class<T> argumentType);
+T getByClassOrDefault(String nodeName, Class<T> argumentType, T defaultValue);
+T getOptionalByClass(String nodeName, Class<T> argumentType);
+T getByClass(int index, Class<T> argumentType);
+T getByClassOrDefault(int index, Class<T> argumentType, T defaultValue);
+T getOptionalByClass(int index, Class<T> argumentType);
+
+Compared to the other methods the CommandArguments
class offers, these methods take an additional parameter of type Class<T>
where T
is the return type
+of the argument with the given node name or index.
For example, say you declared a new StringArgument("value")
and you now want to access the return value of this argument using safe casting. This would be done as follows:
String value = args.getByClass("value", String.class);
+
+val value = args.getByClass("value", String::class.java)
+
+Finally, there is one more, even safer way of accessing safe arguments: by using an argument instance:
+T getByArgument(Argument<T> argumentType);
+T getByArgumentOrDefault(Argument<T> argumentType, T defaultValue);
+T getOptionalByArgument(Argument<T> argumentType);
+
+However, while safer, this also introduces the need to first initialize your arguments before you can start implementing your command. +To visualize this, we want to implement the command from Access arguments by node name and index again, but this time using safe arguments with an argument instance:
+StringArgument nameArgument = new StringArgument("name");
+IntegerArgument amountArgument = new IntegerArgument("amount");
+PlayerArgument playerArgument = new PlayerArgument("player");
+PlayerArgument targetArgument = new PlayerArgument("target");
+GreedyStringArgument messageArgument = new GreedyStringArgument("message");
+
+new CommandAPICommand("mycommand")
+ .withArguments(nameArgument)
+ .withArguments(amountArgument)
+ .withOptionalArguments(playerArgument)
+ .withOptionalArguments(targetArgument)
+ .withOptionalArguments(messageArgument)
+ .executesPlayer((player, args) -> {
+ String name = args.getByArgument(nameArgument);
+ int amount = args.getByArgument(amountArgument);
+ Player p = args.getByArgumentOrDefault(playerArgument, player);
+ Player target = args.getByArgumentOrDefault(targetArgument, player);
+ String message = args.getOptionalByArgument(messageArgument).orElse("Hello!");
+
+ // Do whatever with these values
+ })
+ .register();
+
+val nameArgument = StringArgument("name")
+val amountArgument = IntegerArgument("amount")
+val playerArgument = PlayerArgument("player")
+val targetArgument = PlayerArgument("target")
+val messageArgument = GreedyStringArgument("message")
+
+CommandAPICommand("mycommand")
+ .withArguments(nameArgument)
+ .withArguments(amountArgument)
+ .withOptionalArguments(playerArgument)
+ .withOptionalArguments(targetArgument)
+ .withOptionalArguments(messageArgument)
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val name: String = args.getByArgument(nameArgument)!!
+ val amount: Int = args.getByArgument(amountArgument)!!
+ val p: Player = args.getByArgumentOrDefault(playerArgument, player)
+ val target: Player = args.getByArgumentOrDefault(targetArgument, player)
+ val message: String = args.getOptionalByArgument(messageArgument).orElse("Hello!")
+
+ // Do whatever with these values
+ })
+ .register();
+
+++Developer's Note:
+This section can be a little bit difficult to follow. If you only want the bare basic features (executes a command), read the section on Normal command executors - this behaves very similar to the
+onCommand
method in Bukkit.
The CommandAPI provides various command executors which are lambdas which execute the code you want when a command is called. With a lot of simplification, there are two main types of command executors:
+++Developer's Note:
+In general, you need not focus too much on what type of command executor to implement. If you know for certain that you're going to be using your command with command blocks, and specifically want to state whether a command returns a value, just ensure you return an integer at the end of your declared command executor. Java will infer the type (whether it's a normal command executor or a resulting command executor) automatically, so feel free to return an integer or not.
+
In addition to these two types of command executors, there are ways to restrict the execution of commands to certain CommandSender
subclasses. In other words, you can make commands executable by players in game only for instance. These restrictions are covered in more detail in Normal command executors.
Sometimes, you want your command to fail on purpose. This is the way to "gracefully" handle errors in your command execution. This is performed by throwing any of the following methods:
+throw CommandAPI.failWithString(String message);
+throw CommandAPI.failWithMessage(Message message);
+throw CommandAPIBukkit.failWithBaseComponents(BaseComponent... message);
+throw CommandAPIBukkit.failWithAdventureComponent(Component message);
+throw CommandAPIBukkit.failWithAdventureComponent(ComponentLike message);
+
+When the CommandAPI handles the fail method, it will cause the command to return a success value of 0, to indicate failure.
+Say we have some list containing fruit and the player can choose from it. In order to do that, we can use a StringArgument
and suggest it to the player using .replaceSuggestions(info -> String[])
. However, because this only lists suggestions to the player, it does not stop the player from entering an option that isn't on the list of suggestions.
Therefore, to gracefully handle this with a proper error message, we use one of the CommandAPI.failWithXXX()
methods above with a meaningful error message which is displayed to the user.
// Array of fruit
+String[] fruit = new String[] {"banana", "apple", "orange"};
+
+// Register the command
+new CommandAPICommand("getfruit")
+ .withArguments(new StringArgument("item").replaceSuggestions(ArgumentSuggestions.strings(fruit)))
+ .executes((sender, args) -> {
+ String inputFruit = (String) args.get("item");
+
+ if (Arrays.stream(fruit).anyMatch(inputFruit::equals)) {
+ // Do something with inputFruit
+ } else {
+ // The sender's input is not in the list of fruit
+ throw CommandAPI.failWithString("That fruit doesn't exist!");
+ }
+ })
+ .register();
+
+// List of fruit
+val fruit = listOf<String>("banana", "apple", "orange")
+
+// Register the command
+CommandAPICommand("getfruit")
+ .withArguments(StringArgument("item").replaceSuggestions(ArgumentSuggestions.strings(fruit)))
+ .executes(CommandExecutor { _, args ->
+ val inputFruit = args["item"] as String
+
+ if(fruit.any { it == inputFruit }) {
+ // Do something with inputFruit
+ } else {
+ // The sender's input is not in the list of fruit
+ throw CommandAPI.failWithString("That fruit doesn't exist!")
+ }
+ })
+ .register()
+
+++ +Developer's Note:
+In general, it's a good idea to handle unexpected cases with one of the
+CommandAPI.failWithXXX()
methods. Most arguments used by the CommandAPI will have their own built-in failsafe system (e.g. theEntitySelectorArgument
will not execute the command executor if it fails to find an entity), so this feature is for those extra cases.
To register commands with the CommandAPI, we use the CommandAPICommand
class. It follows a simple builder pattern to improve readability.
I think the easiest way to explain it is with an example:
+// Create our command
+new CommandAPICommand("broadcastmsg")
+ .withArguments(new GreedyStringArgument("message")) // The arguments
+ .withAliases("broadcast", "broadcastmessage") // Command aliases
+ .withPermission(CommandPermission.OP) // Required permissions
+ .executes((sender, args) -> {
+ String message = (String) args.get("message");
+ Bukkit.getServer().broadcastMessage(message);
+ })
+ .register();
+
+// Create our command
+CommandAPICommand("broadcastmsg")
+ .withArguments(GreedyStringArgument("message")) // The arguments
+ .withAliases("broadcast", "broadcastmessage") // Command aliases
+ .withPermission(CommandPermission.OP) // Required permissions
+ .executes(CommandExecutor { sender, args ->
+ val message = args["message"] as String
+ Bukkit.getServer().broadcastMessage(message)
+ })
+ .register()
+
+First, we create a new CommandAPICommand
, with the name of the command that the sender must enter to run it.
Then, we create an argument to add to the command using withArguments
. This is described in more detail in the section on arguments.
In this example, we add an alias, "broadcast", to the command. This allows the sender to use either /broadcastmsg <message>
or /broadcast <message>
.
By using withPermission
, we require the sender to be an OP in order to run the command.
We control what the command does using executes
(this is described in more detail in the section on command executors).
Finally, we register the command to the CommandAPI using register
.
That's it! This simple snippet of code fully registers the command to the server. You don't need to supply a plugin instance, you don't have to create a custom class and you don't have to mess with the plugin.yml
file.
Throughout this documentation, we will use the various different methods for command registration to give you an idea of when and where certain methods are more suitable than others.
+CommandAPICommand
methodsThe CommandAPICommand
has various methods, which are outlined below:
new CommandAPICommand(String commandName)
+
+This constructor creates a new instance of the CommandAPICommand
object. This constructor requires the name of the command.
CommandAPICommand withArguments(List<Argument> arguments)
+CommandAPICommand withArguments(Argument... arguments)
+
+The withArguments
method is used to add arguments to your command. The arguments
parameter is appended to the the list of arguments for the command.
CommandAPICommand withPermission(CommandPermission)
+CommandAPICommand withPermission(String)
+
+The withPermission
method is used to assign a permission that is required to execute the command. (See the section on permissions for more info).
CommandAPICommand withRequirements(sender -> {})
+
+The withRequirements
method is used to assign additional constraints required to execute the command, similar to permissions. (See the section on requirements for more info).
CommandAPICommand withAliases(String... args)
+
+The withAliases
method is used to declare a list of aliases that can be used to run this command via. (See the section on aliases for more info).
CommandAPICommand withHelp(String shortDescription, fullDescription)
+CommandAPICommand withHelp(HelpTopic helpTopic)
+CommandAPICommand withShortDescription(String shortDescription)
+CommandAPICommand withFullDescription(String fullDescription)
+
+The withHelp
method, along with its specific withShortDescription
and withFullDescription
methods are used to declare the help topic for this command which is displayed in the /help
command. (See the section on help for more info).
CommandAPICommand withSubcommand(CommandAPICommand subcommand)
+
+The withSubcommand
method is used to declare a subcommand that leads on from the current command. (See the section on subcommands for more info).
CommandAPICommand executes((sender, args) -> {})
+CommandAPICommand executes(info -> {})
+
+Executes a command using the CommandSender
object.
CommandAPICommand executesPlayer((player, args) -> {})
+CommandAPICommand executesPlayer(info -> {})
+
+Executes a command only if the command sender is a Player
.
CommandAPICommand executesEntity((entity, args) -> {})
+CommandAPICommand executesEntity(info -> {})
+
+Executes a command only if the command sender is an Entity
.
CommandAPICommand executesCommandBlock((cmdblock, args) -> {})
+CommandAPICommand executesCommandBlock(info -> {})
+
+Executes a command only if the command sender is a BlockCommandSender
.
CommandAPICommand executesConsole((console, args) -> {})
+CommandAPICommand executesConsole(info -> {})
+
+Executes a command only if the command sender is a ConsoleCommandSender
.
CommandAPICommand executesProxy((proxy, args) -> {})
+CommandAPICommand executesProxy(info -> {})
+
+Executes a command only if the command sender is a ProxiedCommandSender
.
CommandAPICommand executesNative((proxy, args) -> {})
+CommandAPICommand executesNative(info -> {})
+
+Executes a command regardless of what the command sender is, using the NativeProxyCommandSender
. Read more about native proxied command senders here.
++Developer's Note:
+Sometimes, the Java compiler throws an error saying that a method is ambiguous for the type CommandAPICommand. This is due to a limitation in Java's type inference system and is not a fault of the CommandAPI. If we take the following code, used to spawn a pig:
++new CommandAPICommand("spawnpigs") + .executesPlayer((player, args) -> { + for(int i = 0; i < 10; i++) { + player.getWorld().spawnEntity(player.getLocation(), (EntityType) args.get(0)); + } + }) + .register(); +
The Java type inference system cannot determine what the type of the lambda
+(player, args) -> ()
is, therefore it produces the following compilation error:+The method executesPlayer(PlayerCommandExecutor) is ambiguous for the type CommandAPICommand +
This can easily be resolved by declaring the specific type of the command sender and the arguments. For example:
++new CommandAPICommand("spawnpigs") + .executesPlayer((Player player, CommandArguments args) -> { + for(int i = 0; i < 10; i++) { + player.getWorld().spawnEntity(player.getLocation(), (EntityType) args.get(0)); + } + }) + .register(); +
void register()
+
+Registers the command with the default minecraft
namespace. If you are shading you can set the default namespace using CommandAPIConfig#setNamespace(String)
or CommandAPIBukkitConfig#usePluginNamespace()
.
void register(String namespace)
+
+Registers the command with a custom provided namespace.
+void register(JavaPlugin plugin)
+
+Registers the command with the provided plugin's name.
+It is recommended to register commands in either the onLoad()
or onEnable()
method. With the CommandAPI, depending on whether you use onLoad()
or onEnable()
to load your commands depends on whether your plugin is used with Minecraft's functions:
When to load | What to do |
---|---|
onLoad() method | Register commands to be used in Minecraft functions (see the Function section for more info) |
onEnable() method | Register regular commands |
The CommandAPI does support registering commands outside of these methods while the server is running. Commands registered after the server is done loading should work the same as commands registered in onEnable
.
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.
executes()
and then()
methodsThe 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.
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();
+
+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.
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("target");
+ target.sendMessage("Hi");
+ }))
+ .register();
+
+CommandTree("sayhi")
+ .executes(CommandExecutor { sender, _ ->
+ sender.sendMessage("Hi!")
+ })
+ .then(PlayerArgument("target")
+ .executes(CommandExecutor { _, args ->
+ val target = args["target"] 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.
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("line_number");
+ String text = (String) args.get("text");
+ 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("line_number");
+ 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("line_number");
+ 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("line_number");
+ 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["line_number"] as Int
+ val text = args["text"] 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["line_number"] 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["line_number"] 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["line_number"] as Int
+ sign.setLine(line_number - 1, player.getMetadata("copied_sign_text")[0].asString())
+ sign.update(true)
+ })))
+ .register()
+
+The CommandAPI allows you to remove commands completely from Minecraft's command list. This includes Vanilla commands, Bukkit commands, and plugin commands.
+There are three methods you might use when unregistering commands:
+CommandAPI.unregister(String commandName);
+CommandAPI.unregister(String commandName, boolean unregisterNamespaces);
+CommandAPIBukkit.unregister(String commandName, boolean unregisterNamespaces, boolean unregisterBukkit);
+
+To understand when and how to use these methods, you need to know a little about how Bukkit loads and sets up commands. This is basically the order of events when a Bukkit server starts:
+bukkit
namespace (E.g. bukkit:version
)onLoad
is calledplugin.yml
file and placed in the Bukkit CommandMapluckperms:lp
)onEnable
is calledminecraft
namespace (E.g. minecraft:gamemode
)Unregistering a command only works if it happens after the command is created. Bukkit's command system is special and has two locations where commands can exist -- either the Vanilla CommandDispatcher or the Bukkit CommandMap -- so you also need to know where your command is registered. With that in mind, here is what each of the unregister
methods do:
CommandAPI.unregister(String commandName);
+
+Unregisters a command from the Vanilla CommandDispatcher.
+CommandAPI.unregister(String commandName, boolean unregisterNamespaces);
+
+Unregisters a command from the Vanilla CommandDispatcher. If unregisterNamespaces
is true
, then any namespaced version of the command is also unregistered.
CommandAPIBukkit.unregister(String commandName, boolean unregisterNamespaces, boolean unregisterBukkit);
+
+Unregisters a command from Bukkit. As before, if unregisterNamespaces
is true
, then any namespaced version of the command is also unregistered. If unregisterBukkit
is true
, then only Bukkit commands in the Bukkit CommandMap are unregistered. If unregisterBukkit
is false
, only commands from the Vanilla CommandDispatcher are unregistered.
To give a better idea of how and when to use these methods, the rest of this page documents how to unregister different types of commands.
+/version
/version
is a command provided by Bukkit. Looking at the sequence of events above, that means it is created during step 2, before plugins are loaded in step 3. Consequently, the command will exist when our plugin's onLoad
method is called, so we'll unregister it there. The same code will work in onEnable
too, since step 4 is also after step 2.
Since this command exists in the Bukkit CommandMap, we'll need to use CommandAPIBukkit#unregister
with unregisterBukkit
set to true
. We'll also remove the namespaced version -- /bukkit:version
-- so unregisterNamespaces
will be true
. All together, the code looks like this:
@Override
+public void onLoad() {
+ CommandAPIBukkit.unregister("version", true, true);
+}
+
+override fun onLoad() {
+ CommandAPIBukkit.unregister("version", false, true)
+}
+
+With this plugin, executing /version
or /bukkit:version
will give the unknown command message. Note that aliases like /ver
and its namespaced version /bukkit:ver
will still work. To remove aliases as well, you need to unregister each as its own command. For, /ver
, that would mean calling CommandAPIBukkit.unregister("ver", true, true)
.
/gamemode
/gamemode
is a command provided by Vanilla Minecraft. Like the previous example, Vanilla commands are created in step 1, before plugins are loaded in step 3. For variety, we'll unregister the command in our plugin's onEnable
-- step 4 -- but the same code would also work in onLoad
.
Since this command exists in the Vanilla CommandDispatcher, we can use CommandAPI#unregister
. That works the same as CommandAPIBukkit#unregister
with unregisterBukkit
set to false
. We don't care about the namespace, so unregisterNamespaces
will be false
. That means we can use the simplest method, CommandAPI.unregister(String commandName)
, since it sets unregisterNamespaces
to false
by default. All together, the code looks like this:
@Override
+public void onEnable() {
+ CommandAPI.unregister("gamemode");
+}
+
+override fun onEnable() {
+ CommandAPI.unregister("gamemode")
+}
+
+With this code, executing /gamemode
will give the unknown command exception as expected. However, even though unregisterNamespaces
was false
, /minecraft:gamemode
can also not be run. This happens because Vanilla commands are given their namespace in step 6, after our plugin has removed /gamemode
.
When the server starts, /gamemode
is created in step 2 inside the Vanilla CommandDispatcher. In step 4, our plugin is enabled and we remove the /gamemode
command from that CommandDispatcher. After all the plugins enable, step 6 moves all commands in the Vanilla CommandDispatcher to the Bukkit CommandMap and gives them the minecraft
namespace. Since /gamemode
doesn't exist at this point, step 6 cannot create the /minecraft:gamemode
command. So, even though unregisterNamespaces
was false
, /minecraft:gamemode
doesn't exist anyway.
/gamemode
commandTo replace a command, first unregister the original command, then register a new implementation for that command.
+@Override
+public void onEnable() {
+ CommandAPI.unregister("gamemode");
+
+ // Register our new /gamemode, with survival, creative, adventure and spectator
+ new CommandAPICommand("gamemode")
+ .withArguments(new MultiLiteralArgument("gamemodes", "survival", "creative", "adventure", "spectator"))
+ .executes((sender, args) -> {
+ // Implementation of our /gamemode command
+ })
+ .register();
+}
+
+override fun onEnable() {
+ CommandAPI.unregister("gamemode");
+
+ // Register our new /gamemode, with survival, creative, adventure and spectator
+ CommandAPICommand("gamemode")
+ .withArguments(MultiLiteralArgument("gamemodes", "survival", "creative", "adventure", "spectator"))
+ .executes(CommandExecutor { sender, args ->
+ // Implementation of our /gamemode command
+ })
+ .register()
+}
+
+Now, when /gamemode
is executed, it will use the new implementation defined using the CommandAPI.
/luckperms:luckperms
The /luckperms
command is provided by the Bukkit LuckPerms plugin. Plugin commands are created during step 4, immediately before calling the onEnable
method of the respective plugin. In this case, unregistering the command in our own plugin's onLoad
would not work, since the command wouldn't exist yet. We also have to make sure that our onEnable
method is called after LuckPerm's. The best way to make sure that happens is to add LuckPerms as a depend
or softdepend
in our plugin's plugin.yml. You can read more about the different between depend
and softdepend
in Spigot's documentation, but that will look something like this:
name: MyPlugin
+main: some.package.name.Main
+version: 1.0
+depend:
+ - LuckPerms
+
+Since plugin commands are stored in the Bukkit CommandMap, we need to use CommandAPIBukkit#unregister
with unregisterBukkit
set to true
. For demonstration’s sake, we only want to unregister the namespaced version -- /luckperms:luckperms
-- and leave /luckperms
alone. To do this, give "luckperms:luckperms"
as the commandName
, and set unregisterNamespaces
to false
. All together, the code looks like this:
@Override
+public void onEnable() {
+ CommandAPIBukkit.unregister("luckperms:luckperms", false, true);
+}
+
+override fun onEnable() {
+ CommandAPIBukkit.unregister("luckperms:luckperms", false, true)
+}
+
+Executing /luckperms
will work as normal, but /luckperms:luckperms
will give the unknown command message.
Unregistering a command created by the CommandAPI is similar to both unregistering a Vanilla command and a plugin command. Like a Vanilla command, CommandAPI commands are stored in the Vanilla CommandDispatcher, so they should be unregistered with unregisterBukkit
set to false
. Like plugin commands, they may be created in onEnable
, so you need to make sure your plugin is enabled after the plugin that adds the command.
Unlike plugin commands, CommandAPI commands may be created in onLoad
, as discussed in Command loading order. That just means you may also be able to unregister the command in you own plugin's onLoad
. As always, simply make sure you unregister a command after it is created, and it will be removed properly.
For our example, let's say we want to unregister the /break
command created by the Bukkit Maven Example Project for the CommandAPI. If you look at that plugin's code, you can see that it registers the /break
command in it's onEnable
method. Therefore, we can unregister the command in our own plugin's onEnable
, making sure that our plugin will enable second by adding ExamplePlugin as a depend
or softdepend
.
name: MyPlugin
+main: some.package.name.Main
+version: 1.0
+depend:
+ - CommandAPI
+ - ExamplePlugin
+
+++Developer's Note:
+If you can't find the code where a CommandAPI command is registered or just don't have access to the code of a plugin, you can still figure out when a command is registered. If you set
+verbose-outputs
totrue
in the CommandAPI's configuration, it will log command registration.For the ExamplePlugin, setting
+verbose-outputs
totrue
gives this:+[Server thread/INFO]: [ExamplePlugin] Enabling ExamplePlugin v0.0.1 +[Server thread/INFO]: [CommandAPI] Registering command /break block<LocationArgument> +[Server thread/INFO]: [CommandAPI] Registering command /myeffect target<PlayerArgument> potion<PotionEffectArgument> +[Server thread/INFO]: [CommandAPI] Registering command /nbt nbt<NBTCompoundArgument> +
You can see that the ExamplePlugin registers its commands when
+onEnable
is called.
In summary, we will unregister the /break
command in our plugin's onEnable
. We added Example plugin to the depend
list in our plugin.yml so that our onEnable
method runs second. unregisterNamespaces
and unregisterBukkit
will be set to false
, and those are the default values, so we can simply use CommandAPI.unregister(String commandName)
. All together, the code looks like this:
@Override
+public void onEnable() {
+ CommandAPI.unregister("break");
+}
+
+override fun onEnable() {
+ CommandAPI.unregister("break")
+}
+
+Now, when you try to execute /break
, you will just get the unknown command message as if it never existed.
/help
If you look at the sequence of events at the top of this page, you might notice that Bukkit's /help
command gets its own place in step 5. Unlike the other Bukkit commands, /help
is special and gets registered after plugins are loaded and enabled (don't ask, I don't know why :P). That means unregistering /help
in onLoad
or onEnable
does not work, since the command doesn't exist yet.
In order to run our unregister task after the server is enabled, we can use Bukkit's Scheduler API. There are many ways to set up and run a task, and this should work in whatever way you like. You can even give the task zero delay, since Bukkit only starts processing tasks after the server is enabled.
+Since /help
is in the Bukkit CommandMap, we need to use CommandAPIBukkit#unregister
with unregisterBukkit
set to true
. We'll leave /bukkit:help
alone, so unregisterNamespaces
will be false
. All together, we can unregister Bukkit's /help
command with this code:
@Override
+public void onEnable() {
+ new BukkitRunnable() {
+ @Override
+ public void run() {
+ CommandAPIBukkit.unregister("help", false, true);
+ }
+ }.runTaskLater(this, 0);
+}
+
+override fun onEnable() {
+ object : BukkitRunnable() {
+ override fun run() {
+ CommandAPIBukkit.unregister("help", false, true)
+ }
+ }.runTaskLater(this, 0)
+}
+
+Funnily, if you try to execute /help
, the server will still tell you: Unknown command. Type "/help" for help.
. Luckily, unregisterNamespaces
was false
, so you can still use /bukkit:help
to figure out your problem.
/minecraft:gamemode
In the earlier example for Unregistering /gamemode
, even though unregisterNamespaces
was false
, the /minecraft:gamemode
command was also not executable. As explained up there, this happens because the namespaced version of commands in the Vanilla CommandDispatcher are not created until after plugins are loaded and enabled. Since we unregistered /gamemode
in onEnable
, when the time came for the server to transfer Vanilla commands into the Bukkit CommandMap, it didn't know to create the minecraft:gamemode
command. Consequently, this means we cannot normally remove only the /minecraft:gamemode
command without also unregistering /gamemode
.
Of course, it is still possible to only unregister /minecraft:gamemode
and the namespaced versions of other Vanilla commands. As always, in order to unregister a command, you have to unregister after the command is created. So, we just need to unregister /minecraft:gamemode
after the server is enabled. Like the previous special case, we can use Bukkit's Scheduler API to run our unregister task after the server is enabled.
While /minecraft:gamemode
only exists in the Bukkit CommandMap, it is the namespaced version of the Vanilla /gamemode
command, so it is considered a Vanilla command. That means unregisterBukkit
should be false
, which is what it defaults to when using CommandAPI#unregister
. The CommandAPI understands that once the server is enabled Vanilla commands will have been copied to the CommandMap, so it will be able to find /minecraft:gamemode
Finally, unregisterNamespaces
should be false
, and since that's the default value we don't have to include it. All together, the code looks like this:
@Override
+public void onEnable() {
+ new BukkitRunnable() {
+ @Override
+ public void run() {
+ CommandAPI.unregister("minecraft:gamemode");
+ }
+ }.runTaskLater(this, 0);
+}
+
+override fun onEnable() {
+ object : BukkitRunnable() {
+ override fun run() {
+ CommandAPI.unregister("minecraft:gamemode")
+ }
+ }.runTaskLater(this, 0)
+}
+
+With this code, /gamemode
will execute as normal, but /minecraft:gamemode
will give the unknown command message.
Developer's Note:
+Doing the opposite action here -- only unregistering /gamemode
but keeping /minecraft:gamemode
-- is not recommended. That would be the following code, where commandName
is "gamemode"
(or any command in the Vanilla CommandDispatcher), and unregisterNamespaces
is false
:
// NOT RECOMMENDED
+@Override
+public void onEnable() {
+ new BukkitRunnable() {
+ @Override
+ public void run() {
+ CommandAPI.unregister("gamemode");
+ }
+ }.runTaskLater(this, 0);
+}
+
+// NOT RECOMMENDED
+override fun onEnable() {
+ object : BukkitRunnable() {
+ override fun run() {
+ CommandAPI.unregister("gamemode")
+ }
+ }.runTaskLater(this, 0)
+}
+
+The expected outcome of this code is that /minecraft:gamemode
would work as expected, and /gamemode
would give the command not found message. However, that is only true for the player's commands. If you try to use /minecraft:gamemode
in the console, it will not work properly. Specifically, while you can tab-complete the command's label, minecraft:gamemode
the command's arguments will not have any suggestions. If you try to execute /minecraft:gamemode
in the console, it will always tell you your command is unknown or incomplete.
The main point is that if you ever try to unregister a Vanilla command after the server is enabled, the namespaced version of that command will break for the console. To avoid this issue, always set unregisterNamespaces
to true
if unregisterBukkit
is false
when unregistering commands after the server is enabled.
@Override
+public void onEnable() {
+ new BukkitRunnable() {
+ @Override
+ public void run() {
+ CommandAPI.unregister("gamemode", true);
+ }
+ }.runTaskLater(this, 0);
+}
+
+override fun onEnable() {
+ object : BukkitRunnable() {
+ override fun run() {
+ CommandAPI.unregister("gamemode", true)
+ }
+ }.runTaskLater(this, 0)
+}
+
+The CommandAPI has a few configuration options to change how it functions. These options can be set in the plugins/CommandAPI/config.yml
file, which is generated automatically when the CommandAPI runs for the first time.
config.yml
fileThe default config.yml
is shown below:
################################################################################
+# Logging #
+################################################################################
+
+# Verbose outputs (default: false)
+# If "true", outputs command registration and unregistration logs in the console
+verbose-outputs: false
+
+# Silent logs (default: false)
+# If "true", turns off all logging from the CommandAPI, except for errors.
+silent-logs: false
+
+################################################################################
+# Messages #
+################################################################################
+
+# Messages
+# Controls messages that the CommandAPI displays to players
+messages:
+
+ # Missing executor implementation (default: "This command has no implementations for %s")
+ # The message to display to senders when a command has no executor. Available
+ # parameters are:
+ # %s - the executor class (lowercase)
+ # %S - the executor class (normal case)
+ missing-executor-implementation: "This command has no implementations for %s"
+
+################################################################################
+# Debugging #
+################################################################################
+
+# Create dispatcher JSON (default: false)
+# If "true", the CommandAPI creates a command_registration.json file showing the
+# mapping of registered commands. This is designed to be used by developers -
+# setting this to "false" will improve command registration performance.
+create-dispatcher-json: false
+
+# Use latest version (default: false)
+# If "true", the CommandAPI will use the latest available NMS implementation
+# when the CommandAPI is used. This avoids all checks to see if the latest NMS
+# implementation is actually compatible with the current Minecraft version.
+use-latest-nms-version: false
+
+# Hook into Paper's ServerResourcesReloadedEvent (default: true)
+# If "true", and the CommandAPI detects it is running on a Paper server, it will
+# hook into Paper's ServerResourcesReloadedEvent to detect when /minecraft:reload is run.
+# This allows the CommandAPI to automatically call its custom datapack-reloading
+# function which allows CommandAPI commands to be used in datapacks.
+# If you set this to false, CommandAPI commands may not work inside datapacks after
+# reloading datapacks.
+hook-paper-reload: true
+
+# Skips the initial datapack reload when the server loads (default: false)
+# If "true", the CommandAPI will not reload datapacks when the server has finished
+# loading. Datapacks will still be reloaded if performed manually when "hook-paper-reload"
+# is set to "true" and /minecraft:reload is run.
+skip-initial-datapack-reload: false
+
+################################################################################
+# Command conversion #
+################################################################################
+
+# Plugins to convert (default: [])
+# Controls the list of plugins to process for command conversion.
+plugins-to-convert: []
+
+# Other commands to convert (default: [])
+# A list of other commands to convert. This should be used for commands which
+# are not declared in a plugin.yml file.
+other-commands-to-convert: []
+
+# Skip sender proxy (default: [])
+# Determines whether the proxy sender should be skipped when converting a
+# command. If you are having issues with plugin command conversion, add the
+# plugin to this list.
+skip-sender-proxy: []
+
+verbose-outputs
If true
, outputs command registration and unregistration logs in the console. This is primarily used for developers to identify issues with command registration.
Default value
+verbose-outputs: false
+
+Example value
+verbose-outputs: true
+
+silent-logs
If true
, turns off all logging from the CommandAPI, except for errors.
Default value
+silent-logs: false
+
+Example value
+silent-logs: true
+
+messages
Controls messages that the CommandAPI displays to players. Available messages:
+missing-executor-implementation
- the message to display to senders when a command has no executor. This message supports format parameters:
+%s
- the executor class (lowercase). For example "craftplayer"%S
- the executor class (normal case). For example "CraftPlayer"Default value
+messages:
+ missing-executor-implementation: "This command has no implementations for %s"
+
+create-dispatcher-json
Controls whether the CommandAPI should generate a command_registration.json
file showing the mapping of registered commands.
This is primarily designed to be used by developers. Setting this to false
will slightly improve command registration performance.
The command_registration.json
JSON representation of commands is in the same format as Minecraft's Data Generators Commands report. The format is Brigadier's command graph - more information about the JSON format can be found here.
Default value
+create-dispatcher-json: false
+
+Example value
+create-dispatcher-json: true
+
+use-latest-nms-version
Controls whether the CommandAPI should use the latest NMS implementation for command registration and execution.
+This setting can be used to run the CommandAPI on Minecraft versions higher than it can support. For example, if the CommandAPI supports Minecraft 1.18 and Minecraft 1.18.1 comes out, you can use this to enable support for 1.18.1 before an official CommandAPI release comes out that supports 1.18.1.
+This feature is very experimental and should only be used if you know what you are doing. In almost every case, it is better to wait for an official CommandAPI release that supports the latest version of Minecraft. Using use-latest-nms-version
is not guaranteed to work and can cause unexpected side-effects!
Default value
+use-latest-nms-version: false
+
+Example value
+use-latest-nms-version: true
+
+hook-paper-reload
Controls whether the CommandAPI hooks into the Paper-exclusive ServerResourcesReloadedEvent
when available.
When the CommandAPI detects it is running on a Paper-based server, its default behavior will be to hook into the ServerResourcesReloadedEvent
, which triggers when /minecraft:reload
is run. During this event, the CommandAPI runs a custom datapack reloading sequence that helps commands registered with the CommandAPI work within datapacks. See Reloading datapacks for more information on this process.
By default, this value is set to true
and the CommandAPI will hook into the ServerResourcesReloadedEvent
. If you want, you can set this to false
, and the CommandAPI will not hook into this event.
Default value
+hook-paper-reload: true
+
+Example value
+hook-paper-reload: false
+
+skip-initial-datapack-reload
Controls whether the CommandAPI should perform its initial datapack reload when the server has finished loading.
+The CommandAPI automatically reloads all datapacks in a similar fashion to /minecraft:reload
in order to propagate CommandAPI commands into datapack functions and tags. This operation may cause a slight delay to server startup and is not necessary if you are not using datapacks or functions that use CommandAPI commands. This operation can be skipped by setting this value to true
.
Note that datapacks will still be reloaded if performed manually when hook-paper-reload
is set to true
and you run /minecraft:reload
.
Default value
+skip-initial-datapack-reload: false
+
+Example value
+skip-initial-datapack-reload: true
+
+plugins-to-convert
Controls the list of plugins to process for command conversion. See Command conversion for more information.
+Default value
+plugins-to-convert: []
+
+Example values
+plugins-to-convert:
+ - Essentials: ~
+
+plugins-to-convert:
+ - Essentials:
+ - speed
+ - hat
+ - MyPlugin:
+ - mycommand
+ - MyOtherPlugin: ~
+
+plugins-to-convert:
+ - Essentials:
+ - speed <speed>[0..10]
+ - speed <target>[minecraft:game_profile]
+ - speed (walk|fly) <speed>[0..10]
+ - speed (walk|fly) <speed>[0..10] <target>[minecraft:game_profile]
+
+skip-sender-proxy
Determines whether the proxy sender should be skipped when converting a command. See Skipping proxy senders for more information.
+Default value
+skip-sender-proxy: []
+
+Example value
+skip-sender-proxy:
+ - SkinsRestorer
+ - MyPlugin
+
+other-commands-to-convert
A list of other commands to convert. This should be used for commands which are not declared in a plugin.yml
file. See Arbitrary command conversion for more information.
Default value
+other-commands-to-convert: []
+
+Example value
+other-commands-to-convert:
+ - /set
+ - mycommand
+
+
+ ++ +Dev note:
+Coming soon! In this section, I'll outline how to get started with building and contributing to the CommandAPI.
+
++Developer's Note:
+If you're a server owner, you're probably lost! This section is for developer command conversion. If you're looking for how to convert plugins with the
+config.yml
file, you want 2. Configuration for server owners.
The CommandAPI has the ability to convert plugin commands to vanilla Minecraft commands using its config.yml
's plugins-to-convert
option. Nevertheless, the API for command conversion is not hidden and you're free to use it as you see fit!
Before you continue, let's clear up a few naming conventions which is used in the following sections!
+boolean onCommand(CommandSender ... )
methodTo register all commands that are declared by a target plugin, the Converter.convert(Plugin)
method can be used. This attempts to register all commands declared in a target plugin's plugin.yml
file, as well as any aliases or permissions stated in the plugin.yml
file.
Say you have some plugin.yml
file for a target plugin that adds some basic functionality to a server. The target plugin in this example is called "TargetPlugin":
name: TargetPlugin
+main: some.random.package.Main
+version: 1.0
+commands:
+ gmc:
+ aliases: gm1
+ gms:
+ i:
+ permission: item.permission
+
+As you can see, it declares 3 commands: /gmc
, /gms
and /i
. We can now begin writing your plugin that uses the CommandAPI converter. We will call this plugin "YourPlugin":
public class YourPlugin extends JavaPlugin {
+
+ @Override
+ public void onEnable() {
+ Converter.convert((JavaPlugin) Bukkit.getPluginManager().getPlugin("TargetPlugin"));
+ // Other code goes here...
+ }
+
+}
+
+class YourPlugin : JavaPlugin() {
+
+ override fun onEnable() {
+ Converter.convert(Bukkit.getPluginManager().getPlugin("TargetPlugin") as JavaPlugin)
+ // Other code goes here...
+ }
+
+}
+
+When this is run, the commands /gmc
, /gm1
, /gms
and /i
will all be registered by the CommandAPI.
In addition to converting all commands from a target plugin, the CommandAPI allows you to convert single commands at a time using the following methods from the Converter
class:
public static convert(Plugin plugin, String cmdName);
+public static convert(Plugin plugin, String cmdName, List<Argument> arguments);
+public static convert(Plugin plugin, String cmdName, Argument... arguments);
+
+In these commands, the plugin
refers to the plugin which has the command you want to convert and cmdName
is the name of the command declared in the target plugin's plugin.yml
file (just the main command, not the aliases!).
The List<Argument>
or Argument...
can be used to provide argument checks that lets you apply the command UI to a bukkit command.
Say we want to convert EssentialsX's /speed
command using the CommandAPI. The plugin.yml
entry for the /speed
command is the following:
speed:
+ description: Change your speed limits.
+ usage: /<command> [type] <speed> [player]
+ aliases: [flyspeed,eflyspeed,fspeed,efspeed,espeed,walkspeed,ewalkspeed,wspeed,ewspeed]
+
+From this, we can determine that there are the following commands, where "walk" and "fly" are the different types that the command can take:
+/speed <speed>
+/speed <speed> <target>
+/speed <walk/fly> <speed>
+/speed <walk/fly> <speed> <target>
+
+With the EssentialsX plugin, the <speed>
value can only take numbers between 0 and 10. As such, we'll ensure to apply these limits using the IntegerArgument
. In addition, since the speed type can only be "walk" or "fly", we'll add that to our converter as well using a MultiLiteralArgument
:
JavaPlugin essentials = (JavaPlugin) Bukkit.getPluginManager().getPlugin("Essentials");
+
+// /speed <speed>
+Converter.convert(essentials, "speed", new IntegerArgument("speed", 0, 10));
+
+// /speed <target>
+Converter.convert(essentials, "speed", new PlayerArgument("target"));
+
+// /speed <walk/fly> <speed>
+Converter.convert(essentials, "speed",
+ new MultiLiteralArgument("modes", "walk", "fly"),
+ new IntegerArgument("speed", 0, 10)
+);
+
+// /speed <walk/fly> <speed> <target>
+Converter.convert(essentials, "speed",
+ new MultiLiteralArgument("modes", "walk", "fly"),
+ new IntegerArgument("speed", 0, 10),
+ new PlayerArgument("target")
+);
+
+val essentials = Bukkit.getPluginManager().getPlugin("Essentials") as JavaPlugin
+
+// /speed <speed>
+Converter.convert(essentials, "speed", IntegerArgument("speed", 0, 10))
+
+// /speed <target>
+Converter.convert(essentials, "speed", PlayerArgument("target"))
+
+// /speed <walk/fly> <speed>
+Converter.convert(essentials, "speed",
+ MultiLiteralArgument("modes", "walk", "fly"),
+ IntegerArgument("speed", 0, 10)
+)
+
+// /speed <walk/fly> <speed> <target>
+Converter.convert(essentials, "speed",
+ MultiLiteralArgument("modes", "walk", "fly"),
+ IntegerArgument("speed", 0, 10),
+ PlayerArgument("target")
+)
+
+Entity selectors (also known as target selectors) allows you to select certain entities or players which fit a certain criteria when writing a command. Typically, these are of the form @p
, @r
, @a
, @e
or @s
. By default, when converting a command without arguments, the CommandAPI will not handle these entity selectors. In order to get entity selectors to cooperate with plugins, they must be declared in the relevant config.yml
section.
EssentialsX includes a command /ext
which lets you extinguish a player that is currently on fire. The command format is the following:
/ext
+/ext <player>
+
+In order to convert this command, we could use the following config.yml
file:
verbose-outputs: false
+create-dispatcher-json: false
+plugins-to-convert:
+ - Essentials:
+ - ext
+
+Using the above config.yml
file will support the following commands:
/ext
+/ext Notch
+
+However, the above config.yml
will not support the following commands:
/ext @a[distance=10]
+/ext @p
+
+In order to handle this, we have to use the conversion with arguments (as described in the previous section). For this ext
command, we want to only use this command on one or more players, therefore we want to use the api:players
argument which is compatible with one or more players:
verbose-outputs: false
+create-dispatcher-json: false
+plugins-to-convert:
+ - Essentials:
+ - ext <player>[api:players]
+ - ext
+
+Note that we declare ext <player>[api:players]
before we declare ext
. This is because more precise commands MUST be declared before lesser precise commands.
The CommandAPI has the ability to convert plugin commands into "Vanilla compatible" commands automatically on startup. This allows you to use /execute
and Minecraft functions/tags for plugins that do not use the CommandAPI. For example, if you want to use the /hat
command from the plugin Essentials
in an /execute
command or from a command block, you can use the CommandAPI's command conversion setting to do so.
The CommandAPI has 3 different conversion methods, each one more specific and powerful than the others. These are the following:
+Converts all commands from a plugin into Vanilla compatible commands
+Converts a single command into a Vanilla compatible command
+Single command conversion with custom arguments
+Converts a single command from a plugin into a Vanilla compatible command, whilst also declaring what the arguments to the command are
+Drag a plugin here to view a list of available commands which can be registered for the CommandAPI.
+ +To configure command conversion, the CommandAPI reads this information from the config.yml
file. This file has a bit of a weird structure, so to put it simply, these are the following rules:
config.yml
cannot have tab characters - The config.yml
file must only consist of spaces!If you're uncertain if your configuration is valid (or you're getting weird errors in the console), you can check if your configuration is valid by dropping your config.yml
file below:
To convert all of the commands that a plugin has, add the name of the plugin, followed by a ~
character to the list of plugins to convert in the config.yml
file.
For example, if you wanted to convert all commands that EssentialsX has, you can use the following config.yml
:
verbose-outputs: true
+create-dispatcher-json: false
+plugins-to-convert:
+ - Essentials: ~
+
+Often, you don't want to convert every single command that a plugin declares, and instead you only want to convert a few commands that a plugin has.
+The CommandAPI has two ways of doing this:
+plugin.yml
fileplugin.yml
fileTo convert a single command, you need to first populate the config.yml
with the name of the plugin and commands to be converted. To illustrate this, we'll use an example:
Say we're using EssentialsX on our server and we want to be able to use /afk
and /hat
in command blocks. This would allow us to use (for example) the following commands in command blocks:
/execute as @p run afk
+/execute as @p run hat
+
+To do this, we need to add Essentials
to our config.yml
file, and include the commands afk
and hat
as the commands to be converted from the Essentials plugin. This would then make our config.yml
file look like this:
verbose-outputs: true
+create-dispatcher-json: false
+plugins-to-convert:
+ - Essentials:
+ - hat
+ - afk
+
+++Developer's Note:
+Note that the commands
+hat
andafk
are used, as opposed to an alias such ashead
. The CommandAPI is only able to convert plugin commands that are declared in a plugin'splugin.yml
file. For example, if we take a look at the EssentialsXplugin.yml
file, we can see the commandsafk
andhat
have been declared and thus, are the commands which must be used in the CommandAPI'sconfig.yml
file:+name: Essentials +main: com.earth2me.essentials.Essentials +version: 2.18.0.0 +website: http://tiny.cc/EssentialsCommands +description: Provides an essential, core set of commands for Bukkit. +softdepend: [Vault, LuckPerms] +authors: [Zenexer, ementalo, Aelux, Brettflan, KimKandor, snowleo, ceulemans, Xeology, KHobbits, md_5, Iaccidentally, drtshock, vemacs, SupaHam, md678685] +api-version: "1.13" +commands: + afk: + description: Marks you as away-from-keyboard. + usage: /<command> [player/message...] + aliases: [eafk,away,eaway] + +# (other config options omitted) + + hat: + description: Get some cool new headgear. + usage: /<command> [remove] + aliases: [ehat,head,ehead] + +# (other config options omitted) +
Some commands which are registered by plugins are not present in the plugin's plugin.yml
file. Due to this, the CommandAPI cannot link a command to its respective plugin for command conversion. In order to deal with this, the CommandAPI can link arbitrary commands via the other-commands-to-convert
option in the CommandAPI's config.yml
file.
This can be used for any of the following (and more):
+plugin.yml
, such as WorldEdit commandsTo add commands to be converted, simply add them under the other-command-to-convert
section in the CommandAPI's config.yml
.
In this example, we want to convert the //set
command from WorldEdit, as well as a custom command /mycommand
. Note that since WorldEdit's //set
command has two forward slashes, we ignore the first one (because that is the symbol used to start commands), but must make sure that we include the second one as that is part of WorldEdit's command syntax. This is the following config.yml
that you would need:
verbose-outputs: false
+create-dispatcher-json: false
+plugins-to-convert: []
+skip-sender-proxy: []
+other-commands-to-convert:
+ - /set
+ - mycommand
+
+For even finer control when converting a single command, you can provide the list of arguments that are required to run the command! This lets you use the command UI in converted commands as you see fit. Before we explain how to do this in detail, let's first take a look at an example of this in action.
+EssentialsX includes a command /speed
which lets you change the current speed that a player can move at. The command format is the following:
/speed <speed>
+/speed <speed> <target>
+/speed <walk/fly> <speed>
+/speed <walk/fly> <speed> <target>
+
+Which means you can run any of the following commands:
+/speed 5
+/speed 2 Notch
+/speed fly 6
+/speed walk 3 Notch
+
+By looking at this, we can see that:
+<speed>
is a number. By using the command, we can determine that the range of values is between 0 and 10 (inclusive).<target>
is a player<walk/fly>
don't change - these are "fixed" values.We can represent this using the following config.yml
file:
verbose-outputs: false
+create-dispatcher-json: false
+plugins-to-convert:
+ - Essentials:
+ - speed <speed>[0..10]
+ - speed <target>[minecraft:game_profile]
+ - speed (walk|fly) <speed>[0..10]
+ - speed (walk|fly) <speed>[0..10] <target>[minecraft:game_profile]
+
+Using this, we can display options, such as "fly" and "walk", as well as optional targets ("Skepter"):
+ +Additionally, we can apply limits to the numbers that can be provided. For example, here we limit the number to a value between 0 to 10. If a value is outside of that range, and error is shown to the user:
+ +The argument syntax is a little tricky to get the hang of at the beginning, but it should be fairly straight forward. There are two main types of arguments that you can have:
+Literal arguments are arguments with "fixed" values, such as walk
or fly
from our example above. To declare a literal value, place brackets around the value. For example:
(walk)
+
+To have multiple different literals, place a pipe symbol |
between each entry within the brackets. For example:
(walk|fly)
+
+Named arguments must have a name, declared in angled brackets <name>
, followed by the type of the argument in square brackets [type]
. In the example above, we had a named argument <target>
, with the argument type as a player: [minecraft:game_profile]
.
The name in the argument can be whatever you want, but it is recommended to keep it as a lowercase value consisting only of letters.
+The following argument types are highly recommended and are very likely to be compatible with every plugin command that you may want to convert:
+Type | Description |
---|---|
api:entity | An single entity (e.g. @e[limit=1] ) |
api:entities | Many entities (e.g. @e ) |
api:player | A single player (e.g. Notch or @r ) |
api:players | Many players (e.g. @a ) |
api:greedy_string | An unlimited amount of text. This can only be used as the last entry of a list of arguments |
brigadier:bool | A Boolean value true or false |
brigadier:double | A decimal number |
brigadier:float | A decimal number |
brigadier:integer | A whole number |
brigadier:long | A whole number |
brigadier:string | A single word |
minecraft:block_pos | A location of x, y and z coordinates (whole numbers) |
In the example above, we used the a "range type" in the form [0..10]
. This is a special argument type that will conform to brigader:long
or brigader:double
and apply a limit to the values that can be entered.
To declare the range \(10 \le x \le 50\) (a value must be between 10 and 50 (inclusive)):
+<name>[10..50]
+
+To declare the range \(10 \le x\) (a value must be bigger than or equal to 10):
+<name>[10..]
+
+To declare the range \(x \le 50\) (a value must be less than or equal to 50):
+<name>[..50]
+
+To declare the range \(0 \le x \le 1\), where \(x\) is a decimal value:
+<name>[0.0..1.0]
+
+To declare a value \(x\) that can take any range of values and is a decimal number:
+<name>[brigadier:double]
+
+The list of types are based on the list of argument types from the Minecraft Wiki, with a few changes. The complete list that the CommandAPI supports is as follows:
+Type | Description |
---|---|
api:advancement | An advancement |
api:biome | A biome |
api:entity | An single entity (e.g. @e[limit=1] ) |
api:entities | Many entities (e.g. @e ) |
api:greedy_string | An unlimited amount of text. This can only be used as the last entry of a list of arguments |
api:loot_table | A loot table |
api:player | A single player (e.g. Notch or @r ) |
api:players | Many players (e.g. @a ) |
api:recipe | A recipe |
api:sound | A sound effect |
api:text | Text encased in quotes: "text with spaces" |
brigadier:bool | A Boolean value true or false |
brigadier:double | A decimal number |
brigadier:float | A decimal number |
brigadier:integer | A whole number |
brigadier:long | A whole number |
brigadier:string | A single word |
minecraft:angle | A yaw angle in degrees (from -180.0 to 179.9) |
minecraft:block_pos | A location of x, y and z coordinates (whole numbers) |
minecraft:block_predicate | A block predicate |
minecraft:block_state | A block type (e.g. stone ) |
minecraft:color | A chat color (e.g. red , green ) |
minecraft:column_pos | A location of x and z coordinates (whole numbers) |
minecraft:component | Raw JSON text |
minecraft:dimension | A dimension/world, (e.g. minecraft:overworld ) |
minecraft:entity | An entity (e.g. Notch ) |
minecraft:entity_summon | An entity type (e.g. cow , wither ) |
minecraft:float_range | A range of decimal numbers |
minecraft:function | A datapack function |
minecraft:game_profile | A player (e.g. Notch ) |
minecraft:int_range | A range of whole numbers |
minecraft:item_enchantment | An enchantment (e.g. unbreaking ) |
minecraft:item_predicate | An item predicate |
minecraft:item_stack | An item (e.g. stick ) |
minecraft:message | A plain text message which can have target selectors (e.g. Hello @p ). This can only be used as the last entry of a list of arguments |
minecraft:mob_effect | A potion effect (e.g. speed , jump_boost ) |
minecraft:nbt_compound_tag | Raw compound NBT in SNBT format |
minecraft:objective | An objective name (e.g. temperature ) |
minecraft:objective_criteria | An objective criteria (e.g. deaths ) |
minecraft:operation | An operation symbol (e.g. += , *= ) |
minecraft:particle | A particle (e.g. crit , flame ) |
minecraft:rotation | A rotation of yaw and pitch values (e.g. ~ ~ ) |
minecraft:score_holder | A score holder (e.g. Notch ) |
minecraft:scoreboard_slot | A scoreboard slot (e.g. sidebar ) |
minecraft:swizzle | A collection of axes (e.g. xyz , xz ) |
minecraft:team | A team name (e.g. hunters ) |
minecraft:time | A duration of time (e.g. 2d ) |
minecraft:uuid | A UUID (e.g. dd12be42-52a9-4a91-a8a1-11c01849e498 ) |
minecraft:vec2 | A location of x and z coordinates (decimal numbers) |
minecraft:vec3 | A location of x, y and z coordinates (decimal numbers) |
The CommandAPI offers an additional way to access arguments when using Kotlin: delegated properties. With delegated properties, there are two possible dependencies you can use:
+commandapi-core-kotlin
Support for delegated properties has been added to the commandapi-core-kotlin
module. If you want to use delegated properties, you need to add this dependency.
commandapi-bukkit-kotlin
If you are already using the Kotlin DSL to create your commands, you can already use delegated properties. commandapi-core-kotlin
is included in commandapi-bukkit-kotlin
.
To be able to access arguments by using delegated properties, your variable name needs to match the node name of the argument. This could look like this:
+CommandAPICommand("mycommand")
+ .withArguments(StringArgument("string"))
+ .withArguments(PlayerArgument("target"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val string: String by args
+ val target: Player by args
+ // Implementation...
+ })
+ .register()
+
+commandAPICommand("mycommand") {
+ stringArgument("string")
+ playerArgument("target")
+ playerExecutor { player, args ->
+ val string: String by args
+ val target: Player by args
+ // Implementation...
+ }
+}
+
+Here's a list of questions that have come up time and time again which all have the same answer.
+The CommandAPI's documentation is the place to search for anything! In the top left corner of this documentation, you can find this icon. You can pretty much search for anything - it'll find it!
+/reload
?Formally, no. If you are encountering issues with /reload
, consider not using /reload
. More information about reloading can be found in Plugin reloading.
As of 9.0.0, yes! Please view information on optional arguments in Optional arguments.
+Not yet. The CommandAPI's annotation system was actually originally a little test on writing a compile-time annotation system which actually worked out much better than I had intended. I plan to rewrite the CommandAPI's annotation system to make it much more powerful (and support suggestions!). This is stated in the project roadmap
+No. This is a Brigadier limitation.
+++Technical reason that this is a limitation of Brigadier
+Brigadier's code has two classes for arguments,
+LiteralCommandNode
andArgumentCommandNode
. TheArgumentCommandNode
class contains a fieldcustomSuggestions
of typeSuggestionProvider<S>
which is used to handle suggestions - this field is not present insideLiteralCommandNode
, meaning that LiteralArguments cannot provide suggestions to users.We need suggestions to provide tooltips. This is because
+SuggestionProvider
provides us with an instance ofSuggestionsBuilder
,SuggestionsBuilder
contains aList<Suggestion>
and theSuggestion
class containsMessage
. ThisMessage
class is what is needed to display a tooltip to users.
No. That message is handled client-side and isn't controlled by the CommandAPI. An alternative solution is to perform permission checking inside the command and return a custom message if it's not satisfied:
+new CommandAPICommand("mycommand")
+ .withPermission("some.permission")
+ .executes((sender, args) -> {
+ sender.sendMessage("Hello!");
+ })
+ .register();
+
+$$\downarrow$$
+new CommandAPICommand("mycommand")
+ .executes((sender, args) -> {
+ if(!sender.hasPermission("some.permission")) {
+ throw CommandAPI.failWithString("You don't have permission to run /mycommand!");
+ }
+ sender.sendMessage("Hello!");
+ })
+ .register();
+
+No. That message is handled client-side and isn't controlled by the CommandAPI.
+Arguments with suggestions provided using ArgumentSuggestions.strings(String...)
are calculated when the command is registered. In order to have argument suggestions calculated when the command is being typed, you need to use the lambda-variant of the ArgumentSuggestions.strings(Function<SuggestionInfo, String[]> suggestions)
method instead. More information about the different methods can be found here.
The easiest way to do this is to add info ->
at the start of your array:
ArgumentSuggestions.strings(SomeClass.someArray);
+
+$$\downarrow$$
+ArgumentSuggestions.strings(info -> SomeClass.someArray);
+
+
+ The CommandAPI has support to use Minecraft's functions within your plugins. This is handled by using a class provided by the CommandAPI called FunctionWrapper
, which allows you to execute functions. The CommandAPI also provides support to let you run your own commands within Minecraft function files.
++Developer's Note:
+Minecraft 1.16+ change the way that datapacks are loaded on the server, so that they load before plugins are enabled. This means that non-vanilla commands that are declared in functions and tags will be detected as invalid, causing the server to throw a lot of errors at the very start.
+The CommandAPI reloads datapacks once the server has finished loading using all declared commands, therefore the error messages at the start of the server can be ignored.
+
In order to use a command from your plugin in a .mcfunction
file, you must register your command in your plugin's onLoad()
method, instead of the onEnable()
method. Failure to do so will not allow the command to be registered for Minecraft functions, causing the function file to fail to load during the server startup phase.
++Developer's Note:
+In short, if you want to register a command which can be used in Minecraft functions, register it in your plugin's
+onLoad()
method.
Say we have a command /killall
that simply kills all entities in all worlds on the server. If we were to register this in our onLoad()
method, this would allow us to use the /killall
command in Minecraft functions and tags.
public class Main extends JavaPlugin {
+
+ @Override
+ public void onLoad() {
+ // Commands which will be used in Minecraft functions are registered here
+
+ new CommandAPICommand("killall")
+ .executes((sender, args) -> {
+ // Kills all enemies in all worlds
+ Bukkit.getWorlds().forEach(w -> w.getLivingEntities().forEach(e -> e.setHealth(0)));
+ })
+ .register();
+ }
+
+ @Override
+ public void onEnable() {
+ // Register all other commands here
+ }
+}
+
+class Main : JavaPlugin() {
+
+ override fun onLoad() {
+ // Commands which will be used in Minecraft functions are registered here
+
+ CommandAPICommand("killall")
+ .executes(CommandExecutor { _, _ ->
+ // Kills all enemies in all worlds
+ Bukkit.getWorlds().forEach { world -> world.livingEntities.forEach { entity -> entity.health = 0.0 } }
+ })
+ .register()
+ }
+
+ override fun onEnable() {
+ // Register all other commands here
+ }
+
+}
+
+++Developer's Note:
+Most developers can completely skip this section. This is for those that are unfamiliar with functions and tags and is less about how the CommandAPI works.
+
This section explains how functions are declared and set up for use in a Minecraft server. This is ideal for server owners who've never set up functions, or developers (like the Command API's creator) that has no idea how to set this sort of thing up.
+Functions are text files (with the .mcfunction
extension) which contains lists of functions which are executed one after another. Each line of the file is a valid Minecraft command. Say we have text.mcfunction
:
killall
+say Killed all living entities on the server
+
+This will run the custom command killall (as declared in Example - Registering command for use in a function), and then broadcast a message to all players stating that all entities were killed.
+Tags are json files which contain a list of functions. Tags let you run multiple functions at a time. Say we have a tag called mytag.json
:
{
+ "values": [
+ "mycustomnamespace:test",
+ "mycustomnamespace:test2"
+ ]
+}
+
+This will run the function test
and the function test2
, which are in the namespace mycustomnamespace
.
The following hierarchy explains where functions and tags go. In this diagram, the two functions test
and test2
are in a directory called functions
. There is also a tag called mytag
which is placed in the tags
directory under functions
. These are all under the namespace called mycustomnamespace
server/
+├── world/
+│ ├── advancements/
+│ ├── data/
+│ ├── datapacks/
+│ │ └── bukkit/
+│ │ ├── pack.mcmeta
+│ │ └── data/
+│ │ └── mycustomnamespace/
+│ │ ├── functions/
+│ │ │ ├── test.mcfunction
+│ │ │ └── test2.mcfunction
+│ │ └── tags/
+│ │ └── functions/
+│ │ └── mytag.json
+│ └── ...
+├── world_nether/
+├── world_the_end/
+├── ...
+└── spigot.jar
+
+To execute the test
function, you would run the following command:
/function mycustomnamespace:test
+
+To execute the mytag
tag, you would run the following command:
/function #mycustomnamespace:mytag
+
+
+ The CommandAPI includes the FunctionWrapper
class which is a wrapper for Minecraft's functions. It allows you to execute the commands that are represented by the respective .mcfunction
file.
The FunctionWrapper
class is an extension of the SimpleFunctionWrapper
class. It is a SimpleFunctionWrapper
which has been constructed from an existing command sender when a command is used. This means that the command sender has already been "baked into" the FunctionWrapper
object, allowing you to run it without having to provide a command sender.
The FunctionWrapper
class has the following methods:
class FunctionWrapper extends SimpleFunctionWrapper {
+
+ // Methods specific to this class
+ int run();
+ int runAs(Entity e);
+
+ // Methods inherited from SimpleFunctionWrapper
+ static SimpleFunctionWrapper getFunction(NamespacedKey key);
+ static SimpleFunctionWrapper[] getTag(NamespacedKey key);
+ static Set<NamespacedKey> getFunctions();
+ static Set<NamespacedKey> getTags();
+ int run(CommandSender sender);
+ String[] getCommands();
+ NamespacedKey getKey();
+}
+
+These methods allow you to interact with the Minecraft function that this class wraps.
+run()
As of CommandAPI 9.3.0 (compatible with Minecraft versions 1.20.3 and 1.20.4), calling run()
will always return a value of 1
, regardless of whether the command succeeds, fails, or returns a result.
The run()
method runs the function. The command executor that runs this function is the command executor that was used to retrieve it. For example, if a player in-game populated this argument, then the player will be filled in for @p
and the player's location would be used for things such as ~ ~ ~
:
new CommandAPICommand("runfunc")
+ .withArguments(new FunctionArgument("function"))
+ .executes((sender, args) -> {
+ FunctionWrapper[] functions = (FunctionWrapper[]) args.get("function");
+ for (FunctionWrapper function : functions) {
+ function.run(); // The command executor in this case is 'sender'
+ }
+ })
+ .register();
+
+CommandAPICommand("runfunc")
+ .withArguments(FunctionArgument("function"))
+ .executes(CommandExecutor { _, args ->
+ val functions = args["function"] as Array<FunctionWrapper>
+ for (function in functions) {
+ function.run() // The command executor in this case is 'sender'
+ }
+ })
+ .register()
+
+runAs(Entity)
As of CommandAPI 9.3.0 (compatible with Minecraft versions 1.20.3 and 1.20.4), calling runAs(Entity)
will always return a value of 1
, regardless of whether the command succeeds, fails, or returns a result.
The runAs(Entity)
is the same as the run()
method, but it allows you to change the command executor to another entity.
Help topics can be added to your command using the withHelp()
, withShortDescription()
, withFullDescription()
or withUsage()
methods when registering a command. Help allows users to understand what your command does and provides them with a list of usage forms to aid in writing a command.
A help topic consists of two mains parts:
+This can be seen with the following example, for a command /mycmd
. This example has the short description "Says hi", and a full description "Broadcasts hi to everyone on the server". The short help is shown in the help list, which (in this example) is viewed using /help 5
. The full description is shown for the help for the command on its own, which is viewed using /help mycmd
:
The CommandAPI has three methods to register parts of a help. The withShortDescription()
sets the short description for the command, the withFullDescription()
sets the full description for the command and withHelp()
is a simple way to set both the short and full description at the same time. The withHelp()
method is the recommended method to use to set the help for a command.
If no short description is provided, the CommandAPI will attempt to use the full description if one is present. Note that this may be truncated automatically, so it is recommended to provide your own short description.
+These are the following methods that the CommandAPI provides to set the help topic for a command:
+CommandAPICommand withShortDescription(String description);
+
+The withShortDescription
method simply sets the short description for the command. In the above screenshot, the short description is "Says hi".
CommandAPICommand withFullDescription(String description);
+
+The withFullDescription
method sets the full description for the command. In the above screenshot, the full description is "Broadcasts hi to everyone on the server".
CommandAPICommand withHelp(String shortDescription, String fullDescription);
+
+The withHelp
method sets both the short description and the full description at the same time.
In this simple example, we implement the above screenshot's help topic. We register a command /mycmd
and use the withShortDescription
and withFullDescription
methods to create a help topic:
new CommandAPICommand("mycmd")
+ .withShortDescription("Says hi")
+ .withFullDescription("Broadcasts hi to everyone on the server")
+ .executes((sender, args) -> {
+ Bukkit.broadcastMessage("Hi!");
+ })
+ .register();
+
+CommandAPICommand("mycmd")
+ .withShortDescription("Says hi")
+ .withFullDescription("Broadcasts hi to everyone on the server")
+ .executes(CommandExecutor { _, _ ->
+ Bukkit.broadcastMessage("Hi!")
+ })
+ .register()
+
+commandAPICommand("mycmd") {
+ withShortDescription("Says hi")
+ withFullDescription("Broadcasts hi to everyone on the server")
+ anyExecutor { _, _ ->
+ Bukkit.broadcastMessage("Hi!")
+ }
+}
+
+We could also register this command using the withHelp
method instead:
new CommandAPICommand("mycmd")
+ .withHelp("Says hi", "Broadcasts hi to everyone on the server")
+ .executes((sender, args) -> {
+ Bukkit.broadcastMessage("Hi!");
+ })
+ .register();
+
+CommandAPICommand("mycmd")
+ .withHelp("Says hi", "Broadcasts hi to everyone on the server")
+ .executes(CommandExecutor { _, _ ->
+ Bukkit.broadcastMessage("Hi!")
+ })
+ .register()
+
+commandAPICommand("mycmd") {
+ withHelp("Says hi", "Broadcasts hi to everyone on the server")
+ anyExecutor { _, _ ->
+ Bukkit.broadcastMessage("Hi!")
+ }
+}
+
+
+For more control over help topics, the CommandAPI offers the following method, which allows you to provide your own HelpTopic
object:
CommandAPICommand withHelp(HelpTopic helpTopic);
+
+In this example, we implement locale-specific help so players can see help in their desired language. To do this, we must make use of the Bukkit HelpTopic
object which gives us more control over the content of help that is displayed to a player:
public HelpTopic makeHelp(String command) {
+ return new HelpTopic() {
+
+ @Override
+ public String getShortText() {
+ return "Says hi";
+ }
+
+ @Override
+ public String getFullText(CommandSender forWho) {
+ String helpText = "";
+ if (forWho instanceof Player player) {
+ // Make use of the player's locale to make language-specific help!
+ Locale playerLocale = player.locale();
+ if (playerLocale.getLanguage().equals("en")) {
+ helpText = "Broadcasts \"Hi!\" to everyone on the server";
+ } else if (playerLocale.getLanguage().equals("de")) {
+ helpText = "Sendet \"Hi!\" an alle auf dem Server";
+ }
+ } else {
+ helpText = "Broadcasts \"Hi!\" to everyone on the server";
+ }
+ return helpText;
+ }
+
+ // Allow anyone to see this help topic
+ @Override
+ public boolean canSee(CommandSender player) {
+ return true;
+ }
+ };
+}
+
+fun makeHelp(command: String): HelpTopic = object: HelpTopic() {
+ override fun getShortText(): String = "Says hi"
+
+ override fun getFullText(forWho: CommandSender): String {
+ var helpText = ""
+ if (forWho is Player) {
+ // Make use of the player's locale to make language-specific help!
+ val playerLocale = forWho.locale()
+ if (playerLocale.getLanguage() == "en") {
+ helpText = "Broadcasts \"Hi!\" to everyone on the server"
+ } else if (playerLocale.getLanguage() == "de") {
+ helpText = "Sendet \"Hi!\" an alle auf dem Server"
+ }
+ } else {
+ helpText = "Broadcasts \"Hi!\" to everyone on the server"
+ }
+ return helpText
+ }
+
+ // Allow anyone to see this help topic
+ override fun canSee(player: CommandSender): Boolean = true
+}
+
+We then add our new HelpTopic
to the command using the withHelp
method:
new CommandAPICommand("mycmd")
+ .withHelp(makeHelp("mycmd"))
+ .executes((sender, args) -> {
+ Bukkit.broadcastMessage("Hi!");
+ })
+ .register();
+
+return CommandAPICommand("mycmd")
+ .withHelp(makeHelp("mycmd"))
+ .executes(CommandExecutor { _, _ ->
+ Bukkit.broadcastMessage("Hi!")
+ })
+ .register()
+
+When registering a command, there also is a command usage generated. The CommandAPI provides a way to customise this usage by providing the withUsage()
method:
CommandAPICommand withUsage(String... usage)
+
+In this example, we want to showcase how usage generation displays the usage vs. how a custom usage displays the usage:
+/command <help> <admin|user|moderator|vip>
+/command <reload> <commandsystem|config|server>
+
+This is how it would get displayed:
+Usage:
+- /command <help> <admin>
+- /command <help> <user>
+- /command <help> <moderator>
+- /command <help> <vip>
+- /command <reload> <commandsystem>
+- /command <reload> <config>
+- /command <reload> <server>
+
+Now, we are implementing the withUsage()
method:
new CommandAPICommand("...")
+ .withUsage(
+ "/command <help> <section>",
+ "/command <reload> <system>"
+ )
+
+By using withUsage()
like that, the CommandAPI will produce this usage:
Usage:
+- /command <help> <section>
+- /command <reload> <system>
+
+To configure command conversion, the CommandAPI reads this information from the config.yml
file. This file has a bit of a weird structure, so to put it simply, these are the following rules:
config.yml
cannot have tab characters - The config.yml
file must only consist of spaces!If you're uncertain if your configuration is valid (or you're getting weird errors in the console), you can check if your configuration is valid by dropping your config.yml
file below:
To convert all of the commands that a plugin has, add the name of the plugin, followed by a ~
character to the list of plugins to convert in the config.yml
file.
For example, if you wanted to convert all commands that EssentialsX has, you can use the following config.yml
:
verbose-outputs: true
+create-dispatcher-json: false
+plugins-to-convert:
+ - Essentials: ~
+
+Often, you don't want to convert every single command that a plugin declares, and instead you only want to convert a few commands that a plugin has.
+The CommandAPI has two ways of doing this:
+plugin.yml
fileplugin.yml
fileTo convert a single command, you need to first populate the config.yml
with the name of the plugin and commands to be converted. To illustrate this, we'll use an example:
Say we're using EssentialsX on our server and we want to be able to use /afk
and /hat
in command blocks. This would allow us to use (for example) the following commands in command blocks:
/execute as @p run afk
+/execute as @p run hat
+
+To do this, we need to add Essentials
to our config.yml
file, and include the commands afk
and hat
as the commands to be converted from the Essentials plugin. This would then make our config.yml
file look like this:
verbose-outputs: true
+create-dispatcher-json: false
+plugins-to-convert:
+ - Essentials:
+ - hat
+ - afk
+
+++Developer's Note:
+Note that the commands
+hat
andafk
are used, as opposed to an alias such ashead
. The CommandAPI is only able to convert plugin commands that are declared in a plugin'splugin.yml
file. For example, if we take a look at the EssentialsXplugin.yml
file, we can see the commandsafk
andhat
have been declared and thus, are the commands which must be used in the CommandAPI'sconfig.yml
file:+name: Essentials +main: com.earth2me.essentials.Essentials +version: 2.18.0.0 +website: http://tiny.cc/EssentialsCommands +description: Provides an essential, core set of commands for Bukkit. +softdepend: [Vault, LuckPerms] +authors: [Zenexer, ementalo, Aelux, Brettflan, KimKandor, snowleo, ceulemans, Xeology, KHobbits, md_5, Iaccidentally, drtshock, vemacs, SupaHam, md678685] +api-version: "1.13" +commands: + afk: + description: Marks you as away-from-keyboard. + usage: /<command> [player/message...] + aliases: [eafk,away,eaway] + +# (other config options omitted) + + hat: + description: Get some cool new headgear. + usage: /<command> [remove] + aliases: [ehat,head,ehead] + +# (other config options omitted) +
Some commands which are registered by plugins are not present in the plugin's plugin.yml
file. Due to this, the CommandAPI cannot link a command to its respective plugin for command conversion. In order to deal with this, the CommandAPI can link arbitrary commands via the other-commands-to-convert
option in the CommandAPI's config.yml
file.
This can be used for any of the following (and more):
+plugin.yml
, such as WorldEdit commandsTo add commands to be converted, simply add them under the other-command-to-convert
section in the CommandAPI's config.yml
.
In this example, we want to convert the //set
command from WorldEdit, as well as a custom command /mycommand
. Note that since WorldEdit's //set
command has two forward slashes, we ignore the first one (because that is the symbol used to start commands), but must make sure that we include the second one as that is part of WorldEdit's command syntax. This is the following config.yml
that you would need:
verbose-outputs: false
+create-dispatcher-json: false
+plugins-to-convert: []
+skip-sender-proxy: []
+other-commands-to-convert:
+ - /set
+ - mycommand
+
+For even finer control when converting a single command, you can provide the list of arguments that are required to run the command! This lets you use the command UI in converted commands as you see fit. Before we explain how to do this in detail, let's first take a look at an example of this in action.
+EssentialsX includes a command /speed
which lets you change the current speed that a player can move at. The command format is the following:
/speed <speed>
+/speed <speed> <target>
+/speed <walk/fly> <speed>
+/speed <walk/fly> <speed> <target>
+
+Which means you can run any of the following commands:
+/speed 5
+/speed 2 Notch
+/speed fly 6
+/speed walk 3 Notch
+
+By looking at this, we can see that:
+<speed>
is a number. By using the command, we can determine that the range of values is between 0 and 10 (inclusive).<target>
is a player<walk/fly>
don't change - these are "fixed" values.We can represent this using the following config.yml
file:
verbose-outputs: false
+create-dispatcher-json: false
+plugins-to-convert:
+ - Essentials:
+ - speed <speed>[0..10]
+ - speed <target>[minecraft:game_profile]
+ - speed (walk|fly) <speed>[0..10]
+ - speed (walk|fly) <speed>[0..10] <target>[minecraft:game_profile]
+
+Using this, we can display options, such as "fly" and "walk", as well as optional targets ("Skepter"):
+ +Additionally, we can apply limits to the numbers that can be provided. For example, here we limit the number to a value between 0 to 10. If a value is outside of that range, and error is shown to the user:
+ +The argument syntax is a little tricky to get the hang of at the beginning, but it should be fairly straight forward. There are two main types of arguments that you can have:
+Literal arguments are arguments with "fixed" values, such as walk
or fly
from our example above. To declare a literal value, place brackets around the value. For example:
(walk)
+
+To have multiple different literals, place a pipe symbol |
between each entry within the brackets. For example:
(walk|fly)
+
+Named arguments must have a name, declared in angled brackets <name>
, followed by the type of the argument in square brackets [type]
. In the example above, we had a named argument <target>
, with the argument type as a player: [minecraft:game_profile]
.
The name in the argument can be whatever you want, but it is recommended to keep it as a lowercase value consisting only of letters.
+The following argument types are highly recommended and are very likely to be compatible with every plugin command that you may want to convert:
+Type | Description |
---|---|
api:entity | An single entity (e.g. @e[limit=1] ) |
api:entities | Many entities (e.g. @e ) |
api:player | A single player (e.g. Notch or @r ) |
api:players | Many players (e.g. @a ) |
api:greedy_string | An unlimited amount of text. This can only be used as the last entry of a list of arguments |
brigadier:bool | A Boolean value true or false |
brigadier:double | A decimal number |
brigadier:float | A decimal number |
brigadier:integer | A whole number |
brigadier:long | A whole number |
brigadier:string | A single word |
minecraft:block_pos | A location of x, y and z coordinates (whole numbers) |
In the example above, we used the a "range type" in the form [0..10]
. This is a special argument type that will conform to brigader:long
or brigader:double
and apply a limit to the values that can be entered.
To declare the range \(10 \le x \le 50\) (a value must be between 10 and 50 (inclusive)):
+<name>[10..50]
+
+To declare the range \(10 \le x\) (a value must be bigger than or equal to 10):
+<name>[10..]
+
+To declare the range \(x \le 50\) (a value must be less than or equal to 50):
+<name>[..50]
+
+To declare the range \(0 \le x \le 1\), where \(x\) is a decimal value:
+<name>[0.0..1.0]
+
+To declare a value \(x\) that can take any range of values and is a decimal number:
+<name>[brigadier:double]
+
+The list of types are based on the list of argument types from the Minecraft Wiki, with a few changes. The complete list that the CommandAPI supports is as follows:
+Type | Description |
---|---|
api:advancement | An advancement |
api:biome | A biome |
api:entity | An single entity (e.g. @e[limit=1] ) |
api:entities | Many entities (e.g. @e ) |
api:greedy_string | An unlimited amount of text. This can only be used as the last entry of a list of arguments |
api:loot_table | A loot table |
api:player | A single player (e.g. Notch or @r ) |
api:players | Many players (e.g. @a ) |
api:recipe | A recipe |
api:sound | A sound effect |
api:text | Text encased in quotes: "text with spaces" |
brigadier:bool | A Boolean value true or false |
brigadier:double | A decimal number |
brigadier:float | A decimal number |
brigadier:integer | A whole number |
brigadier:long | A whole number |
brigadier:string | A single word |
minecraft:angle | A yaw angle in degrees (from -180.0 to 179.9) |
minecraft:block_pos | A location of x, y and z coordinates (whole numbers) |
minecraft:block_predicate | A block predicate |
minecraft:block_state | A block type (e.g. stone ) |
minecraft:color | A chat color (e.g. red , green ) |
minecraft:column_pos | A location of x and z coordinates (whole numbers) |
minecraft:component | Raw JSON text |
minecraft:dimension | A dimension/world, (e.g. minecraft:overworld ) |
minecraft:entity | An entity (e.g. Notch ) |
minecraft:entity_summon | An entity type (e.g. cow , wither ) |
minecraft:float_range | A range of decimal numbers |
minecraft:function | A datapack function |
minecraft:game_profile | A player (e.g. Notch ) |
minecraft:int_range | A range of whole numbers |
minecraft:item_enchantment | An enchantment (e.g. unbreaking ) |
minecraft:item_predicate | An item predicate |
minecraft:item_stack | An item (e.g. stick ) |
minecraft:message | A plain text message which can have target selectors (e.g. Hello @p ). This can only be used as the last entry of a list of arguments |
minecraft:mob_effect | A potion effect (e.g. speed , jump_boost ) |
minecraft:nbt_compound_tag | Raw compound NBT in SNBT format |
minecraft:objective | An objective name (e.g. temperature ) |
minecraft:objective_criteria | An objective criteria (e.g. deaths ) |
minecraft:operation | An operation symbol (e.g. += , *= ) |
minecraft:particle | A particle (e.g. crit , flame ) |
minecraft:rotation | A rotation of yaw and pitch values (e.g. ~ ~ ) |
minecraft:score_holder | A score holder (e.g. Notch ) |
minecraft:scoreboard_slot | A scoreboard slot (e.g. sidebar ) |
minecraft:swizzle | A collection of axes (e.g. xyz , xz ) |
minecraft:team | A team name (e.g. hunters ) |
minecraft:time | A duration of time (e.g. 2d ) |
minecraft:uuid | A UUID (e.g. dd12be42-52a9-4a91-a8a1-11c01849e498 ) |
minecraft:vec2 | A location of x and z coordinates (decimal numbers) |
minecraft:vec3 | A location of x, y and z coordinates (decimal numbers) |
Entity selectors (also known as target selectors) allows you to select certain entities or players which fit a certain criteria when writing a command. Typically, these are of the form @p
, @r
, @a
, @e
or @s
. By default, when converting a command without arguments, the CommandAPI will not handle these entity selectors. In order to get entity selectors to cooperate with plugins, they must be declared in the relevant config.yml
section.
EssentialsX includes a command /ext
which lets you extinguish a player that is currently on fire. The command format is the following:
/ext
+/ext <player>
+
+In order to convert this command, we could use the following config.yml
file:
verbose-outputs: false
+create-dispatcher-json: false
+plugins-to-convert:
+ - Essentials:
+ - ext
+
+Using the above config.yml
file will support the following commands:
/ext
+/ext Notch
+
+However, the above config.yml
will not support the following commands:
/ext @a[distance=10]
+/ext @p
+
+In order to handle this, we have to use the conversion with arguments (as described in the previous section). For this ext
command, we want to only use this command on one or more players, therefore we want to use the api:players
argument which is compatible with one or more players:
verbose-outputs: false
+create-dispatcher-json: false
+plugins-to-convert:
+ - Essentials:
+ - ext <player>[api:players]
+ - ext
+
+Note that we declare ext <player>[api:players]
before we declare ext
. This is because more precise commands MUST be declared before lesser precise commands.
When the CommandAPI converts a command, it does some tweaks to the thing running the command (such as the player or console) to improve compatibility with plugins (mostly permissions). This doesn't always work, and can sometimes produce an error which looks like this:
+[20:44:01 ERROR]: java.lang.IllegalArgumentException: object is not an instance of declaring class
+[20:44:01 ERROR]: at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
+[20:44:01 ERROR]: at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
+[20:44:01 ERROR]: at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
+[20:44:01 ERROR]: at java.lang.reflect.Method.invoke(Unknown Source)
+[20:44:01 ERROR]: at dev.jorel.commandapi.Converter.lambda$mergeProxySender$3(Converter.java:151)
+[20:44:01 ERROR]: at com.sun.proxy.$Proxy33.getInventory(Unknown Source)
+
+To fix this, add the plugin which the command is registered from to the list of plugins under skip-sender-proxy
.
SkinsRestorer (not associated or sponsored by the CommandAPI in any way) is a plugin that lets you change the skin for a player. This suffers from the above issue and is not compatible with the CommandAPI's conversion compatibility tweaks. To do this, we'll add SkinsRestorer
to the list of plugins which should be skipped:
verbose-outputs: true
+create-dispatcher-json: false
+plugins-to-convert:
+ - SkinsRestorer: ~
+skip-sender-proxy:
+ - SkinsRestorer
+
+To use the CommandAPI in your plugins, there are a few methods of adding it to your development environment. First things first, if you're using the CommandAPI plugin, you need to add the CommandAPI as a dependent in your plugin.yml
or paper-plugin.yml
:
name: MyPlugin
+main: some.package.name.Main
+version: 1.0
+depend: [CommandAPI]
+
+name: MyPlugin
+main: some.package.name.Main
+version: 1.0
+dependencies:
+ server:
+ CommandAPI:
+ load: BEFORE
+ required: true
+ join-classpath: true
+
+++Developer's Note:
+If you've never used maven before, I highly recommend it! It makes it easier to keep your code updated with the latest dependency updates. For information on how to set up a plugin using maven, you can read Bukkit's plugin tutorial.
+
Add the dependency to your pom.xml
:
<dependencies>
+ <dependency>
+ <groupId>dev.jorel</groupId>
+ <artifactId>commandapi-bukkit-core</artifactId>
+ <version>9.5.2</version>
+ <scope>provided</scope>
+ </dependency>
+</dependencies>
+
+Add the repositories to your build.gradle
file (the second repository is required because the CommandAPI depends on the NBT-API):
repositories {
+ mavenCentral()
+ maven { url = "https://repo.codemc.org/repository/maven-public/" }
+}
+
+Add the dependency to your list of dependencies in your build.gradle
file:
dependencies {
+ compileOnly "dev.jorel:commandapi-bukkit-core:9.5.2"
+}
+
+Download the latest CommandAPI.jar from the download page here
+Add the CommandAPI.jar file to your project/environment's build path:
+Adding the external .jar file in Eclipse:
+ +Adding the external .jar file in IntelliJ:
+In your project, first click File
-> Project Structure
-> Libraries
Next, click the little +
at the top:
After you clicked that, you need to select Java
. A little pop-up will appear where you can choose the location of your external .jar file.
"Shading" is the process of including the CommandAPI inside your plugin, rather than requiring the CommandAPI as an external plugin. In other words, if you shade the CommandAPI into your plugin, you don't need to include the CommandAPI.jar
in your server's plugins folder.
The CommandAPI plugin has a few slight differences with the shaded CommandAPI jar file. The CommandAPI plugin has the following extra features that are not present in the shaded version:
+config.yml
fileFor the CommandAPI to function as normal, you must call the CommandAPI's initializers in the onLoad()
and onEnable()
methods of your plugin:
CommandAPI.onLoad(CommandAPIConfig config);
+CommandAPI.onEnable();
+
+If you want to handle reloading, the CommandAPI has minimal support for it with the onDisable()
method, which can go in your plugin. This is optional and is not required if you don't plan on reloading the server.
The onLoad(CommandAPIConfig)
method initializes the CommandAPI's loading sequence. This must be called before you start to access the CommandAPI and must be placed in your plugin's onLoad()
method. The argument CommandAPIConfig
is used to configure how the CommandAPI works. The CommandAPIConfig
class has the following parameters which let you set how the CommandAPI works similar to the config.yml
, which is described here.
public class CommandAPIConfig {
+ CommandAPIConfig verboseOutput(boolean value); // Enables verbose logging
+ CommandAPIConfig silentLogs(boolean value); // Disables ALL logging (except errors)
+ CommandAPIConfig useLatestNMSVersion(boolean value); // Whether the latest NMS implementation should be used or not
+ CommandAPIConfig missingExecutorImplementationMessage(String value); // Set message to display when executor implementation is missing
+ CommandAPIConfig dispatcherFile(File file); // If not null, the CommandAPI will create a JSON file with Brigadier's command tree
+ CommandAPIConfig setNamespace(String namespace); // The namespace to use when the CommandAPI registers a command
+ CommandAPIConfig usePluginNamespace(); // Whether the CommandAPI should use the name of the plugin passed into the CommandAPIConfig implementation as the default namespace for commands
+
+ <T> CommandAPIConfig initializeNBTAPI(Class<T> nbtContainerClass, Function<Object, T> nbtContainerConstructor); // Initializes hooks with an NBT API. See NBT arguments documentation page for more info
+}
+
+The CommandAPIConfig
class follows a typical builder pattern (without you having to run .build()
at the end), which lets you easily construct configuration instances.
However, the CommandAPIConfig
class is abstract and cannot be used to configure the CommandAPI directly. Instead, you must use a subclass of CommandAPIConfig
that corresponds to the platform you are developing for. For example, when developing for Bukkit, you should use the CommandAPIBukkitConfig
class.
public class CommandAPIBukkitConfig extends CommandAPIConfig {
+ CommandAPIBukkitConfig(JavaPlugin plugin);
+
+ CommandAPIBukkitConfig shouldHookPaperReload(boolean hooked); // Whether the CommandAPI should hook into the Paper-exclusive ServerResourcesReloadedEvent
+ CommandAPIBukkitConfig skipReloadDatapacks(boolean skip) // Whether the CommandAPI should reload datapacks on server load
+}
+
+In order to create a CommandAPIBukkitConfig
object, you must give it a reference to your JavaPlugin
instance. The CommandAPI always uses this to registers events, so it is required when loading the CommandAPI on Bukkit. There are also Bukkit-specific features, such as the hook-paper-reload
configuration option, which may be configured using a CommandAPIBukkitConfig
instance.
For example, to load the CommandAPI on Bukkit with all logging disabled, you can use the following:
+CommandAPI.onLoad(new CommandAPIBukkitConfig(plugin).silentLogs(true));
+
+CommandAPI.onLoad(CommandAPIBukkitConfig(plugin).silentLogs(true))
+
+The onEnable()
method initializes the CommandAPI's enabling sequence. Similar to the onLoad(CommandAPIConfig)
method, this must be placed in your plugin's onEnable()
method. This isn't as strict as the onLoad(CommandAPIConfig)
method, and can be placed anywhere in your onEnable()
method.
The onDisable()
method disables the CommandAPI gracefully. This should be placed in your plugin's onDisable()
method. This doesn't unregister commands, so commands may persist during reloads - this can be mitigated using the CommandAPI.unregister()
method.
public class MyPlugin extends JavaPlugin {
+
+ @Override
+ public void onLoad() {
+ CommandAPI.onLoad(new CommandAPIBukkitConfig(this).verboseOutput(true)); // Load with verbose output
+
+ new CommandAPICommand("ping")
+ .executes((sender, args) -> {
+ sender.sendMessage("pong!");
+ })
+ .register();
+ }
+
+ @Override
+ public void onEnable() {
+ CommandAPI.onEnable();
+
+ // Register commands, listeners etc.
+ }
+
+ @Override
+ public void onDisable() {
+ CommandAPI.onDisable();
+ }
+
+}
+
+class MyPlugin : JavaPlugin() {
+
+ override fun onLoad() {
+ CommandAPI.onLoad(CommandAPIBukkitConfig(this).verboseOutput(true)) // Load with verbose output
+
+ CommandAPICommand("ping")
+ .executes(CommandExecutor { sender, _ ->
+ sender.sendMessage("pong!")
+ })
+ .register()
+ }
+
+ override fun onEnable() {
+ CommandAPI.onEnable()
+
+ // Register commands, listeners etc.
+ }
+
+ override fun onDisable() {
+ CommandAPI.onDisable()
+ }
+
+}
+
+By default, the CommandAPI is written in the dev.jorel.commandapi
package. It is highly recommended to "relocate" the shaded copy of the CommandAPI to your own package instead to prevent package clashes with other projects that shade the CommandAPI:
\begin{align} +&\qquad\texttt{dev.jorel.commandapi} \rightarrow \texttt{my.custom.package.commandapi} \\ +\end{align}
+To shade the CommandAPI into a maven project, you'll need to use the commandapi-bukkit-shade
dependency, which is optimized for shading and doesn't include plugin-specific files (such as plugin.yml
). Here you have a choice between the Spigot-mapped version and the Mojang-mapped version. You do not need to use commandapi-bukkit-core
if you are shading:
Add the CommandAPI shade dependency:
+<dependencies>
+ <dependency>
+ <groupId>dev.jorel</groupId>
+ <artifactId>commandapi-bukkit-shade</artifactId>
+ <version>9.5.2</version>
+ </dependency>
+</dependencies>
+
+<dependencies>
+ <dependency>
+ <groupId>dev.jorel</groupId>
+ <artifactId>commandapi-bukkit-shade-mojang-mapped</artifactId>
+ <version>9.5.2</version>
+ </dependency>
+</dependencies>
+
+You can shade the CommandAPI easily by adding the maven-shade-plugin
to your build sequence using version 3.3.0
(compatible with Java 16):
<build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <version>3.3.0</version>
+ <executions>
+ <execution>
+ <id>shade</id>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ </execution>
+ </executions>
+ <configuration>
+ <minimizeJar>true</minimizeJar>
+ <relocations>
+ <relocation>
+ <pattern>dev.jorel.commandapi</pattern>
+ <!-- TODO: Change this to my own package name -->
+ <shadedPattern>my.custom.package.commandapi</shadedPattern>
+ </relocation>
+ </relocations>
+ </configuration>
+ </plugin>
+ </plugins>
+</build>
+
+As we're shading the CommandAPI into your plugin, you don't need to add depend: [CommandAPI]
to your plugin.yml
file.
To shade the CommandAPI into a Gradle project, we'll use the Goooler Gradle Shadow Plugin. This is a fork of the Shadow Plugin which supports Java 21. Add this to your list of plugins:
+plugins {
+ id 'java'
+ id 'io.github.goooler.shadow' version '8.1.7'
+}
+
+plugins {
+ java
+ id("io.github.goooler.shadow") version "8.1.7"
+}
+
+Add our repositories:
+repositories {
+ mavenCentral()
+
+ // If you want to shade the NBT API as well
+ maven { url = "https://repo.codemc.org/repository/maven-public/" }
+}
+
+repositories {
+ mavenCentral()
+
+ // If you want to shade the NBT API as well
+ maven(url = "https://repo.codemc.org/repository/maven-public/")
+}
+
+Next, we declare our dependencies:
+dependencies {
+ implementation "dev.jorel:commandapi-bukkit-shade:9.5.2"
+}
+
+dependencies {
+ implementation "dev.jorel:commandapi-bukkit-shade-mojang-mapped:9.5.2"
+}
+
+dependencies {
+ implementation("dev.jorel:commandapi-bukkit-shade:9.5.2")
+}
+
+dependencies {
+ implementation("dev.jorel:commandapi-bukkit-shade-mojang-mapped:9.5.2")
+}
+
+Then we add it to the shadowJar
task configuration and relocate the CommandAPI to your desired location:
shadowJar {
+ dependencies {
+ include dependency("dev.jorel:commandapi-bukkit-shade:9.5.2")
+ }
+
+ // TODO: Change this to my own package name
+ relocate("dev.jorel.commandapi", "my.custom.package.commandapi")
+}
+
+shadowJar {
+ dependencies {
+ include dependency("dev.jorel:commandapi-bukkit-shade-mojang-mapped:9.5.2")
+ }
+
+ // TODO: Change this to my own package name
+ relocate("dev.jorel.commandapi", "my.custom.package.commandapi")
+}
+
+tasks.withType<ShadowJar> {
+ dependencies {
+ include(dependency("dev.jorel:commandapi-bukkit-shade:9.5.2"))
+ }
+
+ // TODO: Change this to my own package name
+ relocate("dev.jorel.commandapi", "my.custom.package.commandapi")
+}
+
+tasks.withType<ShadowJar> {
+ dependencies {
+ include(dependency("dev.jorel:commandapi-bukkit-shade-mojang-mapped:9.5.2"))
+ }
+
+ // TODO: Change this to my own package name
+ relocate("dev.jorel.commandapi", "my.custom.package.commandapi")
+}
+
+Finally, we can build the shaded jar using the following command:
+gradlew build shadowJar
+
+As we're shading the CommandAPI into your plugin, we don't need to add depend: [CommandAPI]
to your plugin.yml
file.
The annotation system is a separate part of the CommandAPI, and as a result it needs to be included as an additional dependency to your project.
+The annotation system effectively needs to be added twice: Once for compilation and again to invoke the annotation processor itself.
+Add the annotation dependency to your pom.xml
:
<dependencies>
+ <dependency>
+ <groupId>dev.jorel</groupId>
+ <artifactId>commandapi-annotations</artifactId>
+ <version>9.5.2</version>
+ <scope>provided</scope>
+ </dependency>
+</dependencies>
+
+Add the annotation processor as an annotation process to the compile task in the pom.xml
:
<build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>3.8.1</version>
+ <configuration>
+ <annotationProcessorPaths>
+ <path>
+ <groupId>dev.jorel</groupId>
+ <artifactId>commandapi-annotations</artifactId>
+ <version>9.5.2</version>
+ </path>
+ </annotationProcessorPaths>
+ </configuration>
+ </plugin>
+ </plugins>
+</build>
+
+If you haven't already done so, add the maven central repository to your build.gradle
file:
repositories {
+ mavenCentral()
+ maven { url = "https://repo.codemc.org/repository/maven-public/" }
+}
+
+repositories {
+ mavenCentral()
+ maven(url = "https://repo.codemc.org/repository/maven-public/")
+}
+
+Add the dependency and annotation processor to your list of dependencies in your build.gradle
file:
dependencies {
+ compileOnly "dev.jorel:commandapi-annotations:9.5.2"
+ annotationProcessor "dev.jorel:commandapi-annotations:9.5.2"
+}
+
+dependencies {
+ compileOnly("dev.jorel:commandapi-annotations:9.5.2")
+ annotationProcessor("dev.jorel:commandapi-annotations:9.5.2")
+}
+
+To register commands with the CommandAPI, we use the CommandAPICommand
class. It follows a simple builder pattern to improve readability.
I think the easiest way to explain it is with an example:
+// Create our command
+new CommandAPICommand("broadcastmsg")
+ .withArguments(new GreedyStringArgument("message")) // The arguments
+ .withAliases("broadcast", "broadcastmessage") // Command aliases
+ .withPermission(CommandPermission.OP) // Required permissions
+ .executes((sender, args) -> {
+ String message = (String) args.get("message");
+ Bukkit.getServer().broadcastMessage(message);
+ })
+ .register();
+
+// Create our command
+CommandAPICommand("broadcastmsg")
+ .withArguments(GreedyStringArgument("message")) // The arguments
+ .withAliases("broadcast", "broadcastmessage") // Command aliases
+ .withPermission(CommandPermission.OP) // Required permissions
+ .executes(CommandExecutor { sender, args ->
+ val message = args["message"] as String
+ Bukkit.getServer().broadcastMessage(message)
+ })
+ .register()
+
+First, we create a new CommandAPICommand
, with the name of the command that the sender must enter to run it.
Then, we create an argument to add to the command using withArguments
. This is described in more detail in the section on arguments.
In this example, we add an alias, "broadcast", to the command. This allows the sender to use either /broadcastmsg <message>
or /broadcast <message>
.
By using withPermission
, we require the sender to be an OP in order to run the command.
We control what the command does using executes
(this is described in more detail in the section on command executors).
Finally, we register the command to the CommandAPI using register
.
That's it! This simple snippet of code fully registers the command to the server. You don't need to supply a plugin instance, you don't have to create a custom class and you don't have to mess with the plugin.yml
file.
Throughout this documentation, we will use the various different methods for command registration to give you an idea of when and where certain methods are more suitable than others.
+CommandAPICommand
methodsThe CommandAPICommand
has various methods, which are outlined below:
new CommandAPICommand(String commandName)
+
+This constructor creates a new instance of the CommandAPICommand
object. This constructor requires the name of the command.
CommandAPICommand withArguments(List<Argument> arguments)
+CommandAPICommand withArguments(Argument... arguments)
+
+The withArguments
method is used to add arguments to your command. The arguments
parameter is appended to the the list of arguments for the command.
CommandAPICommand withPermission(CommandPermission)
+CommandAPICommand withPermission(String)
+
+The withPermission
method is used to assign a permission that is required to execute the command. (See the section on permissions for more info).
CommandAPICommand withRequirements(sender -> {})
+
+The withRequirements
method is used to assign additional constraints required to execute the command, similar to permissions. (See the section on requirements for more info).
CommandAPICommand withAliases(String... args)
+
+The withAliases
method is used to declare a list of aliases that can be used to run this command via. (See the section on aliases for more info).
CommandAPICommand withHelp(String shortDescription, fullDescription)
+CommandAPICommand withHelp(HelpTopic helpTopic)
+CommandAPICommand withShortDescription(String shortDescription)
+CommandAPICommand withFullDescription(String fullDescription)
+
+The withHelp
method, along with its specific withShortDescription
and withFullDescription
methods are used to declare the help topic for this command which is displayed in the /help
command. (See the section on help for more info).
CommandAPICommand withSubcommand(CommandAPICommand subcommand)
+
+The withSubcommand
method is used to declare a subcommand that leads on from the current command. (See the section on subcommands for more info).
CommandAPICommand executes((sender, args) -> {})
+CommandAPICommand executes(info -> {})
+
+Executes a command using the CommandSender
object.
CommandAPICommand executesPlayer((player, args) -> {})
+CommandAPICommand executesPlayer(info -> {})
+
+Executes a command only if the command sender is a Player
.
CommandAPICommand executesEntity((entity, args) -> {})
+CommandAPICommand executesEntity(info -> {})
+
+Executes a command only if the command sender is an Entity
.
CommandAPICommand executesCommandBlock((cmdblock, args) -> {})
+CommandAPICommand executesCommandBlock(info -> {})
+
+Executes a command only if the command sender is a BlockCommandSender
.
CommandAPICommand executesConsole((console, args) -> {})
+CommandAPICommand executesConsole(info -> {})
+
+Executes a command only if the command sender is a ConsoleCommandSender
.
CommandAPICommand executesProxy((proxy, args) -> {})
+CommandAPICommand executesProxy(info -> {})
+
+Executes a command only if the command sender is a ProxiedCommandSender
.
CommandAPICommand executesNative((proxy, args) -> {})
+CommandAPICommand executesNative(info -> {})
+
+Executes a command regardless of what the command sender is, using the NativeProxyCommandSender
. Read more about native proxied command senders here.
++Developer's Note:
+Sometimes, the Java compiler throws an error saying that a method is ambiguous for the type CommandAPICommand. This is due to a limitation in Java's type inference system and is not a fault of the CommandAPI. If we take the following code, used to spawn a pig:
++new CommandAPICommand("spawnpigs") + .executesPlayer((player, args) -> { + for(int i = 0; i < 10; i++) { + player.getWorld().spawnEntity(player.getLocation(), (EntityType) args.get(0)); + } + }) + .register(); +
The Java type inference system cannot determine what the type of the lambda
+(player, args) -> ()
is, therefore it produces the following compilation error:+The method executesPlayer(PlayerCommandExecutor) is ambiguous for the type CommandAPICommand +
This can easily be resolved by declaring the specific type of the command sender and the arguments. For example:
++new CommandAPICommand("spawnpigs") + .executesPlayer((Player player, CommandArguments args) -> { + for(int i = 0; i < 10; i++) { + player.getWorld().spawnEntity(player.getLocation(), (EntityType) args.get(0)); + } + }) + .register(); +
void register()
+
+Registers the command with the default minecraft
namespace. If you are shading you can set the default namespace using CommandAPIConfig#setNamespace(String)
or CommandAPIBukkitConfig#usePluginNamespace()
.
void register(String namespace)
+
+Registers the command with a custom provided namespace.
+void register(JavaPlugin plugin)
+
+Registers the command with the provided plugin's name.
+It is recommended to register commands in either the onLoad()
or onEnable()
method. With the CommandAPI, depending on whether you use onLoad()
or onEnable()
to load your commands depends on whether your plugin is used with Minecraft's functions:
When to load | What to do |
---|---|
onLoad() method | Register commands to be used in Minecraft functions (see the Function section for more info) |
onEnable() method | Register regular commands |
The CommandAPI does support registering commands outside of these methods while the server is running. Commands registered after the server is done loading should work the same as commands registered in onEnable
.
The CommandAPI allows you to remove commands completely from Minecraft's command list. This includes Vanilla commands, Bukkit commands, and plugin commands.
+There are three methods you might use when unregistering commands:
+CommandAPI.unregister(String commandName);
+CommandAPI.unregister(String commandName, boolean unregisterNamespaces);
+CommandAPIBukkit.unregister(String commandName, boolean unregisterNamespaces, boolean unregisterBukkit);
+
+To understand when and how to use these methods, you need to know a little about how Bukkit loads and sets up commands. This is basically the order of events when a Bukkit server starts:
+bukkit
namespace (E.g. bukkit:version
)onLoad
is calledplugin.yml
file and placed in the Bukkit CommandMapluckperms:lp
)onEnable
is calledminecraft
namespace (E.g. minecraft:gamemode
)Unregistering a command only works if it happens after the command is created. Bukkit's command system is special and has two locations where commands can exist -- either the Vanilla CommandDispatcher or the Bukkit CommandMap -- so you also need to know where your command is registered. With that in mind, here is what each of the unregister
methods do:
CommandAPI.unregister(String commandName);
+
+Unregisters a command from the Vanilla CommandDispatcher.
+CommandAPI.unregister(String commandName, boolean unregisterNamespaces);
+
+Unregisters a command from the Vanilla CommandDispatcher. If unregisterNamespaces
is true
, then any namespaced version of the command is also unregistered.
CommandAPIBukkit.unregister(String commandName, boolean unregisterNamespaces, boolean unregisterBukkit);
+
+Unregisters a command from Bukkit. As before, if unregisterNamespaces
is true
, then any namespaced version of the command is also unregistered. If unregisterBukkit
is true
, then only Bukkit commands in the Bukkit CommandMap are unregistered. If unregisterBukkit
is false
, only commands from the Vanilla CommandDispatcher are unregistered.
To give a better idea of how and when to use these methods, the rest of this page documents how to unregister different types of commands.
+/version
/version
is a command provided by Bukkit. Looking at the sequence of events above, that means it is created during step 2, before plugins are loaded in step 3. Consequently, the command will exist when our plugin's onLoad
method is called, so we'll unregister it there. The same code will work in onEnable
too, since step 4 is also after step 2.
Since this command exists in the Bukkit CommandMap, we'll need to use CommandAPIBukkit#unregister
with unregisterBukkit
set to true
. We'll also remove the namespaced version -- /bukkit:version
-- so unregisterNamespaces
will be true
. All together, the code looks like this:
@Override
+public void onLoad() {
+ CommandAPIBukkit.unregister("version", true, true);
+}
+
+override fun onLoad() {
+ CommandAPIBukkit.unregister("version", false, true)
+}
+
+With this plugin, executing /version
or /bukkit:version
will give the unknown command message. Note that aliases like /ver
and its namespaced version /bukkit:ver
will still work. To remove aliases as well, you need to unregister each as its own command. For, /ver
, that would mean calling CommandAPIBukkit.unregister("ver", true, true)
.
/gamemode
/gamemode
is a command provided by Vanilla Minecraft. Like the previous example, Vanilla commands are created in step 1, before plugins are loaded in step 3. For variety, we'll unregister the command in our plugin's onEnable
-- step 4 -- but the same code would also work in onLoad
.
Since this command exists in the Vanilla CommandDispatcher, we can use CommandAPI#unregister
. That works the same as CommandAPIBukkit#unregister
with unregisterBukkit
set to false
. We don't care about the namespace, so unregisterNamespaces
will be false
. That means we can use the simplest method, CommandAPI.unregister(String commandName)
, since it sets unregisterNamespaces
to false
by default. All together, the code looks like this:
@Override
+public void onEnable() {
+ CommandAPI.unregister("gamemode");
+}
+
+override fun onEnable() {
+ CommandAPI.unregister("gamemode")
+}
+
+With this code, executing /gamemode
will give the unknown command exception as expected. However, even though unregisterNamespaces
was false
, /minecraft:gamemode
can also not be run. This happens because Vanilla commands are given their namespace in step 6, after our plugin has removed /gamemode
.
When the server starts, /gamemode
is created in step 2 inside the Vanilla CommandDispatcher. In step 4, our plugin is enabled and we remove the /gamemode
command from that CommandDispatcher. After all the plugins enable, step 6 moves all commands in the Vanilla CommandDispatcher to the Bukkit CommandMap and gives them the minecraft
namespace. Since /gamemode
doesn't exist at this point, step 6 cannot create the /minecraft:gamemode
command. So, even though unregisterNamespaces
was false
, /minecraft:gamemode
doesn't exist anyway.
/gamemode
commandTo replace a command, first unregister the original command, then register a new implementation for that command.
+@Override
+public void onEnable() {
+ CommandAPI.unregister("gamemode");
+
+ // Register our new /gamemode, with survival, creative, adventure and spectator
+ new CommandAPICommand("gamemode")
+ .withArguments(new MultiLiteralArgument("gamemodes", "survival", "creative", "adventure", "spectator"))
+ .executes((sender, args) -> {
+ // Implementation of our /gamemode command
+ })
+ .register();
+}
+
+override fun onEnable() {
+ CommandAPI.unregister("gamemode");
+
+ // Register our new /gamemode, with survival, creative, adventure and spectator
+ CommandAPICommand("gamemode")
+ .withArguments(MultiLiteralArgument("gamemodes", "survival", "creative", "adventure", "spectator"))
+ .executes(CommandExecutor { sender, args ->
+ // Implementation of our /gamemode command
+ })
+ .register()
+}
+
+Now, when /gamemode
is executed, it will use the new implementation defined using the CommandAPI.
/luckperms:luckperms
The /luckperms
command is provided by the Bukkit LuckPerms plugin. Plugin commands are created during step 4, immediately before calling the onEnable
method of the respective plugin. In this case, unregistering the command in our own plugin's onLoad
would not work, since the command wouldn't exist yet. We also have to make sure that our onEnable
method is called after LuckPerm's. The best way to make sure that happens is to add LuckPerms as a depend
or softdepend
in our plugin's plugin.yml. You can read more about the different between depend
and softdepend
in Spigot's documentation, but that will look something like this:
name: MyPlugin
+main: some.package.name.Main
+version: 1.0
+depend:
+ - LuckPerms
+
+Since plugin commands are stored in the Bukkit CommandMap, we need to use CommandAPIBukkit#unregister
with unregisterBukkit
set to true
. For demonstration’s sake, we only want to unregister the namespaced version -- /luckperms:luckperms
-- and leave /luckperms
alone. To do this, give "luckperms:luckperms"
as the commandName
, and set unregisterNamespaces
to false
. All together, the code looks like this:
@Override
+public void onEnable() {
+ CommandAPIBukkit.unregister("luckperms:luckperms", false, true);
+}
+
+override fun onEnable() {
+ CommandAPIBukkit.unregister("luckperms:luckperms", false, true)
+}
+
+Executing /luckperms
will work as normal, but /luckperms:luckperms
will give the unknown command message.
Unregistering a command created by the CommandAPI is similar to both unregistering a Vanilla command and a plugin command. Like a Vanilla command, CommandAPI commands are stored in the Vanilla CommandDispatcher, so they should be unregistered with unregisterBukkit
set to false
. Like plugin commands, they may be created in onEnable
, so you need to make sure your plugin is enabled after the plugin that adds the command.
Unlike plugin commands, CommandAPI commands may be created in onLoad
, as discussed in Command loading order. That just means you may also be able to unregister the command in you own plugin's onLoad
. As always, simply make sure you unregister a command after it is created, and it will be removed properly.
For our example, let's say we want to unregister the /break
command created by the Bukkit Maven Example Project for the CommandAPI. If you look at that plugin's code, you can see that it registers the /break
command in it's onEnable
method. Therefore, we can unregister the command in our own plugin's onEnable
, making sure that our plugin will enable second by adding ExamplePlugin as a depend
or softdepend
.
name: MyPlugin
+main: some.package.name.Main
+version: 1.0
+depend:
+ - CommandAPI
+ - ExamplePlugin
+
+++Developer's Note:
+If you can't find the code where a CommandAPI command is registered or just don't have access to the code of a plugin, you can still figure out when a command is registered. If you set
+verbose-outputs
totrue
in the CommandAPI's configuration, it will log command registration.For the ExamplePlugin, setting
+verbose-outputs
totrue
gives this:+[Server thread/INFO]: [ExamplePlugin] Enabling ExamplePlugin v0.0.1 +[Server thread/INFO]: [CommandAPI] Registering command /break block<LocationArgument> +[Server thread/INFO]: [CommandAPI] Registering command /myeffect target<PlayerArgument> potion<PotionEffectArgument> +[Server thread/INFO]: [CommandAPI] Registering command /nbt nbt<NBTCompoundArgument> +
You can see that the ExamplePlugin registers its commands when
+onEnable
is called.
In summary, we will unregister the /break
command in our plugin's onEnable
. We added Example plugin to the depend
list in our plugin.yml so that our onEnable
method runs second. unregisterNamespaces
and unregisterBukkit
will be set to false
, and those are the default values, so we can simply use CommandAPI.unregister(String commandName)
. All together, the code looks like this:
@Override
+public void onEnable() {
+ CommandAPI.unregister("break");
+}
+
+override fun onEnable() {
+ CommandAPI.unregister("break")
+}
+
+Now, when you try to execute /break
, you will just get the unknown command message as if it never existed.
/help
If you look at the sequence of events at the top of this page, you might notice that Bukkit's /help
command gets its own place in step 5. Unlike the other Bukkit commands, /help
is special and gets registered after plugins are loaded and enabled (don't ask, I don't know why :P). That means unregistering /help
in onLoad
or onEnable
does not work, since the command doesn't exist yet.
In order to run our unregister task after the server is enabled, we can use Bukkit's Scheduler API. There are many ways to set up and run a task, and this should work in whatever way you like. You can even give the task zero delay, since Bukkit only starts processing tasks after the server is enabled.
+Since /help
is in the Bukkit CommandMap, we need to use CommandAPIBukkit#unregister
with unregisterBukkit
set to true
. We'll leave /bukkit:help
alone, so unregisterNamespaces
will be false
. All together, we can unregister Bukkit's /help
command with this code:
@Override
+public void onEnable() {
+ new BukkitRunnable() {
+ @Override
+ public void run() {
+ CommandAPIBukkit.unregister("help", false, true);
+ }
+ }.runTaskLater(this, 0);
+}
+
+override fun onEnable() {
+ object : BukkitRunnable() {
+ override fun run() {
+ CommandAPIBukkit.unregister("help", false, true)
+ }
+ }.runTaskLater(this, 0)
+}
+
+Funnily, if you try to execute /help
, the server will still tell you: Unknown command. Type "/help" for help.
. Luckily, unregisterNamespaces
was false
, so you can still use /bukkit:help
to figure out your problem.
/minecraft:gamemode
In the earlier example for Unregistering /gamemode
, even though unregisterNamespaces
was false
, the /minecraft:gamemode
command was also not executable. As explained up there, this happens because the namespaced version of commands in the Vanilla CommandDispatcher are not created until after plugins are loaded and enabled. Since we unregistered /gamemode
in onEnable
, when the time came for the server to transfer Vanilla commands into the Bukkit CommandMap, it didn't know to create the minecraft:gamemode
command. Consequently, this means we cannot normally remove only the /minecraft:gamemode
command without also unregistering /gamemode
.
Of course, it is still possible to only unregister /minecraft:gamemode
and the namespaced versions of other Vanilla commands. As always, in order to unregister a command, you have to unregister after the command is created. So, we just need to unregister /minecraft:gamemode
after the server is enabled. Like the previous special case, we can use Bukkit's Scheduler API to run our unregister task after the server is enabled.
While /minecraft:gamemode
only exists in the Bukkit CommandMap, it is the namespaced version of the Vanilla /gamemode
command, so it is considered a Vanilla command. That means unregisterBukkit
should be false
, which is what it defaults to when using CommandAPI#unregister
. The CommandAPI understands that once the server is enabled Vanilla commands will have been copied to the CommandMap, so it will be able to find /minecraft:gamemode
Finally, unregisterNamespaces
should be false
, and since that's the default value we don't have to include it. All together, the code looks like this:
@Override
+public void onEnable() {
+ new BukkitRunnable() {
+ @Override
+ public void run() {
+ CommandAPI.unregister("minecraft:gamemode");
+ }
+ }.runTaskLater(this, 0);
+}
+
+override fun onEnable() {
+ object : BukkitRunnable() {
+ override fun run() {
+ CommandAPI.unregister("minecraft:gamemode")
+ }
+ }.runTaskLater(this, 0)
+}
+
+With this code, /gamemode
will execute as normal, but /minecraft:gamemode
will give the unknown command message.
Developer's Note:
+Doing the opposite action here -- only unregistering /gamemode
but keeping /minecraft:gamemode
-- is not recommended. That would be the following code, where commandName
is "gamemode"
(or any command in the Vanilla CommandDispatcher), and unregisterNamespaces
is false
:
// NOT RECOMMENDED
+@Override
+public void onEnable() {
+ new BukkitRunnable() {
+ @Override
+ public void run() {
+ CommandAPI.unregister("gamemode");
+ }
+ }.runTaskLater(this, 0);
+}
+
+// NOT RECOMMENDED
+override fun onEnable() {
+ object : BukkitRunnable() {
+ override fun run() {
+ CommandAPI.unregister("gamemode")
+ }
+ }.runTaskLater(this, 0)
+}
+
+The expected outcome of this code is that /minecraft:gamemode
would work as expected, and /gamemode
would give the command not found message. However, that is only true for the player's commands. If you try to use /minecraft:gamemode
in the console, it will not work properly. Specifically, while you can tab-complete the command's label, minecraft:gamemode
the command's arguments will not have any suggestions. If you try to execute /minecraft:gamemode
in the console, it will always tell you your command is unknown or incomplete.
The main point is that if you ever try to unregister a Vanilla command after the server is enabled, the namespaced version of that command will break for the console. To avoid this issue, always set unregisterNamespaces
to true
if unregisterBukkit
is false
when unregistering commands after the server is enabled.
@Override
+public void onEnable() {
+ new BukkitRunnable() {
+ @Override
+ public void run() {
+ CommandAPI.unregister("gamemode", true);
+ }
+ }.runTaskLater(this, 0);
+}
+
+override fun onEnable() {
+ object : BukkitRunnable() {
+ override fun run() {
+ CommandAPI.unregister("gamemode", true)
+ }
+ }.runTaskLater(this, 0)
+}
+
+++Developer's Note:
+This section can be a little bit difficult to follow. If you only want the bare basic features (executes a command), read the section on Normal command executors - this behaves very similar to the
+onCommand
method in Bukkit.
The CommandAPI provides various command executors which are lambdas which execute the code you want when a command is called. With a lot of simplification, there are two main types of command executors:
+++Developer's Note:
+In general, you need not focus too much on what type of command executor to implement. If you know for certain that you're going to be using your command with command blocks, and specifically want to state whether a command returns a value, just ensure you return an integer at the end of your declared command executor. Java will infer the type (whether it's a normal command executor or a resulting command executor) automatically, so feel free to return an integer or not.
+
In addition to these two types of command executors, there are ways to restrict the execution of commands to certain CommandSender
subclasses. In other words, you can make commands executable by players in game only for instance. These restrictions are covered in more detail in Normal command executors.
Command executors are of the following format, where sender
is a CommandSender
, and args
is a CommandArguments
object, which represents arguments which are parsed by the CommandAPI.
new CommandAPICommand("...")
+ .executes((sender, args) -> {
+ //Code here
+ })
+ .register();
+
+With normal command executors, these do not need to return anything. By default, this will return a success value of 1 if it runs successfully, and a success value of 0 if it runs unsuccessfully, either by throwing an exception (RuntimeException) or by forcing the command to fail (See the section on handling command failures).
+In short, this is what values are returned when a command is executed from a normal command executor:
+Command Works | Command Doesn't Work | |
---|---|---|
Success Value | 1 | 0 |
Result Value | 1 | 0 |
To illustrate this, let's take a look at a simple message broadcasting command. We'll make a command which sends a message to everyone on the server, using the following syntax:
+/broadcastmsg <message>
+/broadcastmessage <message>
+/broadcast <message>
+
+We use an argument "message" to hold the message to broadcast, we provide some aliases and set a permission required to run the command. Then we declare our main command body by using the .executes()
method, before finally registering the command:
// Create our command
+new CommandAPICommand("broadcastmsg")
+ .withArguments(new GreedyStringArgument("message")) // The arguments
+ .withAliases("broadcast", "broadcastmessage") // Command aliases
+ .withPermission(CommandPermission.OP) // Required permissions
+ .executes((sender, args) -> {
+ String message = (String) args.get("message");
+ Bukkit.getServer().broadcastMessage(message);
+ })
+ .register();
+
+// Create our command
+CommandAPICommand("broadcastmsg")
+ .withArguments(GreedyStringArgument("message")) // The arguments
+ .withAliases("broadcast", "broadcastmessage") // Command aliases
+ .withPermission(CommandPermission.OP) // Required permissions
+ .executes(CommandExecutor { _, args ->
+ val message = args["message"] as String
+ Bukkit.getServer().broadcastMessage(message)
+ })
+ .register()
+
+Note how when we finish up our implementation of .executes()
, we don't return anything. This is unlike commands in the standard Bukkit API where the onCommand
method returns a Boolean value:
boolean onCommand(CommandSender, Command, String, String[])
+
+The returning of this Boolean value is handled automatically by the CommandAPI on a much lower level.
+The CommandAPICommand
class has multiple different executes...()
methods that can restrict the command sender to any of the following objects:
CommandSender Object | Method to use | Who can run this? |
---|---|---|
CommandSender | .executes() | Any CommandSender |
Player | .executesPlayer() | In-game players only |
Entity | .executesEntity() | Entities only |
BlockCommandSender | .executesCommandBlock() | Command blocks only |
ConsoleCommandSender | .executesConsole() | The console only |
ProxiedCommandSender | .executesProxy() | Proxied senders only (via /execute as ... ) |
NativeProxyCommandSender | .executesNative() | See Native commandsenders |
/suicide
commandSay we wanted to create a command /suicide
, which kills the player that executes it. Since this command can't be used by non-players (you can't kill a command block!), we can restrict it so only players can execute this command. Since it's a player, we can use the .executesPlayer()
method:
new CommandAPICommand("suicide")
+ .executesPlayer((player, args) -> {
+ player.setHealth(0);
+ })
+ .register();
+
+CommandAPICommand("suicide")
+ .executesPlayer(PlayerCommandExecutor { player, _ ->
+ player.setHealth(0.0)
+ })
+ .register()
+
+The CommandAPI allows you to chain different implementations of the command depending on the type of CommandSender
. This allows you to easily specify what types of CommandSender
s are required to run a command.
Extending on the suicide example above, we could write another implementation for a different CommandSender
. Here, we write an implementation to make entities (non-player) go out with a bang when they run the command (using /execute as <entity> run suicide
command).
/suicide
command with different implementationsnew CommandAPICommand("suicide")
+ .executesPlayer((player, args) -> {
+ player.setHealth(0);
+ })
+ .executesEntity((entity, args) -> {
+ entity.getWorld().createExplosion(entity.getLocation(), 4);
+ entity.remove();
+ })
+ .register();
+
+CommandAPICommand("suicide")
+ .executesPlayer(PlayerCommandExecutor { player, _ ->
+ player.setHealth(0.0)
+ })
+ .executesEntity(EntityCommandExecutor { entity, _ ->
+ entity.world.createExplosion(entity.location, 4f)
+ entity.remove()
+ })
+ .register()
+
+This saves having to use instanceof
multiple times to check the type of the CommandSender
.
The different command sender priority is the following (from highest priority to lowest priority):
+\begin{align} +&\quad\texttt{.executesNative()} && \texttt{(Always chosen if used)}\\ +&\quad\texttt{.executesPlayer()} \\ +&\quad\texttt{.executesEntity()} \\ +&\quad\texttt{.executesConsole()} \\ +&\quad\texttt{.executesCommandBlock()} \\ +&\quad\texttt{.executesProxy()} \\ +&\quad\texttt{.executes()} +\end{align}
+The CommandAPI also allows you to have multiple command executors with the same command implementation. This is useful for when you want to restrict the command sender required to run a command, but want to run the same code for each different command sender type.
+This is achieved using the .executes(executor, ...)
method, which accepts a variadic array of ExecutorType
objects. ExecutorType
has the following values:
ExecutorType Object | Who can run this? |
---|---|
ALL | Any CommandSender |
PLAYER | In-game players only |
ENTITY | Entities only |
BLOCK | Command blocks only |
CONSOLE | The console only |
PROXY | Proxied senders only (via /execute as ... ) |
NATIVE | See Native commandsenders |
/suicide
command with the same implementationExpanding on the suicide example above, we can restrict the command to only players and entities. We know that the command sender is a LivingEntity
, so we can cast to it safely.
new CommandAPICommand("suicide")
+ .executes((sender, args) -> {
+ LivingEntity entity;
+ if (sender instanceof ProxiedCommandSender proxy) {
+ entity = (LivingEntity) proxy.getCallee();
+ } else {
+ entity = (LivingEntity) sender;
+ }
+ entity.setHealth(0);
+ }, ExecutorType.PLAYER, ExecutorType.PROXY)
+ .register();
+
+CommandAPICommand("suicide")
+ .executes(CommandExecutor { sender, _ ->
+ val entity = (if (sender is ProxiedCommandSender) sender.callee else sender) as LivingEntity
+ entity.setHealth(0.0)
+ }, ExecutorType.PLAYER, ExecutorType.PROXY)
+ .register()
+
+You also have the option to implement a command executor which uses an ExecutionInfo
object:
new CommandAPICommand("...")
+ .executes(info -> {
+ // Use info here
+ })
+ .register();
+
+The ExecutionInfo
provides two user-facing methods which both let you access the CommandSender
and the arguments:
CommandSender sender();
+
+This method automatically detects which CommandSender type has been used, similar to the .executesXXX()
method (see here)! Therefore, casting is not necessary!
The second method lets you access the command's arguments:
+CommandArguments args();
+
+The CommandAPI has extra support for vanilla Minecraft's /execute
command, by allowing the CommandSender to be an instance of the ProxiedCommandSender
class. This allows the CommandSender to contain two extra pieces of information: The "proxied sender" and the original sender.
Say we have a command which kills the sender of a command. This is easily implemented as follows:
+new CommandAPICommand("killme")
+ .executesPlayer((player, args) -> {
+ player.setHealth(0);
+ })
+ .register();
+
+CommandAPICommand("killme")
+ .executesPlayer(PlayerCommandExecutor { player, _ ->
+ player.setHealth(0.0)
+ })
+ .register()
+
+commandAPICommand("killme") {
+ playerExecutor { player, _ ->
+ player.health = 0.0
+ }
+}
+
+But what if the sender of the command is not a player? By using Minecraft's /execute
command, we could execute the command as any arbitrary entity, as shown with the command below:
/execute as @e[type=chicken] run killme
+
+To handle this case, we can use the .executesProxy()
method to ensure that the command sender is a ProxiedCommandSender
. Then, we can kill the callee
(the entity which is being 'forced' to run the command /killme)
new CommandAPICommand("killme")
+ .executesPlayer((player, args) -> {
+ player.setHealth(0);
+ })
+ .executesProxy((proxy, args) -> {
+ // Check if the callee (target) is an Entity and kill it
+ if (proxy.getCallee() instanceof LivingEntity target) {
+ target.setHealth(0);
+ }
+ })
+ .register();
+
+CommandAPICommand("killme")
+ .executesPlayer(PlayerCommandExecutor { player, _ ->
+ player.setHealth(0.0)
+ })
+ .executesProxy(ProxyCommandExecutor { proxy, _ ->
+ // Check if the callee (target) is an Entity and kill it
+ if (proxy.callee is LivingEntity) {
+ (proxy.callee as LivingEntity).setHealth(0.0)
+ }
+ })
+ .register()
+
+commandAPICommand("killme") {
+ playerExecutor { player, _ ->
+ player.health = 0.0
+ }
+ proxyExecutor { proxy, _ ->
+ // Check if the callee (target) is an Entity and kill it
+ if (proxy.callee is LivingEntity) {
+ (proxy.callee as LivingEntity).health = 0.0
+ }
+ }
+}
+
+This allows the command above to run successfully, killing all chickens it can find.
+In a similar way that the ProxiedCommandSender
is used to store information about two command senders: a caller (the one that wrote the command) and a callee (the one that ends up executing the command), the CommandAPI also has a special NativeProxyCommandSender
class which is a more powerful representation of the ProxiedCommandSender
class. In addition to inheriting all of the methods from ProxiedCommandSender
, this class also has the following two methods:
public World getWorld();
+public Location getLocation();
+
+These methods contain additional information about the command executor's state, and are primarily designed to be used with Minecraft's /execute
command.
/execute
argumentsThe following table represents how the different /execute
arguments affect the NativeProxyCommandSender
class:
/execute argument | How it changes NativeProxyCommandSender |
---|---|
/execute align | Changes getLocation() only |
/execute anchored | Changes nothing |
/execute as | Changes getCallee() only |
/execute at | Changes getLocation() and getWorld() only |
/execute facing | Changes getLocation() only |
/execute in | Changes getWorld() only |
/execute positioned | Changes getLocation() only |
/execute rotated | Changes getLocation() only |
As described in the section about normal command executors, there are multiple methods to register a command executor. For the NativeProxyCommandSender
, the .executesNative()
method should be used.
++Note:
+The
+.executesNative()
method has the highest priority over all over.executesXXX()
methods - if you use the.executesNative()
method, no other execution method will be run.
Say we wanted to make a command that simply sets the current block to air. For this example, we'll use the following command syntax:
+/break
+
+As you can see, this command takes no arguments. This is fine, since our "argument" will be the sender's location. We can access the sender's location using the getLocation()
method from the NativeProxyCommandSender
object, available from the .executesNative()
method:
new CommandAPICommand("break")
+ .executesNative((sender, args) -> {
+ Location location = sender.getLocation();
+ if (location != null) {
+ location.getBlock().breakNaturally();
+ }
+ })
+ .register();
+
+CommandAPICommand("break")
+ .executesNative(NativeCommandExecutor { sender, _ ->
+ val location = sender.location
+ location.block.breakNaturally()
+ })
+ .register()
+
+commandAPICommand("break") {
+ nativeExecutor { sender, _ ->
+ val location = sender.location
+ location.block.breakNaturally()
+ }
+}
+
+This can now be used via the following command examples:
+/execute positioned 100 62 50 run break
+
+/execute at @e[type=pig] run break
+
+/execute in minecraft:overworld positioned 20 60 -20 run break
+
+Resulting command executors are very similar to normal command executors, except they can return an integer result value.
+(sender, args) -> {
+ //Code here
+ return /*some integer here*/ ;
+};
+
+Similarly, these will return a success value of 1 if it runs successfully, and a success value of 0 if it runs unsuccessfully. If a success value of 0 occurs, the result value will be 0. In short:
+Command Works | Command Doesn't Work | |
---|---|---|
Success Value | 1 | 0 |
Result Value | result defined in your code | 0 |
The concept of result values are better explained through examples:
+Say we want a command that returns a random number as a result. This can then be used by vanilla Minecraft's /execute store result ...
command, which can be used for other command block chains.
new CommandAPICommand("randnum")
+ .executes((sender, args) -> {
+ return ThreadLocalRandom.current().nextInt();
+ })
+ .register();
+
+CommandAPICommand("randnum")
+ .executes(ResultingCommandExecutor { _, _ ->
+ Random.nextInt()
+ })
+ .register()
+
+commandAPICommand("randnum") {
+ anyResultingExecutor { _, _ ->
+ Random.nextInt()
+ }
+}
+
+This returns a success value of 1 (Because no errors or CommandAPI.failWithString(String)
was thrown) and a result value of a random number.
/execute
commandWe can store state using /execute store
and we can perform conditional checks using /execute if
. By combining these, we can create a system which can be used with commandblocks to say, give players random lootboxes and redeem them. The concept is to create a command that generates a random number from 1 to 100. If the number is 1 (thus, the chance of being chosen is \(\frac{1}{100}\)), then we award a player with some reward, say 64 diamonds.
To do this, we'll declare two commands:
+/randomnumber - returns a random number between 1 and 99 (inclusive)
+/givereward <player> - gives a player 64 diamonds and broadcasts it in the chat
+
+Since we're declaring commands that are to be used in /execute
, we must ensure that these commands are registered in your plugin's onLoad()
method. First, we write our implementation for /randomnumber
. It is fairly straight forward using Java's ThreadLocalRandom
to generate a random number:
// Register random number generator command from 1 to 99 (inclusive)
+new CommandAPICommand("randomnumber")
+ .executes((sender, args) -> {
+ return ThreadLocalRandom.current().nextInt(1, 100); // Returns random number from 1 <= x < 100
+ })
+ .register();
+
+// Register random number generator command from 1 to 99 (inclusive)
+CommandAPICommand("randomnumber")
+ .executes(ResultingCommandExecutor { _, _ ->
+ (1..100).random() // Returns random number from 1 <= x < 100
+ })
+ .register()
+
+// Register random number generator command from 1 to 99 (inclusive)
+commandAPICommand("randomnumber") {
+ anyResultingExecutor { _, _ ->
+ (1..100).random()
+ }
+}
+
+Now we write our implementation for /givereward
. In this example, we use the EntitySelectorArgument
to select a single player. We cast it to Player
and then add the items to their inventory.
// Register reward giving system for a target player
+new CommandAPICommand("givereward")
+ .withArguments(new EntitySelectorArgument.OnePlayer("target"))
+ .executes((sender, args) -> {
+ Player player = (Player) args.get("target");
+ player.getInventory().addItem(new ItemStack(Material.DIAMOND, 64));
+ Bukkit.broadcastMessage(player.getName() + " won a rare 64 diamonds from a loot box!");
+ })
+ .register();
+
+// Register reward giving system for a target player
+CommandAPICommand("givereward")
+ .withArguments(EntitySelectorArgument.OnePlayer("target"))
+ .executes(CommandExecutor { _, args ->
+ val player = args["target"] as Player
+ player.inventory.addItem(ItemStack(Material.DIAMOND, 64))
+ Bukkit.broadcastMessage("${player.name} won a rare 64 diamonds from a loot box!")
+ })
+ .register()
+
+// Register reward giving system for a target player
+commandAPICommand("givereward") {
+ entitySelectorArgumentOnePlayer("target")
+ anyExecutor { _, args ->
+ val player = args["target"] as Player
+ player.inventory.addItem(ItemStack(Material.DIAMOND, 64))
+ Bukkit.broadcastMessage("${player.name} won a rare 64 diamonds from a loot box!")
+ }
+}
+
+Now that we've declared these commands, we can now use them in practice. We can use a command block to store a random number under the scoreboard score randVal
for a player called SomePlayer
, by executing the command /randomnumber
. Since /randomnumber
returns an integer, this value is stored in the scoreboard score:
/execute store result score SomePlayer randVal run randomnumber
+
+To check if the random number is equal to 1, we can use the /execute if
command. If their score stored in randVal
matches 1, then we run the /givereward
command.
/execute if score SomePlayer randVal matches 1 run givereward SomePlayer
+
+Sometimes, you want your command to fail on purpose. This is the way to "gracefully" handle errors in your command execution. This is performed by throwing any of the following methods:
+throw CommandAPI.failWithString(String message);
+throw CommandAPI.failWithMessage(Message message);
+throw CommandAPIBukkit.failWithBaseComponents(BaseComponent... message);
+throw CommandAPIBukkit.failWithAdventureComponent(Component message);
+throw CommandAPIBukkit.failWithAdventureComponent(ComponentLike message);
+
+When the CommandAPI handles the fail method, it will cause the command to return a success value of 0, to indicate failure.
+Say we have some list containing fruit and the player can choose from it. In order to do that, we can use a StringArgument
and suggest it to the player using .replaceSuggestions(info -> String[])
. However, because this only lists suggestions to the player, it does not stop the player from entering an option that isn't on the list of suggestions.
Therefore, to gracefully handle this with a proper error message, we use one of the CommandAPI.failWithXXX()
methods above with a meaningful error message which is displayed to the user.
// Array of fruit
+String[] fruit = new String[] {"banana", "apple", "orange"};
+
+// Register the command
+new CommandAPICommand("getfruit")
+ .withArguments(new StringArgument("item").replaceSuggestions(ArgumentSuggestions.strings(fruit)))
+ .executes((sender, args) -> {
+ String inputFruit = (String) args.get("item");
+
+ if (Arrays.stream(fruit).anyMatch(inputFruit::equals)) {
+ // Do something with inputFruit
+ } else {
+ // The sender's input is not in the list of fruit
+ throw CommandAPI.failWithString("That fruit doesn't exist!");
+ }
+ })
+ .register();
+
+// List of fruit
+val fruit = listOf<String>("banana", "apple", "orange")
+
+// Register the command
+CommandAPICommand("getfruit")
+ .withArguments(StringArgument("item").replaceSuggestions(ArgumentSuggestions.strings(fruit)))
+ .executes(CommandExecutor { _, args ->
+ val inputFruit = args["item"] as String
+
+ if(fruit.any { it == inputFruit }) {
+ // Do something with inputFruit
+ } else {
+ // The sender's input is not in the list of fruit
+ throw CommandAPI.failWithString("That fruit doesn't exist!")
+ }
+ })
+ .register()
+
+++Developer's Note:
+In general, it's a good idea to handle unexpected cases with one of the
+CommandAPI.failWithXXX()
methods. Most arguments used by the CommandAPI will have their own built-in failsafe system (e.g. theEntitySelectorArgument
will not execute the command executor if it fails to find an entity), so this feature is for those extra cases.
Arguments in the CommandAPI are registered by using an Argument[]
or List<Argument>
object. There are two things you need to keep in mind when creating arguments:
By definition of a List
, the order of the elements inserted into it are preserved, meaning the order you add arguments to the List
will be the resulting order of which arguments are presented to the user when they run that command.
Adding arguments for registration is simple:
+// Create a List
+List<Argument> arguments = new ArrayList<>();
+
+// Add an argument with the node "target", which is a PlayerArgument
+arguments.add(new PlayerArgument("target"));
+
+The String value is the node that is registered into Minecraft's internal command graph. This name is also used as a prompt that is shown to a player when they are entering the command.
+The CommandAPI is very flexible when it comes to registering arguments, and lets you use a number of different methods to suit your preference:
+new CommandAPICommand("mycommand")
+ .withArguments(new StringArgument("arg0"))
+ .withArguments(new StringArgument("arg1"))
+ .withArguments(new StringArgument("arg2"))
+ // And so on
+
+CommandAPICommand("mycommand")
+ .withArguments(StringArgument("arg0"))
+ .withArguments(StringArgument("arg1"))
+ .withArguments(StringArgument("arg2"))
+ // And so on
+
+commandAPICommand("mycommand") {
+ stringArgument("arg0")
+ stringArgument("arg1")
+ stringArgument("arg2")
+ // And so on
+}
+
+new CommandAPICommand("mycommand")
+ .withArguments(new StringArgument("arg0"), new StringArgument("arg1"), new StringArgument("arg2"))
+ // And so on
+
+CommandAPICommand("mycommand")
+ .withArguments(StringArgument("arg0"), StringArgument("arg1"), StringArgument("arg2"))
+ // And so on
+
+commandAPICommand("mycommand") {
+ arguments(StringArgument("arg0"), StringArgument("arg1"), StringArgument("arg2"))
+ // And so on
+}
+
+List<Argument<?>> arguments = new ArrayList<>();
+arguments.add(new StringArgument("arg0"));
+arguments.add(new StringArgument("arg1"));
+arguments.add(new StringArgument("arg2"));
+
+new CommandAPICommand("mycommand")
+ .withArguments(arguments)
+ // And so on
+
+val arguments = listOf(
+ StringArgument("arg0"),
+ StringArgument("arg1"),
+ StringArgument("arg2")
+)
+
+CommandAPICommand("mycommand")
+ .withArguments(arguments)
+ // And so on
+
+val arguments = listOf(
+ StringArgument("arg0"),
+ StringArgument("arg1"),
+ StringArgument("arg2")
+)
+
+commandAPICommand("mycommand") {
+ arguments(*arguments.toTypedArray())
+ // And so on
+}
+
+To access arguments, they have to be casted to the type that the argument represents. The order of the arguments in the CommandArguments args
is the same as the order in which the arguments were declared.
List<Argument<?>> commandArguments = new ArrayList<>();
+commandArguments.add(new StringArgument("arg0"));
+commandArguments.add(new PotionEffectArgument("arg1"));
+commandArguments.add(new LocationArgument("arg2"));
+
+new CommandAPICommand("cmd")
+ .withArguments(commandArguments)
+ .executes((sender, args) -> {
+ String stringArg = (String) args.get("arg0");
+ PotionEffectType potionArg = (PotionEffectType) args.get("arg1");
+ Location locationArg = (Location) args.get("arg2");
+ })
+ .register();
+
+val commandArguments = listOf(
+ StringArgument("arg0"),
+ PotionEffectArgument("arg1"),
+ LocationArgument("arg2")
+)
+
+CommandAPICommand("cmd")
+ .withArguments(commandArguments)
+ .executes(CommandExecutor { _, args ->
+ val stringArg = args["arg0"] as String
+ val potionArg = args["arg1"] as PotionEffectType
+ val locationArg = args["arg2"] as Location
+ })
+ .register()
+
+val args = listOf(
+ StringArgument("arg0"),
+ PotionEffectArgument("arg1"),
+ LocationArgument("arg2")
+)
+
+commandAPICommand("cmd") {
+ arguments(*args.toTypedArray())
+ anyExecutor { _, args ->
+ val stringArg = args["arg0"] as String
+ val potionArg = args["arg1"] as PotionEffectType
+ val locationArg = args["arg2"] as Location
+ }
+}
+
+The type to cast each argument (declared in the dev.jorel.commandapi.arguments
package) is listed below:
Argument class | Data type |
---|---|
AngleArgument | float |
AdvancementArgument | org.bukkit.advancement.Advancement |
AdventureChatArgument | net.kyori.adventure.text.Component |
AdventureChatColorArgument | net.kyori.adventure.text.format.NamedTextColor |
AdventureChatComponentArgument | net.kyori.adventure.text.Component |
AxisArgument | java.util.EnumSet<org.bukkit.Axis> |
BiomeArgument | org.bukkit.block.Biome |
BiomeArgument.NamespacedKey | org.bukkit.NamespacedKey |
BlockPredicateArgument | java.util.function.Predicate <org.bukkit.block.Block> |
BlockStateArgument | org.bukkit.block.data.BlockData |
BooleanArgument | boolean |
ChatArgument | net.md_5.bungee.api.chat.BaseComponent[] |
ChatColorArgument | org.bukkit.ChatColor |
ChatComponentArgument | net.md_5.bungee.api.chat.BaseComponent[] |
CommandArgument | dev.jorel.commandapi.wrappers.CommandResult |
CustomArgument<T, B> | T |
DoubleArgument | double |
EnchantmentArgument | org.bukkit.enchantments.Enchantment |
EntitySelectorArgument.ManyEntities | Collection<org.bukkit.entity.Entity> |
EntitySelectorArgument.ManyPlayers | Collection<org.bukkit.entity.Player> |
EntitySelectorArgument.OneEntity | org.bukkit.entity.Entity |
EntitySelectorArgument.OnePlayer | org.bukkit.entity.Player |
EntityTypeArgument | org.bukkit.entity.EntityType |
FloatArgument | float |
FloatRangeArgument | dev.jorel.commandapi.wrappers.FloatRange |
FunctionArgument | dev.jorel.commandapi.wrappers.FunctionWrapper[] |
GreedyStringArgument | String |
IntegerArgument | int |
IntegerRangeArgument | dev.jorel.commandapi.wrappers.IntegerRange |
ItemStackArgument | org.bukkit.inventory.ItemStack |
ItemStackPredicateArgument | java.util.function.Predicate <org.bukkit.inventory.ItemStack> |
ListArgument | java.util.Collection<T> |
LiteralArgument | N/A |
Location2DArgument | dev.jorel.commandapi.wrappers.Location2D |
LocationArgument | org.bukkit.Location |
LongArgument | long |
LootTableArgument | org.bukkit.loot.LootTable |
MapArgument | java.util.LinkedHashMap |
MathOperationArgument | dev.jorel.commandapi.wrappers.MathOperation |
MultiLiteralArgument | String |
NamespacedKeyArgument | org.bukkit.NamespacedKey |
NBTCompoundArgument<T> | The cast type changes depending on whether you're shading the CommandAPI or using the CommandAPI as a plugin:
|
ObjectiveArgument | org.bukkit.scoreboard.Objective |
ObjectiveCriteriaArgument | String |
OfflinePlayerArgument | org.bukkit.OfflinePlayer |
ParticleArgument | dev.jorel.commandapi.wrappers.ParticleData |
PlayerArgument | org.bukkit.entity.Player |
PotionEffectArgument | org.bukkit.potion.PotionEffectType |
PotionEffectArgument.NamespacedKey | org.bukkit.NamespacedKey |
RecipeArgument | The cast type changes depending on your Minecraft version:
|
RotationArgument | dev.jorel.commandapi.wrappers.Rotation |
ScoreboardSlotArgument | dev.jorel.commandapi.wrappers.ScoreboardSlot |
ScoreHolderArgument.Single | String |
ScoreHolderArgument.Multiple | Collection<String> |
SoundArgument | org.bukkit.Sound |
SoundArgument.NamespacedKey | org.bukkit.NamespacedKey |
StringArgument | String |
TeamArgument | org.bukkit.scoreboard.Team |
TextArgument | String |
TimeArgument | int |
UUIDArgument | java.util.UUID |
WorldArgument | org.bukkit.World |
The CommandArguments
class was introduced in CommandAPI 9.0.0 and provides a much more powerful way of accessing arguments than just an array of arguments which existed until 9.0.0.
While the argument array just gives the possibility to access the arguments via the array notation (args[0]
), the CommandArguments
class offers much more, including:
To access the inner structure of the CommandArguments
class directly, it provides various methods which you can learn about below:
Get the argument array
+Object[] args();
+
+This returns the array of arguments as defined when creating your command.
+Get the arguments mapped to their node name
+Map<String, Object> argsMap();
+
+This returns an unmodifiable map which contains the arguments mapped to their node names.
+Get the raw argument array
+String[] rawArgs();
+
+This returns the array of raw arguments. An explanation of what raw arguments are can be found in the section about accessing raw arguments.
+Get the raw arguments mapped to their node name
+Map<String, String> rawArgsMap();
+
+This returns an unmodifiable map which contains the raw arguments mapped to their node names. An explanation of what raw arguments are can be found in the section about accessing raw arguments.
+Other useful methods
+String fullInput(); // Returns the full command input (including the / character)
+int count(); // Returns the amount of arguments
+
+The CommandArguments
class provides its arguments in a way similar to how a List
or Map
let you access their contents. When using these methods, you need to cast the arguments to their respective type. The CommandArguments
class also provides a way to access unsafe arguments.
You can choose to access arguments by their node name or by their index.
+Accessing arguments by their node name is the recommended way of accessing arguments.
+There are four methods you can use to access arguments by their node name:
+Object get(String nodeName);
+Object getOrDefault(String nodeName, Object defaultValue);
+Object getOrDefault(String nodeName, Supplier<?> defaultValue);
+Optional<Object> getOptional(String nodeName);
+
+Accessing arguments by their index is the original way of accessing arguments. However, we recommend to access arguments by node name.
+Similar to the four methods of accessing arguments by their node name, there also are four methods you can use to access arguments by their index:
+Object get(int index);
+Object getOrDefault(int index, Object defaultValue);
+Object getOrDefault(int index, Supplier<?> defaultValue);
+Optional<Object> getOptional(int index);
+
+To demonstrate the different ways of accessing arguments, we want to register a command /mycommand
like this:
/mycommand <name> <amount>
+/mycommand <name> <amount> <player>
+/mycommand <name> <amount> <player> <target>
+/mycommand <name> <amount> <player> <target> <message>
+
+This is how these commands are implemented:
+new CommandAPICommand("mycommand")
+ .withArguments(new StringArgument("name"))
+ .withArguments(new IntegerArgument("amount"))
+ .withOptionalArguments(new PlayerArgument("player"))
+ .withOptionalArguments(new PlayerArgument("target"))
+ .withOptionalArguments(new GreedyStringArgument("message"))
+ .executesPlayer((player, args) -> {
+ String name = (String) args.get(0); // Access arguments by index
+ int amount = (int) args.get("amount"); // Access arguments by node name
+ Player p = (Player) args.getOrDefault("player", player); // Access arguments using the getOrDefault(String, Object) method
+ Player target = (Player) args.getOrDefault("target", () -> player); // Access arguments using the getOrDefault(String, Supplier<?>) method
+ String message = (String) args.getOptional("message").orElse("Hello!"); // Access arguments using the getOptional(String) method
+
+ // Do whatever with these values
+ })
+ .register();
+
+CommandAPICommand("mycommand")
+ .withArguments(StringArgument("name"))
+ .withArguments(IntegerArgument("amount"))
+ .withOptionalArguments(PlayerArgument("player"))
+ .withOptionalArguments(PlayerArgument("target"))
+ .withOptionalArguments(GreedyStringArgument("message"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val name = args[0] as String // Access arguments by index
+ val amount = args["amount"] as Int // Access arguments by node name
+ val p = args.getOrDefault("player", player) as Player // Access arguments using the getOrDefault(String, Object) method
+ val target = args.getOrDefault("target") { player } as Player // Access arguments using the getOrDefault(String, Supplier<?>) method
+ val message = args.getOptional("message").orElse("Hello!") as String // Access arguments using the getOptional(String) method
+
+ // Do whatever with these values
+ })
+ .register();
+
+A "raw argument" is the String
form of an argument as written in a command. For example:
A user defines a command /mycommand
that accepts a double
as the first argument and an entity selector as the second argument. It could be executed with the values 15.3
as the double
value and @e
as the entity selector:
/mycommand 15.3 @e
+
+When accessing the raw arguments of this command there are 15.3
and @e
available as String
s.
However, when accessing the arguments of this command there is 15.3
available as double
and @e
available as Collection<Entity>
.
Raw arguments are accessed basically the same way you would access arguments. You can access them by their node name and their index in the argument array.
+Accessing raw arguments by their node name is the recommended way of doing it.
+To access raw arguments by their node name, you can use these methods:
+String getRaw(String nodeName);
+String getOrDefaultRaw(String nodeName, String defaultValue);
+String getOrDefaultRaw(String nodeName, Supplier<String> defaultValue);
+Optional<String> getRawOptional(String nodeName);
+
+Of course, if you don't want to access raw arguments by their node name, we also provide the option to access them by index with these methods:
+String getRaw(int index);
+String getOrDefaultRaw(int index, String defaultValue);
+String getOrDefaultRaw(int index, Supplier<String> defaultValue);
+Optional<String> getRawOptional(int index);
+
+To demonstrate how to access raw arguments, we are going to implement the /mycommand
again, this time with the following syntax:
/mycommand <entities>
+
+We want to find out which entity selector is being used when the command is executed.
+new CommandAPICommand("mycommand")
+ .withArguments(new EntitySelectorArgument.ManyEntities("entities"))
+ .executesPlayer((player, args) -> {
+ String entitySelector = args.getRaw("entities"); // Access the raw argument with getRaw(String)
+
+ // Do whatever with the entity selector
+ })
+ .register();
+
+CommandAPICommand("mycommand")
+ .withArguments(EntitySelectorArgument.ManyEntities("entities"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val entitySelector = args.getRaw("entities")!! // Access the raw argument with getRaw(String)
+
+ // Do whatever with the entity selector
+ })
+ .register();
+
+When accessing arguments you need to cast the Object
returned by these methods to the type the argument returns. More about casting arguments here.
Unsafe arguments provide the ability to access an argument without needing to cast it to the argument's type. When not using unsafe arguments, your code looks like this:
+String name = (String) args.get("name");
+
+When using unsafe arguments you can make your code look like this:
+String name = args.getUnchecked("name");
+
+Unsafe arguments can also be accessed by their node names and their indices.
+Unsafe arguments can also be accessed by node name which, again, is the recommended way of doing it.
+Use these methods when accessing unsafe arguments by their node name:
+T getUnchecked(String nodeName);
+T getOrDefaultUnchecked(String nodeName, T defaultValue);
+T getOrDefaultUnchecked(String nodeName, Supplier<T> defaultValue);
+Optional<T> getOptionalUnchecked(String nodeName);
+
+If you want to access unsafe arguments by index, you can do that by using these methods:
+T getUnchecked(int index);
+T getOrDefaultUnchecked(int index, T defaultValue);
+T getOrDefaultUnchecked(int index, Supplier<T> defaultValue);
+Optional<T> getOptionalUnchecked(int index);
+
+Finally, we want to implement the /mycommand
again. This time we use this syntax:
/mycommand <player>
+
+Here, we don't actually want to cast the argument, so we use unsafe arguments to remove that cast:
+new CommandAPICommand("mycommand")
+ .withArguments(new PlayerArgument("player"))
+ .executesPlayer((player, args) -> {
+ Player p = args.getUnchecked("player");
+
+ // Do whatever with the player
+ })
+ .register();
+
+CommandAPICommand("mycommand")
+ .withArguments(PlayerArgument("player"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val p: Player = args.getUnchecked("player")!!
+
+ // Do whatever with the player
+ })
+ .register();
+
+Developer's Note:
+The following methods cannot be used to access a value returned by a CustomArgument
as its return type depends on the base argument for it.
Lastly, the CommandArguments class offers you a way to access your arguments in a more safe way by using internal casts. Again, methods are offered to access arguments by their +index or their node name:
+T getByClass(String nodeName, Class<T> argumentType);
+T getByClassOrDefault(String nodeName, Class<T> argumentType, T defaultValue);
+T getOptionalByClass(String nodeName, Class<T> argumentType);
+T getByClass(int index, Class<T> argumentType);
+T getByClassOrDefault(int index, Class<T> argumentType, T defaultValue);
+T getOptionalByClass(int index, Class<T> argumentType);
+
+Compared to the other methods the CommandArguments
class offers, these methods take an additional parameter of type Class<T>
where T
is the return type
+of the argument with the given node name or index.
For example, say you declared a new StringArgument("value")
and you now want to access the return value of this argument using safe casting. This would be done as follows:
String value = args.getByClass("value", String.class);
+
+val value = args.getByClass("value", String::class.java)
+
+Finally, there is one more, even safer way of accessing safe arguments: by using an argument instance:
+T getByArgument(Argument<T> argumentType);
+T getByArgumentOrDefault(Argument<T> argumentType, T defaultValue);
+T getOptionalByArgument(Argument<T> argumentType);
+
+However, while safer, this also introduces the need to first initialize your arguments before you can start implementing your command. +To visualize this, we want to implement the command from Access arguments by node name and index again, but this time using safe arguments with an argument instance:
+StringArgument nameArgument = new StringArgument("name");
+IntegerArgument amountArgument = new IntegerArgument("amount");
+PlayerArgument playerArgument = new PlayerArgument("player");
+PlayerArgument targetArgument = new PlayerArgument("target");
+GreedyStringArgument messageArgument = new GreedyStringArgument("message");
+
+new CommandAPICommand("mycommand")
+ .withArguments(nameArgument)
+ .withArguments(amountArgument)
+ .withOptionalArguments(playerArgument)
+ .withOptionalArguments(targetArgument)
+ .withOptionalArguments(messageArgument)
+ .executesPlayer((player, args) -> {
+ String name = args.getByArgument(nameArgument);
+ int amount = args.getByArgument(amountArgument);
+ Player p = args.getByArgumentOrDefault(playerArgument, player);
+ Player target = args.getByArgumentOrDefault(targetArgument, player);
+ String message = args.getOptionalByArgument(messageArgument).orElse("Hello!");
+
+ // Do whatever with these values
+ })
+ .register();
+
+val nameArgument = StringArgument("name")
+val amountArgument = IntegerArgument("amount")
+val playerArgument = PlayerArgument("player")
+val targetArgument = PlayerArgument("target")
+val messageArgument = GreedyStringArgument("message")
+
+CommandAPICommand("mycommand")
+ .withArguments(nameArgument)
+ .withArguments(amountArgument)
+ .withOptionalArguments(playerArgument)
+ .withOptionalArguments(targetArgument)
+ .withOptionalArguments(messageArgument)
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val name: String = args.getByArgument(nameArgument)!!
+ val amount: Int = args.getByArgument(amountArgument)!!
+ val p: Player = args.getByArgumentOrDefault(playerArgument, player)
+ val target: Player = args.getByArgumentOrDefault(targetArgument, player)
+ val message: String = args.getOptionalByArgument(messageArgument).orElse("Hello!")
+
+ // Do whatever with these values
+ })
+ .register();
+
+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);
+
+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:
+withOptionalArguments
method to add an optional argument to a commandargs.get("target")
to get our player out of the argumentsWith 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.
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.
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.
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!")
+ }
+}
+
+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:
+CommandAPICommand
and you add arguments to it.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:
+B
will be ignored so the OptionalArgumentException
will not be thrownB
has been enteredThis 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
.
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")
+ }
+}
+
+Arguments have a setting which determine whether or not they are present in the CommandArguments args
that is populated when executing a command.
By default, the LiteralArgument
has this setting set to false
, hence the literal values are not present in the CommandArguments args
.
This flag is set using the following function:
+Argument setListed(boolean listed);
+
+Say we have the following command:
+/mycommand <player> <value> <message>
+
+Let's also say that in our implementation of this command, we don't actually perform any processing for <value>
. Hence, listing it in the CommandArguments args
is unnecessary.
new CommandAPICommand("mycommand")
+ .withArguments(new PlayerArgument("player"))
+ .withArguments(new IntegerArgument("value").setListed(false))
+ .withArguments(new GreedyStringArgument("message"))
+ .executes((sender, args) -> {
+ // args == [player, message]
+ Player player = (Player) args.get("player");
+ String message = (String) args.get("message"); // Note that the IntegerArgument is not available in the CommandArguments
+ player.sendMessage(message);
+ })
+ .register();
+
+CommandAPICommand("mycommand")
+ .withArguments(PlayerArgument("player"))
+ .withArguments(IntegerArgument("value").setListed(false))
+ .withArguments(GreedyStringArgument("message"))
+ .executes(CommandExecutor { _, args ->
+ // args == [player, message]
+ val player = args["player"] as Player
+ val message = args["message"] as String // Note that the IntegerArgument is not available in the CommandArguments
+ player.sendMessage(message)
+ })
+ .register()
+
+In this scenario, the argument <value>
is not present in the CommandArguments args
for the executor.
Sometimes, you want to modify the list of suggestions that are provided by an argument. To handle this, CommandAPI arguments have two methods:
+Argument replaceSuggestions(ArgumentSuggestions suggestions);
+Argument includeSuggestions(ArgumentSuggestions suggestions);
+
+The replaceSuggestions
method replaces all suggestions with the provided list of suggestions, whereas the includeSuggestions
method will include the provided suggestions with the suggestions already present by the argument.
Because argument suggestions are arguably the most powerful feature that the CommandAPI offers, I've split this section into a number of subsections. To give an overview on what CommandAPI argument suggestions can do:
+ArgumentSuggestions
interfaceThe two methods above require an ArgumentSuggestions
object, which is a functional interface that takes in a SuggestionInfo
record and the current Brigadier SuggestionsBuilder
and returns a CompletableFuture<Suggestions>
object. This may sound a bit complicated, but this allows you to implement very powerful suggestions using a combination of the CommandAPI and raw Brigadier API methods. More information about using Brigadier-level suggestions can be found in the brigadier suggestions section.
To simplify this, the CommandAPI provides a number of methods to generate suggestions:
+ArgumentSuggestions strings(String... suggestions);
+ArgumentSuggestions strings(Collection<String>);
+ArgumentSuggestions strings(Function<SuggestionInfo, String[]> suggestions);
+ArgumentSuggestions stringCollection(Function<SuggestionInfo<CommandSender>, Collection<String>>);
+ArgumentSuggestions stringsAsync(Function<SuggestionInfo, CompletableFuture<String[]>> suggestions);
+ArgumentSuggestions stringCollectionAsync(Function<SuggestionInfo<CommandSender>, CompletableFuture<Collection<String>>>);
+
+ArgumentSuggestions stringsWithTooltips(IStringTooltip... suggestions);
+ArgumentSuggestions stringsWithTooltips(Collection<IStringTooltip>);
+ArgumentSuggestions stringsWithTooltips(Function<SuggestionInfo, IStringTooltip[]> suggestions);
+ArgumentSuggestions stringsWithTooltipsCollection(Function<SuggestionInfo<CommandSender>, Collection<IStringTooltip>>);
+ArgumentSuggestions stringsWithTooltipsAsync(Function<SuggestionInfo, CompletableFuture<IStringTooltip[]>> suggestions);
+ArgumentSuggestions stringsWithTooltipsCollectionAsync(Function<SuggestionInfo<CommandSender>, CompletableFuture<Collection<IStringTooltip>>>);
+
+SuggestionsInfo
recordArgument suggestion methods can accept a function which takes in a SuggestionsInfo
object and returns a suitable format for suggestions. The SuggestionInfo
class is a record which contains the following methods:
public record SuggestionInfo {
+ CommandSender sender();
+ CommandArguments previousArgs();
+ String currentInput();
+ String currentArg();
+}
+
+These methods can be used to aid with providing context-aware suggestions for users. The fields are as follows:
+CommandSender sender();
+
+sender()
represents the command sender which is typing this command and requesting these suggestions. This is normally a Player
, but can also be a console command sender if using a Paper server.
CommandArguments previousArgs();
+
+previousArgs()
represents the previously declared arguments, which are parsed and interpreted as if they were being used to execute the command. See this example on the string argument suggestions page for an idea of how to utilize this field.
String currentInput();
+
+currentInput()
represents the current input that the command sender has entered. This is effectively everything that they have typed, including the leading /
symbol which is required to start a command. If a user is typing /mycommand hellowor¦
, the result of currentInput()
would be "/mycommand hellowor"
.
String currentArg();
+
+currentArg()
represents the current text which the command sender has entered for the argument which you're trying to provide suggestions for. If a user is typing /mycommand hellowor¦
, the result of currentArg()
would be "hellowor"
.
The first method, replaceSuggestions(ArgumentSuggestions suggestions)
, allows you to replace the suggestions normally associated with that argument. This can be replaced with an array of strings by using the strings(String... suggestions)
method.
Say we're creating a plugin with the ability to teleport to different warps on the server. If we were to retrieve a list of warps, we would be able to replace the suggestions of a typical StringArgument
to teleport to that warp. Let's create a command with the following syntax:
/warp <warp>
+
+We then implement our warp teleporting command using replaceSuggestions()
on the StringArgument
to provide a list of warps to teleport to:
List<Argument<?>> arguments = new ArrayList<>();
+arguments.add(new StringArgument("world").replaceSuggestions(ArgumentSuggestions.strings(
+ "northland", "eastland", "southland", "westland"
+)));
+
+new CommandAPICommand("warp")
+ .withArguments(arguments)
+ .executesPlayer((player, args) -> {
+ String warp = (String) args.get("world");
+ player.teleport(warps.get(warp)); // Look up the warp in a map, for example
+ })
+ .register();
+
+val arguments = listOf<Argument<*>>(
+ StringArgument("world").replaceSuggestions(ArgumentSuggestions.strings(
+ "northland", "eastland", "southland", "westland"
+ ))
+)
+
+CommandAPICommand("warp")
+ .withArguments(arguments)
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val warp = args["world"] as String
+ player.teleport(warps[warp]!!) // Look up the warp in a map, for example
+ })
+ .register()
+
+The second method, includeSuggestions(ArgumentSuggestions suggestions)
, allows you to include additional suggestions in combination with the list of existing suggestions for a command.
The strings(Function<SuggestionInfo, String[]> suggestions)
method in ArgumentSuggestions
allows you to modify suggestions normally associated with that argument with an array of strings that are evaluated dynamically using information about the command sender, using the sender()
method.
Say you have a plugin which has a "friend list" for players. If you want to teleport to a friend in that list, you could use a PlayerArgument
, which has the list of suggestions replaced with the list of friends that that player has. Since the list of friends depends on the sender, we can use the function to determine what our suggestions should be. Let's use the following command to teleport to a friend from our friend list:
/friendtp <friend>
+
+Let's say we have a simple class to get the friends of a command sender:
+public class Friends {
+
+ static Map<UUID, String[]> friendsMap = new HashMap<>();
+
+ public static String[] getFriends(CommandSender sender) {
+ if (sender instanceof Player player) {
+ // Look up friends in a database or file
+ return friendsMap.get(player.getUniqueId());
+ } else {
+ return new String[0];
+ }
+ }
+}
+
+class Friends {
+
+ companion object {
+
+ val friends = mutableMapOf<UUID, Array<String>>()
+
+ fun getFriends(sender: CommandSender): Array<String> {
+ if (sender is Player) {
+ // Look up friends in a database or file
+ return friends[sender.uniqueId]!!
+ } else {
+ return arrayOf<String>()
+ }
+ }
+
+ }
+
+}
+
+We can then use this to generate our suggested list of friends:
+List<Argument<?>> arguments = new ArrayList<>();
+arguments.add(new PlayerArgument("friend").replaceSuggestions(ArgumentSuggestions.strings(info ->
+ Friends.getFriends(info.sender())
+)));
+
+new CommandAPICommand("friendtp")
+ .withArguments(arguments)
+ .executesPlayer((player, args) -> {
+ Player target = (Player) args.get("friend");
+ player.teleport(target);
+ })
+ .register();
+
+val arguments = listOf<Argument<*>>(
+ PlayerArgument("friend").replaceSuggestions(ArgumentSuggestions.strings { info ->
+ Friends.getFriends(info.sender())
+ } )
+)
+
+CommandAPICommand("friendtp")
+ .withArguments(arguments)
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val target = args["friend"] as Player
+ player.teleport(target)
+ })
+ .register()
+
+The strings(Function<SuggestionInfo, String[]>)
method also has the capability to suggest arguments based on the values of previously inputted arguments, using the previousArgs()
method in SuggestionInfo
. This previousArgs()
method returns a list of previous arguments which are parsed exactly like any regular CommandAPI command argument.
Note:
+The ability to use previously declared arguments does not work via redirects. This means that any command that comes before it that leads into a command that uses suggestions depending on previous arguments will not work. For example, if we had a command /mycommand <arg1> <arg2> <arg3>
and ran it as normal, it would work as normal:
/mycommand arg1 arg2 arg3
+
+However, if we redirect execution via the /execute
command to have the following:
/execute run mycommand <suggestions>
+
+This won't work, because we make use of a redirect:
+\(\texttt{/execute run} \xrightarrow{redirect} \texttt{mycommand arg1 arg2 arg3}\)
+It is not possible to access the CommandArguments
of previously declared arguments. If a command occurs via a redirect, the CommandArguments
of previously declared arguments will be null.
Say we wanted to create a command that lets you send a message to a specific player in a given radius. (This is a bit of a contrived example, but let's roll with it). To do this, we'll use the following command syntax:
+/localmsg <radius> <target> <message>
+
+When run, this command will send a message to a target player within the provided radius. To help identify which players are within a radius, we can replace the suggestions on the <target>
argument to include a list of players within the provided radius. We do this with the following code:
// Declare our arguments as normal
+List<Argument<?>> commandArgs = new ArrayList<>();
+commandArgs.add(new IntegerArgument("radius"));
+
+// Replace the suggestions for the PlayerArgument.
+// info.sender() refers to the command sender that is running this command
+// info.previousArgs() refers to the Object[] of previously declared arguments (in this case, the IntegerArgument radius)
+commandArgs.add(new PlayerArgument("target").replaceSuggestions(ArgumentSuggestions.strings(info -> {
+
+ // Cast the first argument (radius, which is an IntegerArgument) to get its value
+ int radius = (int) info.previousArgs().get("radius");
+
+ // Get nearby entities within the provided radius
+ Player player = (Player) info.sender();
+ Collection<Entity> entities = player.getWorld().getNearbyEntities(player.getLocation(), radius, radius, radius);
+
+ // Get player names within that radius
+ return entities.stream()
+ .filter(e -> e.getType() == EntityType.PLAYER)
+ .map(Entity::getName)
+ .toArray(String[]::new);
+})));
+commandArgs.add(new GreedyStringArgument("message"));
+
+// Declare our command as normal
+new CommandAPICommand("localmsg")
+ .withArguments(commandArgs)
+ .executesPlayer((player, args) -> {
+ Player target = (Player) args.get("target");
+ String message = (String) args.get("message");
+ target.sendMessage(message);
+ })
+ .register();
+
+// Declare our arguments as normal
+val commandArgs = mutableListOf<Argument<*>>()
+commandArgs.add(IntegerArgument("radius"))
+
+// Replace the suggestions for the PlayerArgument.
+// info.sender() refers to the command sender that is running this command
+// info.previousArgs() refers to the Object[] of previously declared arguments (in this case, the IntegerArgument radius)
+commandArgs.add(PlayerArgument("target").replaceSuggestions(ArgumentSuggestions.strings { info: SuggestionInfo<CommandSender> ->
+
+ // Cast the first argument (radius, which is an IntegerArgument) to get its value
+ val radius = (info.previousArgs()["radius"] as Int).toDouble()
+
+ // Get nearby entities within the provided radius
+ val player = info.sender() as Player
+ val entities = player.world.getNearbyEntities(player.location, radius, radius, radius)
+
+ // Get player names within that radius
+ entities
+ .filter { it.type == EntityType.PLAYER }
+ .map { it.name }
+ .toTypedArray()
+}))
+commandArgs.add(GreedyStringArgument("message"))
+
+// Declare our command as normal
+CommandAPICommand("localmsg")
+ .withArguments(*commandArgs.toTypedArray())
+ .executesPlayer(PlayerCommandExecutor { _, args ->
+ val target = args["target"] as Player
+ val message = args["message"] as String
+ target.sendMessage(message)
+ })
+ .register()
+
+As shown in this code, we use the previousArgs()
method access the previously declared arguments. In this example, info.previousArgs()
will be { int }
, where this int
refers to the radius. Note how this object array only has the previously declared arguments (and not for example { int, Player, String }
).
So far, we've covered how to replace suggestions using the replaceSuggestions()
method. The issue with using strings for suggestion listings is that they are prone to errors - it is possible to suggest something which is not actually a valid argument, which makes that suggestion unusable. As a result, some arguments include the replaceSafeSuggestions()
, which provides type-safety checks for argument suggestions, as well as automatic "Bukkit-to-suggestion" conversion.
The whole point of the safe argument suggestions method is that parameters entered in this method are guaranteed to work.
+The use of the safe replace suggestions function is the same as replaceSuggestions()
from the previous section, except instead of returning a String[]
, you now return a T[]
, where T
is the class corresponding to the argument. This is described in more detail in the table below.
Argument replaceSafeSuggestions(SafeSuggestions<T> suggestions);
+Argument includeSafeSuggestions(SafeSuggestions<T> suggestions);
+
+SafeSuggestions
interfaceSimilar to the ArgumentSuggestions
interface, safe suggestions use the SafeSuggestions
interface which is a functional interface that takes in a mapping function from an Object to a String and returns some ArgumentSuggestions
which represent the argument's suggestions. Again, this is typically implemented for anyone that wants to use a more powerful suggestion system.
As with ArgumentSuggestions
, the CommandAPI provides some methods to generate safe suggestions:
SafeSuggestions<T> suggest(T... suggestions);
+SafeSuggestions<T> suggest(Function<SuggestionInfo, T[]> suggestions);
+SafeSuggestions<T> suggestAsync(Function<SuggestionInfo, CompletableFuture<T[]>> suggestions);
+
+SafeSuggestions<T> tooltips(Tooltip<T>... suggestions);
+SafeSuggestions<T> tooltips(Function<SuggestionInfo, Tooltip<T>[]> suggestions);
+SafeSuggestions<T> tooltipsAsync(Function<SuggestionInfo, CompletableFuture<Tooltip<T>[]>> suggestions);
+
+Not all arguments support safe suggestions. This is mostly due to implementation constraints or inadequate support by the Bukkit API.
+The list of supported arguments are displayed in the following table. The parameter T
(shown in the method signatures above) are also provided for each argument. This parameter is the same as the cast argument described in Argument Casting, except for a few exceptions which are outlined in bold.
Argument | Class (T) |
---|---|
AdvancementArgument | org.bukkit.advancement.Advancement |
AxisArgument | java.util.EnumSet<org.bukkit.Axis> |
BiomeArgument | org.bukkit.block.Biome |
BooleanArgument | Boolean |
ChatColorArgument | org.bukkit.ChatColor |
DoubleArgument | Double |
EnchantmentArgument | org.bukkit.enchantments.Enchantment |
EntityTypeArgument | org.bukkit.entity.EntityType |
FloatArgument | Float |
FloatRangeArgument | dev.jorel.commandapi.wrappers.FloatRange |
FunctionArgument | org.bukkit.NamespacedKey |
GreedyStringArgument | String |
IntegerArgument | Integer |
IntegerRangeArgument | dev.jorel.commandapi.wrappers.IntegerRange |
ItemStackArgument | org.bukkit.inventory.ItemStack |
Location2DArgument | dev.jorel.commandapi.wrappers.Location2D |
LocationArgument | org.bukkit.Location |
LongArgument | Long |
LootTableArgument | org.bukkit.loot.LootTable |
MathOperationArgument | dev.jorel.commandapi.wrappers.MathOperation |
NBTCompoundArgument | de.tr7zw.nbtapi.NBTContainer |
ObjectiveArgument | org.bukkit.scoreboard.Objective |
OfflinePlayerArgument | org.bukkit.OfflinePlayer |
ParticleArgument | org.bukkit.Particle |
PlayerArgument | org.bukkit.entity.Player |
PotionEffectArgument | org.bukkit.potion.PotionEffectType |
RecipeArgument | org.bukkit.inventory.Recipe |
RotationArgument | dev.jorel.commandapi.wrappers.Rotation |
ScoreboardSlotArgument | dev.jorel.commandapi.wrappers.ScoreboardSlot |
SoundArgument | org.bukkit.Sound |
TeamArgument | org.bukkit.scoreboard.Team |
TimeArgument | dev.jorel.commandapi.wrappers.Time |
WorldArgument | org.bukkit.World |
While most of the arguments are fairly straight forward, I'd like to bring your attention to the TimeArgument
's safe suggestions function. This uses dev.jorel.commandapi.wrappers.Time
as the class for T
to ensure type-safety. The Time
class has three static methods:
Time ticks(int ticks);
+Time days(int days);
+Time seconds(int seconds);
+
+These create representations of ticks (e.g. 40t
), days (e.g. 2d
) and seconds (e.g. 60s
) respectively.
Although all safe arguments are indeed "type-safe", the function argument uses a NamespacedKey
which cannot be checked fully at compile time. As a result, this is argument should be used with caution - providing a NamespacedKey
suggestion that does not exist when the server is running will cause that command to fail if that suggestion is used.
Scoreboard slots now include two new static methods so they can be used with safe arguments:
+ScoreboardSlot of(DisplaySlot slot);
+ScoreboardSlot ofTeamColor(ChatColor color);
+
+This allows you to create ScoreboardSlot
instances which can be used with the safe replace suggestions method.
While this should be fairly straight forward, here's a few examples of how this can be used in practice:
+Say we have a plugin that registers custom items which can be crafted. In this example, we use an "emerald sword" with a custom crafting recipe. Now say that we want to have a command that gives the player the item from our declared recipes, which will have the following syntax:
+/giverecipe <recipe>
+
+To do this, we first register our custom items:
+// Create our itemstack
+ItemStack emeraldSword = new ItemStack(Material.DIAMOND_SWORD);
+ItemMeta meta = emeraldSword.getItemMeta();
+meta.setDisplayName("Emerald Sword");
+meta.setUnbreakable(true);
+emeraldSword.setItemMeta(meta);
+
+// Create and register our recipe
+ShapedRecipe emeraldSwordRecipe = new ShapedRecipe(new NamespacedKey(this, "emerald_sword"), emeraldSword);
+emeraldSwordRecipe.shape(
+ "AEA",
+ "AEA",
+ "ABA"
+);
+emeraldSwordRecipe.setIngredient('A', Material.AIR);
+emeraldSwordRecipe.setIngredient('E', Material.EMERALD);
+emeraldSwordRecipe.setIngredient('B', Material.BLAZE_ROD);
+getServer().addRecipe(emeraldSwordRecipe);
+
+// Omitted, more itemstacks and recipes
+
+// Create our itemstack
+val emeraldSword = ItemStack(Material.DIAMOND_SWORD)
+val meta = emeraldSword.itemMeta
+meta?.setDisplayName("Emerald Sword")
+meta?.isUnbreakable = true
+emeraldSword.itemMeta = meta
+
+// Create and register our recipe
+val emeraldSwordRecipe = ShapedRecipe(NamespacedKey(this, "emerald_sword"), emeraldSword)
+emeraldSwordRecipe.shape(
+ "AEA",
+ "AEA",
+ "ABA"
+)
+emeraldSwordRecipe.setIngredient('A', Material.AIR)
+emeraldSwordRecipe.setIngredient('E', Material.EMERALD)
+emeraldSwordRecipe.setIngredient('B', Material.BLAZE_ROD)
+server.addRecipe(emeraldSwordRecipe)
+
+// Omitted, more itemstacks and recipes
+
+Once we've done that, we can now include them in our command registration. To do this, we use replaceSafeSuggestions(recipes)
and then register our command as normal:
// Safely override with the recipe we've defined
+List<Argument<?>> arguments = new ArrayList<>();
+arguments.add(new RecipeArgument("recipe").replaceSafeSuggestions(SafeSuggestions.suggest(info ->
+ new Recipe[] { emeraldSwordRecipe, /* Other recipes here */ }
+)));
+
+// Register our command
+new CommandAPICommand("giverecipe")
+ .withArguments(arguments)
+ .executesPlayer((player, args) -> {
+ Recipe recipe = (Recipe) args.get("recipe");
+ player.getInventory().addItem(recipe.getResult());
+ })
+ .register();
+
+// Safely override with the recipe we've defined
+val arguments = listOf<Argument<*>>(
+ RecipeArgument("recipe").replaceSafeSuggestions(SafeSuggestions.suggest {
+ arrayOf(emeraldSwordRecipe, /* Other recipes here */)
+ })
+)
+
+// Register our command
+CommandAPICommand("giverecipe")
+ .withArguments(arguments)
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val recipe = args["recipe"] as Recipe
+ player.inventory.addItem(recipe.result)
+ })
+ .register()
+
+Say we have a command to spawn mobs:
+/spawnmob <mob>
+
+Now say that we don't want non-op players to spawn bosses. To do this, we'll create a List<EntityType>
which is the list of all mobs that non-ops are allowed to spawn:
EntityType[] forbiddenMobs = new EntityType[] {EntityType.ENDER_DRAGON, EntityType.WITHER};
+List<EntityType> allowedMobs = new ArrayList<>(Arrays.asList(EntityType.values()));
+allowedMobs.removeAll(Arrays.asList(forbiddenMobs)); // Now contains everything except enderdragon and wither
+
+val forbiddenMobs = listOf<EntityType>(EntityType.ENDER_DRAGON, EntityType.WITHER)
+val allowedMobs = EntityType.values().toMutableList()
+allowedMobs.removeAll(forbiddenMobs) // Now contains everything except enderdragon and wither
+
+We then use our safe arguments to return an EntityType[]
as the list of values that are suggested to the player. In this example, we use the sender()
method to determine if the sender has permissions to view the suggestions:
List<Argument<?>> safeArguments = new ArrayList<>();
+safeArguments.add(new EntityTypeArgument("mob").replaceSafeSuggestions(SafeSuggestions.suggest(
+ info -> {
+ if (info.sender().isOp()) {
+ // All entity types
+ return EntityType.values();
+ } else {
+ // Only allowedMobs
+ return allowedMobs.toArray(new EntityType[0]);
+ }
+ })
+));
+
+val safeArguments = listOf<Argument<*>>(
+ EntityTypeArgument("mob").replaceSafeSuggestions(SafeSuggestions.suggest {
+ info ->
+ if (info.sender().isOp) {
+ // All entity types
+ EntityType.values()
+ } else {
+ // Only allowedMobs
+ allowedMobs.toTypedArray()
+ }
+ }
+ )
+)
+
+Now we register our command as normal:
+new CommandAPICommand("spawnmob")
+ .withArguments(safeArguments)
+ .executesPlayer((player, args) -> {
+ EntityType entityType = (EntityType) args.get("mob");
+ player.getWorld().spawnEntity(player.getLocation(), entityType);
+ })
+ .register();
+
+CommandAPICommand("spawnmob")
+ .withArguments(safeArguments)
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val entityType = args["mob"] as EntityType
+ player.world.spawnEntity(player.location, entityType)
+ })
+ .register()
+
+Say we wanted to remove a potion effect from a player. To do this, we'll use the following command syntax:
+/removeeffect <player> <potioneffect>
+
+Now, we don't want to remove a potion effect that already exists on a player, so instead we'll use the safe arguments to find a list of potion effects on the target player and then only suggest those potion effects. To do this, we'll use the previousArguments()
method, as it allows us to access the previously defined <player>
argument.
List<Argument<?>> safeArgs = new ArrayList<>();
+safeArgs.add(new EntitySelectorArgument.OnePlayer("target"));
+safeArgs.add(new PotionEffectArgument("potioneffect").replaceSafeSuggestions(SafeSuggestions.suggest(
+ info -> {
+ Player target = (Player) info.previousArgs().get(0);
+
+ // Convert PotionEffect[] into PotionEffectType[]
+ return target.getActivePotionEffects().stream()
+ .map(PotionEffect::getType)
+ .toArray(PotionEffectType[]::new);
+ })
+));
+
+val safeArgs = mutableListOf<Argument<*>>()
+safeArgs.add(EntitySelectorArgument.OnePlayer("target"))
+safeArgs.add(PotionEffectArgument("potioneffect").replaceSafeSuggestions(SafeSuggestions.suggest {
+ info ->
+ val target = info.previousArgs()["target"] as Player
+
+ // Convert PotionEffect[] into PotionEffectType[]
+ target.activePotionEffects.map{ it.type }.toTypedArray()
+ })
+)
+
+And then we can register our command as normal:
+new CommandAPICommand("removeeffect")
+ .withArguments(safeArgs)
+ .executesPlayer((player, args) -> {
+ Player target = (Player) args.get("target");
+ PotionEffectType potionEffect = (PotionEffectType) args.get("potioneffect");
+ target.removePotionEffect(potionEffect);
+ })
+ .register();
+
+CommandAPICommand("removeeffect")
+ .withArguments(safeArgs)
+ .executesPlayer(PlayerCommandExecutor { _, args ->
+ val target = args["target"] as Player
+ val potionEffect = args["potioneffect"] as PotionEffectType
+ target.removePotionEffect(potionEffect)
+ })
+ .register()
+
+The CommandAPI can also display tooltips for specific argument suggestions. These are shown to the user when they hover over a given suggestion and can be used to provide more context to a user about the suggestions that are shown to them. In this section, we'll outline the two ways of creating suggestions with tooltips:
+Tooltips can have formatting to change how the text is displayed by using the ChatColor
class.
To use these features, the CommandAPI includes the stringsWithTooltips
methods for arguments, that accept IStringTooltip
objects instead of String
objects:
ArgumentSuggestions stringsWithTooltips(IStringTooltip... suggestions);
+ArgumentSuggestions stringsWithTooltips(Function<SuggestionInfo, IStringTooltip[]> suggestions);
+
+The StringTooltip
class is the CommandAPI's default implementation of IStringTooltip
, which has some static methods to construct tooltips easily:
StringTooltip none(String suggestion);
+StringTooltip ofString(String suggestion, String tooltip);
+StringTooltip ofMessage(String suggestion, Message tooltip);
+StringTooltip ofBaseComponents(String suggestion, BaseComponent... tooltip);
+StringTooltip ofAdventureComponent(String suggestion, Component tooltip);
+
+The first method, StringTooltip.none(String)
creates a normal suggestion entry with no tooltip. The other methods create a suggestion with the provided tooltip text in either String
, Brigadier Message
, Spigot BaseComponent[]
or Adventure Component
format.
Say we want to create a simple command to provide in-game emotes between players. For example, if you did /emote wave Bob
, you'll "wave" to the player Bob. For this example, we'll use the following command syntax:
/emote <emote> <target>
+
+First, we'll declare our arguments. Here, we'll use the stringsWithTooltips
method, along with the StringTooltip.ofString(String, String)
method to create emote suggestions and include suitable descriptions:
List<Argument<?>> arguments = new ArrayList<>();
+arguments.add(new StringArgument("emote")
+ .replaceSuggestions(ArgumentSuggestions.stringsWithTooltips(info ->
+ new IStringTooltip[] {
+ StringTooltip.ofString("wave", "Waves at a player"),
+ StringTooltip.ofString("hug", "Gives a player a hug"),
+ StringTooltip.ofString("glare", "Gives a player the death glare")
+ }
+ ))
+);
+arguments.add(new PlayerArgument("target"));
+
+val arguments = mutableListOf<Argument<*>>()
+arguments.add(StringArgument("emote")
+ .replaceSuggestions(ArgumentSuggestions.stringsWithTooltips { info ->
+ arrayOf<IStringTooltip>(
+ StringTooltip.ofString("wave", "Waves at a player"),
+ StringTooltip.ofString("hug", "Gives a player a hug"),
+ StringTooltip.ofString("glare", "Gives a player the death glare")
+ )
+ })
+)
+arguments.add(PlayerArgument("target"))
+
+Finally, we declare our command as normal:
+new CommandAPICommand("emote")
+ .withArguments(arguments)
+ .executesPlayer((player, args) -> {
+ String emote = (String) args.get("emote");
+ Player target = (Player) args.get("target");
+
+ switch (emote) {
+ case "wave":
+ target.sendMessage(player.getName() + " waves at you!");
+ break;
+ case "hug":
+ target.sendMessage(player.getName() + " hugs you!");
+ break;
+ case "glare":
+ target.sendMessage(player.getName() + " gives you the death glare...");
+ break;
+ default:
+ player.sendMessage("Invalid emote '" + emote + "'!");
+ break;
+ }
+ })
+ .register();
+
+CommandAPICommand("emote")
+ .withArguments(*arguments.toTypedArray())
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val emote = args["emote"] as String
+ val target = args["target"] as Player
+
+ when (emote) {
+ "wave" -> target.sendMessage("${player.name} waves at you!")
+ "hug" -> target.sendMessage("${player.name} hugs you!")
+ "glare" -> target.sendMessage("${player.name} gives you the death glare...")
+ }
+ })
+ .register()
+
+IStringTooltip
directlyThe IStringTooltip
interface can be implemented by any other class to provide tooltips for custom objects. The IStringTooltip
interface has the following methods:
public interface IStringTooltip {
+ public String getSuggestion();
+ public Message getTooltip();
+}
+
+++Note that the
+Message
class is from the Brigadier library, which you will have to add as a dependency to your plugin. Information on how to do that can be found here.
This is incredibly useful if you are using suggestions with custom objects, such as a plugin that has custom items.
+IStringTooltip
for custom itemsLet's say we've created a simple plugin which has custom items. For a custom item, we'll have a simple class CustomItem
that sets its name, lore and attached itemstack:
public @SuppressWarnings("deprecation")
+class CustomItem implements IStringTooltip {
+
+ private ItemStack itemstack;
+ private String name;
+
+ public CustomItem(ItemStack itemstack, String name, String lore) {
+ ItemMeta meta = itemstack.getItemMeta();
+ meta.setDisplayName(name);
+ meta.setLore(Arrays.asList(lore));
+ itemstack.setItemMeta(meta);
+ this.itemstack = itemstack;
+ this.name = name;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public ItemStack getItem() {
+ return this.itemstack;
+ }
+
+ @Override
+ public String getSuggestion() {
+ return this.itemstack.getItemMeta().getDisplayName();
+ }
+
+ @Override
+ public Message getTooltip() {
+ return BukkitTooltip.messageFromString(this.itemstack.getItemMeta().getLore().get(0));
+ }
+
+}
+
+class CustomItem(val item: ItemStack, val name: String, lore: String): IStringTooltip {
+
+ init {
+ val meta = item.itemMeta
+ meta.setDisplayName(name)
+ meta.lore = listOf(lore)
+ item.itemMeta = meta
+ }
+
+ override fun getSuggestion(): String = this.item.itemMeta.displayName
+
+ override fun getTooltip(): Message = BukkitTooltip.messageFromString(this.item.itemMeta.lore?.get(0) ?: "")
+
+}
+
+We make use of the Tooltip.messageFromString()
method to generate a Brigadier Message
object from our string tooltip.
Let's also say that our plugin has registered lots of CustomItem
s and has this stored in a CustomItem[]
in our plugin. We could then use this as our input for suggestions:
CustomItem[] customItems = new CustomItem[] {
+ new CustomItem(new ItemStack(Material.DIAMOND_SWORD), "God sword", "A sword from the heavens"),
+ new CustomItem(new ItemStack(Material.PUMPKIN_PIE), "Sweet pie", "Just like grandma used to make")
+};
+
+new CommandAPICommand("giveitem")
+ .withArguments(new StringArgument("item").replaceSuggestions(ArgumentSuggestions.stringsWithTooltips(customItems))) // We use customItems[] as the input for our suggestions with tooltips
+ .executesPlayer((player, args) -> {
+ String itemName = (String) args.get("item");
+
+ // Give them the item
+ for (CustomItem item : customItems) {
+ if (item.getName().equals(itemName)) {
+ player.getInventory().addItem(item.getItem());
+ break;
+ }
+ }
+ })
+ .register();
+
+val customItems = arrayOf<CustomItem>(
+ CustomItem(ItemStack(Material.DIAMOND_SWORD), "God sword", "A sword from the heavens"),
+ CustomItem(ItemStack(Material.PUMPKIN_PIE), "Sweet pie", "Just like grandma used to make")
+)
+
+CommandAPICommand("giveitem")
+ .withArguments(StringArgument("item").replaceSuggestions(ArgumentSuggestions.stringsWithTooltips(*customItems))) // We use customItems[] as the input for our suggestions with tooltips
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val itemName = args["item"] as String
+
+ // Give them the item
+ for (item in customItems) {
+ if (item.name == itemName) {
+ player.inventory.addItem(item.item)
+ break
+ }
+ }
+ })
+ .register()
+
+Using tooltips with safe suggestions is almost identical to the method described above for normal suggestions, except for two things. Firstly, you must use tooltips
method instead of the stringsWithTooltips
method and secondly, instead of using StringTooltip
, you must use Tooltip<S>
. Let's look at these differences in more detail.
The tooltips
methods are fairly similar to the stringsWithTooltips
methods, except instead of using StringTooltip
, it simply uses Tooltip<S>
:
SafeSuggestions<T> tooltips(Tooltip<T>... suggestions);
+SafeSuggestions<T> tooltips(Function<SuggestionInfo, Tooltip<T>[]> suggestions);
+
+The Tooltip<S>
class represents a tooltip for a given object S
. For example, a tooltip for a LocationArgument
would be a Tooltip<Location>
and a tooltip for an EnchantmentArgument
would be a Tooltip<Enchantment>
.
Just like the StringTooltip
class, the Tooltip<S>
class provides the following static methods, which operate exactly the same as the ones in the StringTooltip
class:
Tooltip<S> none(S object);
+Tooltip<S> ofString(S object, String tooltip);
+Tooltip<S> ofMessage(S object, Message tooltip);
+Tooltip<S> ofBaseComponents(S object, BaseComponent... tooltip);
+Tooltip<S> ofAdventureComponent(S object, Component tooltip);
+
+Tooltip<S>[] arrayOf(Tooltip<S>... tooltips);
+
+The use of arrayOf
is heavily recommended as it provides the necessary type safety for Java code to ensure that the correct types are being passed to the tooltips
method.
Say we wanted to create a custom teleport command which suggestions a few key locations. In this example, we'll use the following command syntax:
+/warp <location>
+
+First, we'll declare our arguments. Here, we use a LocationArgument
and use the tooltips
method, with a parameter for the command sender, so we can get information about the world. We populate the suggestions with tooltips using Tooltip.ofString(Location, String)
and collate them together with Tooltip.arrayOf(Tooltip<Location>...)
:
List<Argument<?>> arguments = new ArrayList<>();
+arguments.add(new LocationArgument("location")
+ .replaceSafeSuggestions(SafeSuggestions.tooltips(info -> {
+ // We know the sender is a player if we use .executesPlayer()
+ Player player = (Player) info.sender();
+ return BukkitTooltip.arrayOf(
+ BukkitTooltip.ofString(player.getWorld().getSpawnLocation(), "World spawn"),
+ BukkitTooltip.ofString(player.getBedSpawnLocation(), "Your bed"),
+ BukkitTooltip.ofString(player.getTargetBlockExact(256).getLocation(), "Target block")
+ );
+ })));
+
+val arguments = listOf<Argument<*>>(
+ LocationArgument("location")
+ .replaceSafeSuggestions(SafeSuggestions.tooltips { info ->
+ // We know the sender is a player if we use .executesPlayer()
+ val player = info.sender() as Player
+ BukkitTooltip.arrayOf(
+ BukkitTooltip.ofString(player.world.spawnLocation, "World spawn"),
+ BukkitTooltip.ofString(player.bedSpawnLocation, "Your bed"),
+ BukkitTooltip.ofString(player.getTargetBlockExact(256)?.location, "Target block")
+ )
+ } )
+)
+
+In the arguments declaration, we've casted the command sender to a player. To ensure that the command sender is definitely a player, we'll use the executesPlayer
command execution method in our command declaration:
new CommandAPICommand("warp")
+ .withArguments(arguments)
+ .executesPlayer((player, args) -> {
+ player.teleport((Location) args.get("location"));
+ })
+ .register();
+
+CommandAPICommand("warp")
+ .withArguments(arguments)
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ player.teleport(args["location"] as Location)
+ })
+ .register()
+
+In addition to normal suggestions, safely-typed suggestions and suggestions with tooltips, the CommandAPI can support computing the list of suggestions to send to a player asynchronously. This allows you to perform suggestions independent of the main server thread, which is useful for slow operations, such as:
+The asynchronous methods for suggestions are similar in format to normal suggestions, except for two things: the name of the method ends with Async
, and the return type for the inner function is a CompletableFuture
:
ArgumentSuggestions stringsAsync(Function<SuggestionInfo, CompletableFuture<String[]>> suggestions);
+ArgumentSuggestions stringsWithTooltipsAsync(Function<SuggestionInfo, CompletableFuture<IStringTooltip[]>> suggestions);
+
+SafeSuggestions<T> suggestAsync(Function<SuggestionInfo, CompletableFuture<T[]>> suggestions);
+SafeSuggestions<T> tooltipsAsync(Function<SuggestionInfo, CompletableFuture<Tooltip<T>[]>> suggestions);
+
+The easiest way to create a CompleteableFuture
for asynchronous suggestions is to use Java's CompletableFuture.supplyAsync()
method. If you have a simple array of string suggestions (String[]
), these can be turned into a completable future (CompletableFuture<String[]>
) using this method:
new String[] { "dirt", "grass", "cobblestone", };
+
+$$\downarrow$$
+CompletableFuture.supplyAsync(() -> {
+ return new String[] { "dirt", "grass", "cobblestone", };
+});
+
+++Developer's Note:
+As with all asynchronous operations running in a Minecraft server, you should not try to access the Bukkit API within an asynchronous block. If you want to run code which accesses Bukkit's API while inside an asynchronous block, you can schedule a synchronous task using the Bukkit scheduler, for example:
++CompletableFuture.supplyAsync(() -> { + + Bukkit.getScheduler().scheduleAsyncDelayedTask(plugin, () -> + // Your code here + ); + + return new String[] { "dirt", "grass", "cobblestone", }; +}); +
Say you wanted to write a command to modify your plugin's config file. Since the config file is an external file, you ideally want to access the file in a separate thread to the main server thread to retain performance for players on the server. We have the following command syntax:
+/setconfig <key> <value>
+
+We make use of the ArgumentSuggestions.stringsAsync
method to provide asynchronous suggestions. In our completable future implementation, we access the keys from the plugin configuration.
new CommandAPICommand("setconfig")
+ .withArguments(new StringArgument("key").replaceSuggestions(ArgumentSuggestions.stringsAsync(info -> {
+ return CompletableFuture.supplyAsync(() -> {
+ return plugin.getConfig().getKeys(false).toArray(new String[0]);
+ });
+ })))
+ .withArguments(new TextArgument("value"))
+ .executes((sender, args) -> {
+ String key = (String) args.get("key");
+ String value = (String) args.get("value");
+ plugin.getConfig().set(key, value);
+ })
+ .register();
+
+CommandAPICommand("setconfig")
+ .withArguments(StringArgument("key").replaceSuggestions(ArgumentSuggestions.stringsAsync { _ ->
+ CompletableFuture.supplyAsync { plugin.config.getKeys(false).toTypedArray() }
+ } ))
+ .withArguments(TextArgument("value"))
+ .executes(CommandExecutor { _, args ->
+ val key = args["key"] as String
+ val value = args["value"] as String
+ plugin.config.set(key, value)
+ })
+ .register()
+
+For more information on argument casting types, see Argument Casting. This section is primarily about use cases for each argument and any implementation notes.
+Primitive arguments are arguments that represent Java primitive types, such as int
, float
, double
, boolean
and long
. These arguments are defined in their respective classes:
Primitive type | CommandAPI class |
---|---|
int | IntegerArgument |
float | FloatArgument |
double | DoubleArgument |
long | LongArgument |
boolean | BooleanArgument |
These arguments simply cast to their primitive type and don't need any extra work.
+The BooleanArgument
class represents the Boolean values true
and false
.
Say we want to create a plugin that lets you edit its own config.yml
file using a command. To do this, let's create a command with the following syntax:
/editconfig <config-key> <value>
+
+We first retrieve the keys from the configuration file using the typical Bukkit API. We construct our List
to hold our arguments, with the first parameter being a String key (in the form of a TextArgument
, overridden with an array of suggestions). Finally, we register our command and update the config, ensuring that we cast the BooleanArgument
to boolean
:
// Load keys from config file
+String[] configKeys = getConfig().getKeys(true).toArray(new String[0]);
+
+// Register our command
+new CommandAPICommand("editconfig")
+ .withArguments(new TextArgument("config-key").replaceSuggestions(ArgumentSuggestions.strings(info -> configKeys)))
+ .withArguments(new BooleanArgument("value"))
+ .executes((sender, args) -> {
+ // Update the config with the boolean argument
+ getConfig().set((String) args.get("config-key"), (boolean) args.get("value"));
+ })
+ .register();
+
+// Load keys from config file
+val configKeys: Array<String> = config.getKeys(true).toTypedArray()
+
+// Register our command
+CommandAPICommand("editconfig")
+ .withArguments(TextArgument("config-key").replaceSuggestions(ArgumentSuggestions.strings { _ -> configKeys }))
+ .withArguments(BooleanArgument("value"))
+ .executes(CommandExecutor { _, args ->
+ // Update the config with the boolean argument
+ config.set(args["config-key"] as String, args["value"] as Boolean)
+ })
+ .register()
+
+// Load keys from config file
+val configKeys: Array<String> = getConfig().getKeys(true).toTypedArray()
+
+// Register our command
+commandAPICommand("editconfig") {
+ argument(TextArgument("config-key").replaceSuggestions(ArgumentSuggestions.strings { configKeys }))
+ booleanArgument("value")
+ anyExecutor { _, args ->
+ // Update the config with the boolean argument
+ getConfig().set(args["config-key"] as String, args["value"] as Boolean)
+ }
+}
+
+Numbers are represented using the designated number classes:
+Class | Description |
---|---|
IntegerArgument | Whole numbers between Integer.MIN_VALUE and Integer.MAX_VALUE |
LongArgument | Whole numbers between Long.MIN_VALUE and Long.MAX_VALUE |
DoubleArgument | Double precision floating point numbers |
FloatArgument | Single precision floating point numbers |
Each numerical argument can have ranges applied to them, which restricts the user to only entering numbers from within a certain range. This is done using the constructor, and the range specified:
+Constructor | Description |
---|---|
new IntegerArgument() | Any range |
new IntegerArgument(min) | Values greater than or equal to min |
new IntegerArgument(min, max) | Values greater than or equal to min and less than or equal to max |
Each range is inclusive, so it includes the number given to it. If the minimum value provided is larger than the maximum value, an InvalidRangeException
is thrown.
Ranged arguments allow players to provide a range between two numbers, all within a single argument. The CommandAPI provides two ranged arguments, IntegerRangeArgument
for ranges with only integer values, and FloatRangeArgument
for ranged with potential floating point values.
These consist of values such as:
+Input | What it means |
---|---|
5 | The number 5 |
5..10 | Numbers between 5 and 10, including 5 and 10 |
5.. | Numbers greater than or equal to 5 (bounded by Java's max number size) |
..5 | Numbers less than or equal to 5 (bounded by Java's min number size) |
This allows you to let users define a range of values, which can be used to limit a value, such as the number of players in a region or for a random number generator.
+The CommandAPI returns an IntegerRange
from the IntegerRangeArgument
, and a FloatRange
from the FloatRangeArgument
, which represents the upper and lower bounds of the numbers provided by the command sender, as well as a method to check if a number is within that range.
The IntegerRange
class has the following methods:
class IntegerRange {
+ public int getLowerBound();
+ public int getUpperBound();
+ public boolean isInRange(int);
+}
+
+The FloatRange
class has the following methods:
class FloatRange {
+ public float getLowerBound();
+ public float getUpperBound();
+ public boolean isInRange(float);
+}
+
+Say you're working on a plugin for server administrators to help them find restricted items. A method of doing so would be to search chests in a given radius for certain items. As such, we can use the following syntax:
+/searchchests <range> <item>
+
+Now, we simply create our arguments using IntegerRangeArgument
for our range and ItemStackArgument
as the item to search for. We can then find all chests in a given area and determine if it is within the range provided by the command sender by using range.isInRange(distance)
:
new CommandAPICommand("searchrange")
+ .withArguments(new IntegerRangeArgument("range")) // Range argument
+ .withArguments(new ItemStackArgument("item")) // The item to search for
+ .executesPlayer((player, args) -> {
+ // Retrieve the range from the arguments
+ IntegerRange range = (IntegerRange) args.get("range");
+ ItemStack itemStack = (ItemStack) args.get("item");
+
+ // Store the locations of chests with certain items
+ List<Location> locations = new ArrayList<>();
+
+ // Iterate through all chunks, and then all tile entities within each chunk
+ for (Chunk chunk : player.getWorld().getLoadedChunks()) {
+ for (BlockState blockState : chunk.getTileEntities()) {
+
+ // The distance between the block and the player
+ int distance = (int) blockState.getLocation().distance(player.getLocation());
+
+ // Check if the distance is within the specified range
+ if (range.isInRange(distance)) {
+
+ // Check if the tile entity is a chest
+ if (blockState instanceof Chest chest) {
+
+ // Check if the chest contains the item specified by the player
+ if (chest.getInventory().contains(itemStack.getType())) {
+ locations.add(chest.getLocation());
+ }
+ }
+ }
+
+ }
+ }
+
+ // Output the locations of the chests, or whether no chests were found
+ if (locations.isEmpty()) {
+ player.sendMessage("No chests were found");
+ } else {
+ player.sendMessage("Found " + locations.size() + " chests:");
+ locations.forEach(location -> {
+ player.sendMessage(" Found at: "
+ + location.getX() + ", "
+ + location.getY() + ", "
+ + location.getZ());
+ });
+ }
+ })
+ .register();
+
+CommandAPICommand("searchrange")
+ .withArguments(IntegerRangeArgument("range")) // Range argument
+ .withArguments(ItemStackArgument("item")) // The item to search for
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ // Retrieve the range from the arguments
+ val range = args["range"] as IntegerRange
+ val itemStack = args["item"] as ItemStack
+
+ // Store the locations of chests with certain items
+ val locations = mutableListOf<Location>()
+
+ // Iterate through all chunks, and then all tile entities within each chunk
+ for (chunk in player.world.loadedChunks) {
+ for (blockState in chunk.tileEntities) {
+
+ // The distance between the block and the player
+ val distance = blockState.location.distance(player.location).toInt()
+
+ // Check if the distance is within the specified range
+ if (range.isInRange(distance)) {
+
+ // Check if the tile entity is a chest
+ if (blockState is Chest) {
+
+ // Check if the chest contains the item specified by the player
+ if (blockState.inventory.contains(itemStack.type)) {
+ locations.add(blockState.location)
+ }
+ }
+ }
+
+ }
+ }
+
+ // Output the locations of the chests, or whether no chests were found
+ if (locations.isEmpty()) {
+ player.sendMessage("No chests were found")
+ } else {
+ player.sendMessage("Found ${locations.size} chests:")
+ locations.forEach {
+ player.sendMessage(" Found at: ${it.x}, ${it.y}, ${it.z}")
+ }
+ }
+ })
+ .register()
+
+commandAPICommand("searchrange") {
+ integerRangeArgument("range") // Range argument
+ itemStackArgument("item") // The item to search for
+ playerExecutor { player, args ->
+ // Retrieve the range from the arguments
+ val range = args["range"] as IntegerRange
+ val itemStack = args["item"] as ItemStack
+
+ // Store the locations of chests with certain items
+ val locations = mutableListOf<Location>()
+
+ // Iterate through all chunks, and then all tile entities within each chunk
+ for (chunk in player.world.loadedChunks) {
+ for (blockState in chunk.tileEntities) {
+
+ // The distance between the block and the player
+ val distance = blockState.location.distance(player.location).toInt()
+
+ // Check if the distance is within the specified range
+ if (range.isInRange(distance)) {
+
+ // Check if the tile entity is a chest
+ if (blockState is Chest) {
+
+ // Check if the chest contains the item specified by the player
+ if (blockState.inventory.contains(itemStack.type)) {
+ locations.add(blockState.location)
+ }
+ }
+ }
+
+ }
+ }
+
+ // Output the locations of the chests, or whether no chests were found
+ if (locations.isEmpty()) {
+ player.sendMessage("No chests were found")
+ } else {
+ player.sendMessage("Found ${locations.size} chests:")
+ locations.forEach {
+ player.sendMessage(" Found at: ${it.x}, ${it.y}, ${it.z}")
+ }
+ }
+ }
+}
+
+There are three types of arguments that return Java's String
object. Each have their own unique set of features which make them suitable for specific needs.
The StringArgument
class is used to represent a single word. These words can only contain alphanumeric characters (A-Z, a-z and 0-9) and the underscore (_), plus (+), minus (-) and period (.) characters.
Accepted StringArgument
values:
Hello
+123
+hello123
+hello-123
+hello.WORLD
+Hello_world
+
+Rejected StringArgument
values:
hello@email.com
+yesn't
+
+The TextArgument
acts similar to any String in Java. These can be single words, like the StringArgument
, or have additional characters (e.g. spaces, symbols) if surrounded by quotes. To type quotation marks, you can use \"
(as similar to Java) to escape these special characters.
Accepted TextArgument
values:
hello
+"hello world!"
+"hello@gmail.com"
+"this has \" <<-- speech marks! "
+
+Rejected TextArgument
values:
hello world
+私
+"speech marks: ""
+
+++Greedy Arguments:
+The
+GreedyStringArgument
, similar to theChatArgument
uses the entire argument array from its current position. This means that it never ends, therefore if it is used, it must be the last element of yourList
of arguments.For example, if you have a command
+/message <message> <target>
, it would not be able to determine where the message ends and the<target>
argument begins.If a
+GreedyStringArgument
orChatArgument
is not declared at the end of theList
of arguments, or multiple of these arguments are used in the sameList
, the CommandAPI throws aGreedyArgumentException
.
The GreedyStringArgument
takes the TextArgument
a step further. Any characters and symbols are allowed and quotation marks are not required.
Say we have a simple message command of the following form:
+/message <target> <message>
+
+This would be ideal for a greedy string, since it can consume all text after the player's name:
+new CommandAPICommand("message")
+ .withArguments(new PlayerArgument("target"))
+ .withArguments(new GreedyStringArgument("message"))
+ .executes((sender, args) -> {
+ ((Player) args.get("target")).sendMessage((String) args.get("message"));
+ })
+ .register();
+
+CommandAPICommand("message")
+ .withArguments(PlayerArgument("target"))
+ .withArguments(GreedyStringArgument("message"))
+ .executes(CommandExecutor { _, args ->
+ (args["target"] as Player).sendMessage(args["message"] as String)
+ })
+ .register()
+
+commandAPICommand("message") {
+ playerArgument("target")
+ greedyStringArgument("message")
+ anyExecutor { _, args ->
+ (args["target"] as Player).sendMessage(args["message"] as String)
+ }
+}
+
+Any text entered after the <target>
argument would be sent to the player. For example, the command could be used as follows:
/message Skepter This is some incredibly long string with "symbols" and $p3c!aL characters~
+
+Note how this only works if the greedy string argument is at the end. If, say, the command was /message <message> <target>
, it would not be able to determine where the <message>
argument ends and the <target>
argument begins.
In the CommandAPI, there are two arguments used to represent location. The LocationArgument
argument, which represents a 3D location \( (x, y, z) \) and the Location2DArgument
, which represents 2D location \( (x, z) \).
The LocationArgument
class is used to specify a location in the command sender's current world, returning a Bukkit Location
object. It allows the user to enter three numbers as coordinates, or use relative coordinates (i.e. the ~
and ^
operators).
The LocationArgument
constructor requires a LocationType
, which specifies the type of location that is accepted by the command. The LocationType
enum consists of two values:
LocationType.BLOCK_POSITION
BLOCK_POSITION
refers to integer block coordinates. When in-game as a player, the suggested location is the coordinates of block you are looking at when you type the command.
LocationType.PRECISE_POSITION
PRECISE_PRECISION
uses exact coordinates, using the double
primitive type. When in-game as a player, the suggested location is the exact coordinates of where your cursor is pointing at when you type the command.
If no LocationType
is provided, the LocationArgument
will use PRECISE_POSITION
by default.
The LocationArgument
constructor can also accept a boolean centerPosition
. If set to true
, when using LocationType.PRECISE_POSITION
, if an integer is provided in the value, it will add 0.5 to the x and z coordinates to center the position within a block. If set to false
, the integer value will be provided as is.
If no centerPosition
parameter is provided, the LocationArgument
will use centerPosition = true
by default.
Say you use the following constructor, which sets centerPosition
to true
:
new LocationArgument("location", LocationType.PRECISE_POSITION, true);
+
+LocationArgument("location", LocationType.PRECISE_POSITION, true)
+
+Integer positions are centered
+Let's also say you use the following location using this location argument in a command:
+10 20 30
+
+The resulting location will be the following, which centers the position of the x and z coordinates. This does not change the y coordinate:
+10.5 20 30.5
+
+Non-integer positions remain as normal
+If you use the following location using this location argument in a command:
+10.2 20.2 30.2
+
+The resulting location will be the following, which does not change the x and z coordinates, because the positions are not integers:
+10.2 20.2 30.2
+
+Say you use the following constructor, which sets centerPosition
to false
:
new LocationArgument("location", LocationType.PRECISE_POSITION, false);
+
+LocationArgument("location", LocationType.PRECISE_POSITION, false)
+
+Integer positions are not centered
+Let's also say you use the following location using this location argument in a command:
+10 20 30
+
+The resulting location will be the following, which does not modify the position of the x and z coordinates:
+10 20 30
+
+We can declare a simple command to break a block:
+/break <location>
+
+Simply put, given the coordinates provided to the command, "break" the block by setting it's type to Material.AIR
. For this example, we're referring to block specific coordinates, so we want to use LocationType.BLOCK_POSITION
:
new CommandAPICommand("break")
+ // We want to target blocks in particular, so use BLOCK_POSITION
+ .withArguments(new LocationArgument("block", LocationType.BLOCK_POSITION))
+ .executesPlayer((player, args) -> {
+ Location location = (Location) args.get("block");
+ location.getBlock().setType(Material.AIR);
+ })
+ .register();
+
+CommandAPICommand("break")
+ // We want to target blocks in particular, so use BLOCK_POSITION
+ .withArguments(LocationArgument("block", LocationType.BLOCK_POSITION))
+ .executesPlayer(PlayerCommandExecutor { _, args ->
+ (args["block"] as Location).block.type = Material.AIR
+ })
+ .register()
+
+commandAPICommand("break") {
+ // We want to target blocks in particular, so use BLOCK_POSITION
+ locationArgument("block", LocationType.BLOCK_POSITION)
+ playerExecutor { _, args ->
+ (args["block"] as Location).block.type = Material.AIR
+ }
+}
+
+The Location2DArgument
is pretty much identical in use to the LocationArgument
for 3D coordinates, except instead of returning a Location
object, it instead returns a Location2D
object that extends Location
(thus, being compatible anywhere you would normally be able to use Location
).
The RotationArgument
allows users to specify a pair of pitch and yaw coordinates. By default (using the ~
symbol), this refers to the player's current pitch and yaw of where they are looking at.
The RotationArgument
class returns a Rotation
object, which consists of the following methods:
Method name | What it does |
---|---|
float getPitch() | Returns a player's pitch (up and down rotation) |
float getYaw() | Returns a player's yaw (left and right rotation) |
float getNormalizedPitch() | Returns a player's pitch between -90 and 90 degrees |
float getNormalizedYaw() | Returns a player's yaw between -180 and 180 degrees |
Say we want to make an armor stand look in a certain direction. To do this, we'll use the following command:
+/rotate <rotation> <target>
+
+To do this, we'll use the rotation from the RotationArgument
and select an entity using the EntitySelectorArgument.OneEntity
class. We then check if our entity is an armor stand and if so, we set its head pose to the given rotation.
new CommandAPICommand("rotate")
+ .withArguments(new RotationArgument("rotation"))
+ .withArguments(new EntitySelectorArgument.OneEntity("target"))
+ .executes((sender, args) -> {
+ Rotation rotation = (Rotation) args.get("rotation");
+ Entity target = (Entity) args.get("target");
+
+ if (target instanceof ArmorStand armorStand) {
+ armorStand.setHeadPose(new EulerAngle(Math.toRadians(rotation.getPitch()), Math.toRadians(rotation.getYaw() - 90), 0));
+ }
+ })
+ .register();
+
+CommandAPICommand("rotate")
+ .withArguments(RotationArgument("rotation"))
+ .withArguments(EntitySelectorArgument.OneEntity("target"))
+ .executes(CommandExecutor { _, args ->
+ val rotation = args["rotation"] as Rotation
+ val target = args["target"] as Entity
+
+ if (target is ArmorStand) {
+ target.headPose = EulerAngle(Math.toRadians(rotation.pitch.toDouble()), Math.toRadians(rotation.yaw.toDouble() - 90), 0.0)
+ }
+ })
+ .register()
+
+commandAPICommand("rotate") {
+ rotationArgument("rotation")
+ entitySelectorArgumentOneEntity("target")
+ anyExecutor { _, args ->
+ val rotation = args["rotation"] as Rotation
+ val target = args["target"] as Entity
+
+ if (target is ArmorStand) {
+ target.headPose = EulerAngle(Math.toRadians(rotation.pitch.toDouble()), Math.toRadians(rotation.yaw.toDouble() - 90), 0.0)
+ }
+ }
+}
+
+Note how the head pose requires an EulerAngle
as opposed to a pitch and yaw. To account for this, we convert our rotation (which is in degrees) into an EulerAngle
in radians.
The AxisArgument
class refers to the x, y and z axes. When used with the CommandAPI, it returns an EnumSet<Axis>
(You can view the documentation for EnumSet
here).
The CommandAPI provides a number of ways to interact with chat formatting in Minecraft. These are the following:
+@a
and @r
The CommandAPI implements ChatColor, Chat and ChatComponent in two separate ways: Spigot-compatible and Adventure-compatible. The differences between these and how to use them are described in their own relevant pages.
+The CommandAPI also supports Minecraft 1.19's chat preview feature. To use Minecraft 1.19's chat preview feature, information on that can be found in Chat preview.
+The ChatColorArgument
class is used to represent a given chat color (e.g. red or green). This argument returns the ChatColor
object.
Say we want to create a plugin to change the color of a player's username. We want to create a command of the following form:
+/namecolor <chatcolor>
+
+We then use the ChatColorArgument
to change the player's name color:
new CommandAPICommand("namecolor")
+ .withArguments(new ChatColorArgument("chatcolor"))
+ .executesPlayer((player, args) -> {
+ ChatColor color = (ChatColor) args.get("chatcolor");
+ player.setDisplayName(color + player.getName());
+ })
+ .register();
+
+CommandAPICommand("namecolor")
+ .withArguments(ChatColorArgument("chatColor"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val color = args["chatColor"] as ChatColor
+ player.setDisplayName("$color${player.name}")
+ })
+ .register()
+
+commandAPICommand("namecolor") {
+ chatColorArgument("chatcolor")
+ playerExecutor { player, args ->
+ val color = args["chatcolor"] as ChatColor
+ player.setDisplayName("$color${player.name}")
+ }
+}
+
+++Developer's Note:
+The two following classes,
+ChatComponentArgument
andChatArgument
depend on a Spigot based server. This means that these arguments will not work on a non-Spigot based server, such as CraftBukkit. If you use this class on a non-Spigot based server, it will throw aSpigotNotFoundException
The ChatComponentArgument
class accepts raw chat-based JSON as valid input. Despite being regular JSON, it must conform to the standard declared here, which consists of JSON that has a limited subset of specific keys (In other words, you can have a JSON object that has the key text
, but not one that has the key blah
).
This is converted into Spigot's BaseComponent[]
, which can be used for the following:
Broadcasting messages to all players on the server using:
+Bukkit.getServer().spigot().broadcast(BaseComponent[]);
+
+Adding and setting pages to books using BookMeta
:
BookMeta meta = // ...
+meta.spigot().setPages(BaseComponent[]);
+
+Sending messages to Player
objects:
Player player = // ...
+player.spigot().sendMessage(BaseComponent[]);
+
+Sending messages to CommandSender
objects:
CommandSender sender = // ...
+sender.spigot().sendMessage(BaseComponent[]);
+
+Say we want to generate a book using raw JSON. For this example, we'll use the following JSON (generated from minecraftjson.com) to generate our book:
+["", {
+ "text": "Once upon a time, there was a guy call "
+}, {
+ "text": "Skepter",
+ "color": "light_purple",
+ "hoverEvent": {
+ "action": "show_entity",
+ "value": "Skepter"
+ }
+}, {
+ "text": " and he created the "
+}, {
+ "text": "CommandAPI",
+ "underlined": true,
+ "clickEvent": {
+ "action": "open_url",
+ "value": "https://github.com/JorelAli/CommandAPI"
+ }
+}]
+
+Since we're writing a book, we must ensure that all quotes have been escaped. This can also be performed on the minecraftjson.com website by selecting "book":
+["[\"\",{\"text\":\"Once upon a time, there was a guy call \"},{\"text\":\"Skepter\",\"color\":\"light_purple\",\"hoverEvent\":{\"action\":\"show_entity\",\"value\":\"Skepter\"}},{\"text\":\" and he created the \"},{\"text\":\"CommandAPI\",\"underlined\":true,\"clickEvent\":{\"action\":\"open_url\",\"value\":\"https://github.com/JorelAli/CommandAPI\"}}]"]
+
+Now let's define our command. Since book text is typically very large - too large to be entered into a chat, we'll make a command block compatible command by providing a player parameter:
+/makebook <player> <contents>
+
+Now we can create our book command. We use the player as the main target by using their name for the author field, as well as their inventory to place the book. We finally construct our book using the .setPages(BaseComponent[])
method:
new CommandAPICommand("makebook")
+ .withArguments(new PlayerArgument("player"))
+ .withArguments(new ChatComponentArgument("contents"))
+ .executes((sender, args) -> {
+ Player player = (Player) args.get("player");
+ BaseComponent[] arr = (BaseComponent[]) args.get("contents");
+
+ // Create book
+ ItemStack is = new ItemStack(Material.WRITTEN_BOOK);
+ BookMeta meta = (BookMeta) is.getItemMeta();
+ meta.setTitle("Custom Book");
+ meta.setAuthor(player.getName());
+ meta.spigot().setPages(arr);
+ is.setItemMeta(meta);
+
+ // Give player the book
+ player.getInventory().addItem(is);
+ })
+ .register();
+
+CommandAPICommand("makebook")
+ .withArguments(PlayerArgument("player"))
+ .withArguments(ChatComponentArgument("contents"))
+ .executes(CommandExecutor { _, args ->
+ val player = args["player"] as Player
+ val arr = args["contents"] as Array<BaseComponent>
+
+ // Create book
+ val item = ItemStack(Material.WRITTEN_BOOK)
+ val meta = item.itemMeta as BookMeta
+ meta.title = "Custom Book"
+ meta.author = player.name
+ meta.spigot().setPages(arr)
+ item.itemMeta = meta
+
+ // Give player the book
+ player.inventory.addItem(item)
+ })
+ .register()
+
+commandAPICommand("makebook") {
+ playerArgument("player")
+ chatComponentArgument("contents")
+ anyExecutor { _, args ->
+ val player = args["player"] as Player
+ val array = args["contents"] as Array<BaseComponent>
+
+ // Create book
+ val item = ItemStack(Material.WRITTEN_BOOK)
+ val meta = item.itemMeta as BookMeta
+ meta.title = "Custom Book"
+ meta.author = player.name
+ meta.spigot().setPages(array)
+ item.itemMeta = meta
+
+ // Give player the book
+ player.inventory.addItem(item)
+ }
+}
+
+++Note:
+The
+ChatArgument
class is an argument similar to theGreedyStringArgument
, in the sense that it has no terminator and must be defined at the end of yourList
of arguments. For more information on this, please read the section on Greedy arguments.
The ChatArgument
is identical to the GreedyStringArgument
, with the added functionality of enabling entity selectors, such as @e
, @p
and so on. The ChatArgument
also returns a BaseComponent[]
, similar to the ChatComponentArgument
.
Say we wanted to broadcast a "personalized" message to players on the server. By "personalized", we mean a command which changes its output depending on who we are sending the output to. Simply put, we want a command of the following syntax:
+/pbroadcast <message>
+
+Say we're on a server with 2 players: Bob and Michael. If I were to use the following command:
+/pbroadcast Hello @p
+
+Bob would receive the message "Hello Bob", whereas Michael would receive the message "Hello Michael". We can use the ChatArgument
to create this "personalized" broadcast:
new CommandAPICommand("pbroadcast")
+ .withArguments(new ChatArgument("message"))
+ .executes((sender, args) -> {
+ BaseComponent[] message = (BaseComponent[]) args.get("message");
+
+ // Broadcast the message to everyone on the server
+ Bukkit.getServer().spigot().broadcast(message);
+ })
+ .register();
+
+CommandAPICommand("pbroadcast")
+ .withArguments(ChatArgument("message"))
+ .executes(CommandExecutor { _, args ->
+ val message = args["message"] as Array<BaseComponent>
+
+ // Broadcast the message to everyone on the server
+ Bukkit.getServer().spigot().broadcast(*message)
+ })
+ .register()
+
+commandAPICommand("pbroadcast") {
+ chatArgument("message")
+ anyExecutor { _, args ->
+ val message = args["message"] as Array<BaseComponent>
+
+ // Broadcast the message to everyone on the server
+ Bukkit.getServer().spigot().broadcast(*message)
+ }
+}
+
+++Developer's Note:
+The two following classes,
+AdventureChatComponentArgument
andAdventureChatArgument
depend on a Paper based server which has the Adventure library. If you use this class on a server without the Adventure library, it will throw aPaperAdventureNotFoundException
From Paper 1.16.5 build #473 onwards, Paper now includes Kyori's Adventure API. This library is a replacement of the BungeeCord chat API and has all of the same functionality as the BungeeCord chat API (and more!). The documentation for this API can be found here.
+Since this functions very similar to the Spigot chat arguments, this page won't reiterate everything about how it works, we'll just outline some examples of how to use these arguments instead.
+The AdventureChatColorArgument
class is used to represent a given chat color (e.g. red or green). This argument returns the NamedTextColor
object. If reset
is passed to this argument, this will return NamedTextColor.WHITE
.
Say we want to create a plugin to change the color of a player's username. We want to create a command of the following form:
+/namecolor <chatcolor>
+
+We then use the ChatColorArgument
to change the player's name color:
new CommandAPICommand("namecolor")
+ .withArguments(new AdventureChatColorArgument("chatcolor"))
+ .executesPlayer((player, args) -> {
+ NamedTextColor color = (NamedTextColor) args.get("chatcolor");
+ player.displayName(Component.text().color(color).append(Component.text(player.getName())).build());
+ })
+ .register();
+
+CommandAPICommand("namecolor")
+ .withArguments(AdventureChatColorArgument("chatcolor"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val color = args["chatcolor"] as NamedTextColor
+ player.displayName(Component.text().color(color).append(Component.text(player.name)).build())
+ })
+ .register()
+
+commandAPICommand("namecolor") {
+ chatColorArgument("chatcolor")
+ playerExecutor { player, args ->
+ val color = args["chatcolor"] as NamedTextColor
+ player.displayName(Component.text().color(color).append(Component.text(player.name)).build())
+ }
+}
+
+The AdventureChatComponentArgument
class accepts raw chat-based JSON as valid input, as declared here. This is converted into Adventure's Component
class.
In this example, we'll create a simple command which lets you show a book to a user. The syntax for our command is as follows:
+/showbook <target> <title> <author> <contents>
+
+We can construct a book using the Adventure API's Book.book(Component, Component, Component...)
method. In order to convert our strings into Component
objects, we use the Component.text(String)
method. Since Paper supports the Adventure API natively, we can then send this book to a player using the openBook(Book)
method:
new CommandAPICommand("showbook")
+ .withArguments(new PlayerArgument("target"))
+ .withArguments(new TextArgument("title"))
+ .withArguments(new StringArgument("author"))
+ .withArguments(new AdventureChatComponentArgument("contents"))
+ .executes((sender, args) -> {
+ Player target = (Player) args.get("target");
+ String title = (String) args.get("title");
+ String author = (String) args.get("author");
+ Component content = (Component) args.get("contents");
+
+ // Create a book and show it to the user (Requires Paper)
+ Book mybook = Book.book(Component.text(title), Component.text(author), content);
+ target.openBook(mybook);
+ })
+ .register();
+
+CommandAPICommand("showbook")
+ .withArguments(PlayerArgument("target"))
+ .withArguments(TextArgument("title"))
+ .withArguments(StringArgument("author"))
+ .withArguments(AdventureChatComponentArgument("contents"))
+ .executes(CommandExecutor { _, args ->
+ val target = args["target"] as Player
+ val title = args["title"] as String
+ val author = args["author"] as String
+ val content = args["contents"] as Component
+
+ // Create a book and show it to the user (Requires Paper)
+ val mybook = Book.book(Component.text(title), Component.text(author), content)
+ target.openBook(mybook)
+ })
+ .register()
+
+commandAPICommand("showbook") {
+ playerArgument("target")
+ textArgument("title")
+ stringArgument("author")
+ adventureChatComponentArgument("contents")
+ anyExecutor { _, args ->
+ val target = args["target"] as Player
+ val title = args["title"] as String
+ val author = args["author"] as String
+ val content = args["contents"] as Component
+
+ // Create a book and show it to the user (Requires Paper)
+ val mybook = Book.book(Component.text(title), Component.text(author), content)
+ target.openBook(mybook)
+ }
+}
+
+The AdventureChatArgument
class is the equivalent Adventure API class for the ChatArgument
- it represents infinitely long strings similar to the GreedyStringArgument
and allows entity selectors such as @e
, @p
and so on. The AdventureChatArgument
returns a Component
, similar to the AdventureChatComponentArgument
.
We'll take the same example from the ChatArgument
class, but using the AdventureChatArgument
instead - We want to create a personalized message broadcasted to all users using a chat component that allows entity selectors. For this command, we want the following syntax:
/pbroadcast <message>
+
+In order to broadcast an Adventure Component
to all players on the server, we have to use Paper's broadcast(Component, String)
method. This method requires a permission node which all players must have in order to receive the broadcasted message. By default, Bukkit-based servers (Spigot and Paper) use the bukkit.broadcast.user
permission, which is described here:
new CommandAPICommand("pbroadcast")
+ .withArguments(new AdventureChatArgument("message"))
+ .executes((sender, args) -> {
+ Component message = (Component) args.get("message");
+
+ // Broadcast the message to everyone with broadcast permissions.
+ Bukkit.getServer().broadcast(message, Server.BROADCAST_CHANNEL_USERS);
+ Bukkit.getServer().broadcast(message);
+ })
+ .register();
+
+CommandAPICommand("pbroadcast")
+ .withArguments(AdventureChatArgument("message"))
+ .executes(CommandExecutor { _, args ->
+ val message = args["message"] as Component
+
+ // Broadcast the message to everyone with broadcast permissions.
+ Bukkit.getServer().broadcast(message, Server.BROADCAST_CHANNEL_USERS)
+ Bukkit.getServer().broadcast(message)
+ })
+ .register()
+
+commandAPICommand("pbroadcast") {
+ adventureChatArgument("message")
+ anyExecutor { _, args ->
+ val message = args["message"] as Component
+
+ // Broadcast the message to everyone with broadcast permissions.
+ Bukkit.getServer().broadcast(message, Server.BROADCAST_CHANNEL_USERS)
+ Bukkit.getServer().broadcast(message)
+ }
+}
+
+Chat preview is a feature introduced in Minecraft 1.19 that allows the server to display a preview of a chat message to the client before the client sends their message to the server. This chat preview feature is also compatible with /say
and /msg
, as well as the ChatArgument
and AdventureChatArgument
classes.
The chat preview feature is only present in Minecraft versions 1.19, 1.19.1 and 1.19.2. Chat preview was removed in 1.19.3, so this feature is unfortunately no longer usable in Minecraft 1.19.3 and beyond.
+To use chat preview, your server must have previews-chat
set to true
in the server.properties
file:
...
+previews-chat=true
+...
+
+For players that want to use chat preview, they must have Chat Preview
enabled in Options > Chat Settings...
The ChatArgument
and AdventureChatArgument
classes include a method, withPreview
:
public T withPreview(PreviewableFunction preview);
+
+The method withPreview(PreviewableFunction preview)
lets you generate a preview to send to the client. This method takes in the PreviewableFunction
functional interface, which is a function that takes in a PreviewInfo
and returns either a BaseComponent[]
(for ChatArgument
) or a Component
(for AdventureChatArgument
):
public T generatePreview(PreviewInfo info) throws WrapperCommandSyntaxException;
+
+The PreviewInfo
class is a record containing the following:
public record PreviewInfo<T> {
+ Player player();
+ String input();
+ String fullInput();
+ T parsedInput();
+}
+
+The following methods are as follows:
+Player player();
+
+player()
is the player that is currently typing a chat preview.
String input();
+
+input()
is the current input for the current ChatArgument
or AdventureChatArgument
. If a user is typing /mycommand hellowor¦
and the command syntax is /mycommand <ChatArgument>
, the result of input()
would be "hellowor"
.
String fullInput();
+
+fullInput()
is the full input that the player has typed, including the leading /
symbol which is required to start a command. If a user is typing /mycommand hellowor¦
, the result of fullInput()
would be "/mycommand hellowor"
.
T parsedInput();
+
+parsedInput()
is similar to input()
, except it has been parsed by the CommandAPI's argument parser. This is a representation of what the argument in the executor would look like. For a ChatArgument
the return type is BaseComponent[]
, and for AdventureChatArgument
the return type is Component
.
The ChatArgument
and AdventureChatArgument
classes also include a method, usePreview
:
public T usePreview(boolean usePreview);
+
+The usePreview(boolean usePreview)
method lets you specify whether you would like the previewing function to be used as the argument's value during execution. If set to true
, when the command's .executes()
method is called, the argument value (e.g. arg[0]
) will be the same as the content generated by the function provided to withPreview()
.
Say we wanted to make our own /broadcast
command that allowed the user to use &
chat colors. We can use chat preview to show users what the result of their /broadcast
command would look like before running the command. We'll use the following command syntax:
/broadcast <message>
+
+Because the ChatArgument
and AdventureChatArgument
can support entity selectors (such as @p
), it's best to use the info.parsedInput()
method to handle parsed entity selectors. In our code, we use the .withPreview()
method and take the parsed input and convert it to plain text. We then convert the plain text with &
characters into component text to be displayed to the user.
For execution, we do the same procedure, because the text that the user enters still has &
characters that need to be converted into a component.
new CommandAPICommand("broadcast")
+ .withArguments(new ChatArgument("message").withPreview(info -> {
+ // Convert parsed BaseComponent[] to plain text
+ String plainText = BaseComponent.toPlainText(info.parsedInput());
+
+ // Translate the & in plain text and generate a new BaseComponent[]
+ return TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', plainText));
+ }))
+ .executesPlayer((player, args) -> {
+ // The user still entered legacy text. We need to properly convert this
+ // to a BaseComponent[] by converting to plain text then to BaseComponent[]
+ String plainText = BaseComponent.toPlainText((BaseComponent[]) args.get("message"));
+ Bukkit.spigot().broadcast(TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', plainText)));
+ })
+ .register();
+
+new CommandAPICommand("broadcast")
+ .withArguments(new AdventureChatArgument("message").withPreview(info -> {
+ // Convert parsed Component to plain text
+ String plainText = PlainTextComponentSerializer.plainText().serialize(info.parsedInput());
+
+ // Translate the & in plain text and generate a new Component
+ return LegacyComponentSerializer.legacyAmpersand().deserialize(plainText);
+ }))
+ .executesPlayer((player, args) -> {
+ // The user still entered legacy text. We need to properly convert this
+ // to a Component by converting to plain text then to Component
+ String plainText = PlainTextComponentSerializer.plainText().serialize((Component) args.get("broadcast"));
+ Bukkit.broadcast(LegacyComponentSerializer.legacyAmpersand().deserialize(plainText));
+ })
+ .register();
+
+CommandAPICommand("broadcast")
+ .withArguments(ChatArgument("message").withPreview { info ->
+ // Convert parsed BaseComponent[] to plain text
+ val plainText: String = BaseComponent.toPlainText(*info.parsedInput() as Array<BaseComponent>)
+
+ // Translate the & in plain text and generate a new BaseComponent[]
+ TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', plainText))
+ } )
+ .executesPlayer(PlayerCommandExecutor { _, args ->
+ // The user still entered legacy text. We need to properly convert this
+ // to a BaseComponent[] by converting to plain text then to BaseComponent[]
+ val plainText: String = BaseComponent.toPlainText(*args["message"] as Array<BaseComponent>)
+ val baseComponents: Array<BaseComponent> = TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', plainText))
+ Bukkit.spigot().broadcast(*baseComponents)
+ })
+ .register()
+
+CommandAPICommand("broadcast")
+ .withArguments(AdventureChatArgument("message").withPreview { info ->
+ // Convert parsed Component to plain text
+ val plainText: String = PlainTextComponentSerializer.plainText().serialize(info.parsedInput() as Component)
+
+ // Translate the & in plain text and generate a new Component
+ LegacyComponentSerializer.legacyAmpersand().deserialize(plainText)
+ } )
+ .executesPlayer(PlayerCommandExecutor { _, args ->
+ // The user still entered legacy text. We need to properly convert this
+ // to a Component by converting to plain text then to Component
+ val plainText: String = PlainTextComponentSerializer.plainText().serialize(args["message"] as Component)
+ Bukkit.broadcast(LegacyComponentSerializer.legacyAmpersand().deserialize(plainText))
+ })
+ .register()
+
+usePreview()
Extending on the example above where we created a /broadcast
command with chat preview support, we can simplify the code by using .usePreview(true)
to use the preview function as the value of our argument in our executor function. We'll use the same command syntax as the previous example:
/broadcast <message>
+
+By using .usePreview(true)
, we don't have to re-translate &
formatting codes into their corresponding components because that has already been done by the preview function specified in .withPreview()
method.
new CommandAPICommand("broadcast")
+ .withArguments(new ChatArgument("message").usePreview(true).withPreview(info -> {
+ // Convert parsed BaseComponent[] to plain text
+ String plainText = BaseComponent.toPlainText(info.parsedInput());
+
+ // Translate the & in plain text and generate a new BaseComponent[]
+ return TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', plainText));
+ }))
+ .executesPlayer((player, args) -> {
+ Bukkit.spigot().broadcast((BaseComponent[]) args.get("message"));
+ })
+ .register();
+
+new CommandAPICommand("broadcast")
+ .withArguments(new AdventureChatArgument("message").usePreview(true).withPreview(info -> {
+ // Convert parsed Component to plain text
+ String plainText = PlainTextComponentSerializer.plainText().serialize(info.parsedInput());
+
+ // Translate the & in plain text and generate a new Component
+ return LegacyComponentSerializer.legacyAmpersand().deserialize(plainText);
+ }))
+ .executesPlayer((player, args) -> {
+ Bukkit.broadcast((Component) args.get("message"));
+ })
+ .register();
+
+CommandAPICommand("broadcast")
+ .withArguments(ChatArgument("message").usePreview(true).withPreview { info ->
+ // Convert parsed BaseComponent[] to plain text
+ val plainText = BaseComponent.toPlainText(*info.parsedInput() as Array<BaseComponent>)
+
+ // Translate the & in plain text and generate a new BaseComponent[]
+ TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', plainText))
+ } )
+ .executesPlayer(PlayerCommandExecutor { _, args ->
+ Bukkit.spigot().broadcast(*args["message"] as Array<BaseComponent>)
+ })
+ .register()
+
+CommandAPICommand("broadcast")
+ .withArguments(AdventureChatArgument("message").usePreview(true).withPreview { info ->
+ // Convert parsed Component to plain text
+ val plainText = PlainTextComponentSerializer.plainText().serialize(info.parsedInput() as Component)
+
+ // Translate the & in plain text and generate a new Component
+ LegacyComponentSerializer.legacyAmpersand().deserialize(plainText)
+ } )
+ .executesPlayer(PlayerCommandExecutor { _, args ->
+ Bukkit.broadcast(args["message"] as Component)
+ })
+ .register()
+
+Minecraft's target selectors (e.g. @a
or @e
) are implemented using the subclasses of the EntitySelectorArgument
class. This allows you to select specific entities based on certain attributes.
There are four EntitySelectorArgument
subclasses that determine what type of data to return:
EntitySelectorArgument.OneEntity
- A single entity, which returns a Entity
object.EntitySelectorArgument.ManyEntities
- A collection of many entities, which returns a Collection<Entity>
object.EntitySelectorArgument.OnePlayer
- A single player, which returns a Player
object.EntitySelectorArgument.ManyPlayers
- A collection of players, which returns a Collection<Player>
object.The return type is the type to be cast when retrieved from the CommandArguments args
in the command declaration.
Say we want a command to remove certain types of entities. Typically, this would be implemented using a simple command like:
+/remove <player>
+/remove <mob type>
+/remove <radius>
+
+Instead, we can combine all of these into one by using the EntitySelectorArgument
. We want to be able to target multiple entities at a time, so we want to use the EntitySelectorArgument.ManyEntities
constructor. We can simply retrieve the Collection<Entity>
from this argument and iteratively remove each entity:
new CommandAPICommand("remove")
+ // Using a collective entity selector to select multiple entities
+ .withArguments(new EntitySelectorArgument.ManyEntities("entities"))
+ .executes((sender, args) -> {
+ // Parse the argument as a collection of entities (as stated above in the documentation)
+ @SuppressWarnings("unchecked")
+ Collection<Entity> entities = (Collection<Entity>) args.get("entities");
+
+ sender.sendMessage("Removed " + entities.size() + " entities");
+ for (Entity e : entities) {
+ e.remove();
+ }
+ })
+ .register();
+
+CommandAPICommand("remove")
+ // Using a collective entity selector to select multiple entities
+ .withArguments(EntitySelectorArgument.ManyEntities("entities"))
+ .executes(CommandExecutor { sender, args ->
+ // Parse the argument as a collection of entities (as stated above in the documentation)
+ val entities = args["entities"] as Collection<Entity>
+
+ sender.sendMessage("Removed ${entities.size} entities")
+ for (e in entities) {
+ e.remove()
+ }
+ })
+ .register()
+
+commandAPICommand("remove") {
+ // Using a collective entity selector to select multiple entities
+ entitySelectorArgumentManyEntities("entities")
+ anyExecutor { sender, args ->
+ // Parse the argument as a collection of entities (as stated above in the documentation)
+ val entities = args["entities"] as Collection<Entity>
+
+ sender.sendMessage("Removed ${entities.size} entities")
+ for (e in entities) {
+ e.remove()
+ }
+ }
+}
+
+We could then use this to target specific entities, for example:
+To remove all cows:
+/remove @e[type=cow]
+
+To remove the 10 furthest pigs from the command sender:
+/remove @e[type=pig,limit=10,sort=furthest]
+
+The PlayerArgument
class is very similar (almost identical) to EntitySelectorArgument.OnePlayer
. It returns a Player
object and requires the player to be online.
++Developer's Note:
+The
+PlayerArgument
internally uses theGameProfile
class from Mojang's authlib, which means that this argument has a slight performance overhead compared to usingEntitySelectorArgument.OnePlayer
When registering a PlayerArgument
you might notice that it includes Entity Selectors
(@a
, @e
, @r
, etc.). If you want to avoid those, you can use argument suggestions to only suggest the player names. For this example, let us create a /warp command:
/warp <player>
+
+To get a PlayerArgument
which only suggests the actual names, we can define it like this:
Argument<?> noSelectorSuggestions = new PlayerArgument("target")
+ .replaceSafeSuggestions(SafeSuggestions.suggest(info ->
+ Bukkit.getOnlinePlayers().toArray(new Player[0])
+ ));
+
+val noSelectorSuggestions = PlayerArgument("target")
+ .replaceSafeSuggestions(SafeSuggestions.suggest {
+ Bukkit.getOnlinePlayers().toTypedArray()
+ })
+
+Now we can define the rest of the command and include our suggestion inside of it like this:
+new CommandAPICommand("warp")
+ .withArguments(noSelectorSuggestions)
+ .executesPlayer((player, args) -> {
+ Player target = (Player) args.get("target");
+ player.teleport(target);
+ })
+ .register();
+
+CommandAPICommand("warp")
+ .withArguments(noSelectorSuggestions)
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val target = args["target"] as Player
+ player.teleport(target)
+ })
+ .register()
+
+And there we have it! One thing to note is that entity selectors are still a valid input, they are just not included in the suggestions. +
+The OfflinePlayerArgument
class is identical to the PlayerArgument
class, but instead of returning a Player
object, it returns an OfflinePlayer
object. Internally, this argument makes calls to Mojang servers (via Mojang's authlib), meaning it can be slightly slower than alternative methods (such as using a StringArgument
and suggesting a list of existing offline players).
The OfflinePlayerArgument
should be able to retrieve players that have never joined the server before.
The EntityTypeArgument
class is used to retrieve a type of entity as defined in the EntityType
enum. In other words, this is an entity type, for example a pig or a zombie.
Say we want a command to spawn a specific type of entity, similar to the /summon
command in Vanilla Minecraft, with the addition of specifying how many entities to spawn. We want to create a command of the following form:
/spawnmob <entity> <amount>
+
+Since we're trying to specify an entity type, we will use the EntityTypeArgument
as our argument type for <entity>
. We combine this with the IntegerArgument
class with a specified range of \( 1 \le \textit{amount} \le 100 \):
new CommandAPICommand("spawnmob")
+ .withArguments(new EntityTypeArgument("entity"))
+ .withArguments(new IntegerArgument("amount", 1, 100)) // Prevent spawning too many entities
+ .executesPlayer((Player player, CommandArguments args) -> {
+ for (int i = 0; i < (int) args.get("amount"); i++) {
+ player.getWorld().spawnEntity(player.getLocation(), (EntityType) args.get("entity"));
+ }
+ })
+ .register();
+
+CommandAPICommand("spawnmob")
+ .withArguments(EntityTypeArgument("entity"))
+ .withArguments(IntegerArgument("amount", 1, 100)) // Prevent spawning too many entities
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ for (i in 0 until args["amount"] as Int) {
+ player.world.spawnEntity(player.location, args["entity"] as EntityType)
+ }
+ })
+ .register()
+
+commandAPICommand("spawnmob") {
+ entityTypeArgument("entity")
+ integerArgument("amount", 1, 100) // Prevent spawning too many entities
+ playerExecutor { player, args ->
+ for (i in 0 until args["amount"] as Int) {
+ player.world.spawnEntity(player.location, args["entity"] as EntityType)
+ }
+ }
+}
+
+Note how in this example above, we have to explicitly state Player player, CommandArguments args
. This is due to a limitation of Java's type inference system which is discussed here.
The scoreboard arguments that the CommandAPI provides allows you to interact with various scoreboard elements, such as objectives, teams and score holders.
+The CommandAPI uses two classes to provide information about a scoreboard:
+ScoreHolderArgument
class represents score holder - a player's name or an entity's UUID that has scores in an objective. This is described in more detail on the Minecraft Wiki.ScoreboardSlotArgument
class represents a display slot (sidebar, list or belowName) as well as the team color if the display is the sidebar. This is described in more detail on the Minecraft Wiki.The score holder argument can accept either a single entity or a collection of multiple entities. In order to specify which one to use, you must use the ScoreHolderArgument.Single
or ScoreHolderArgument.Multiple
constructor respectively:
new ScoreHolderArgument.Single(nodeName);
+new ScoreHolderArgument.Multiple(nodeName);
+
+Depending on which constructor is used, the cast type changes. If you use ScoreHolderArgument.Single
, the argument must be casted to a String
. Otherwise, if you use ScoreHolderArgument.Multiple
, the argument must be casted to a Collection<String>
.
Say we want to reward all players that fit a certain criteria. We want a command with the following syntax:
+/reward <players>
+
+Since we could have multiple players that fit a certain criterion, we want to use ScoreHolderArgument.Multiple
constructor.
To give this example a bit more context, let's say we want to reward all players that have died less than 10 times in the server. To do this, we will use the following command:
+/reward @e[type=player,scores={deaths=..9}]
+
+Note how we use ..9
to represent 9 or less deaths (since ranges are inclusive). Also note how we restrict our input to players via the command using type=player
. We can now implement our command:
new CommandAPICommand("reward")
+ // We want multiple players, so we use ScoreHolderType.MULTIPLE in the constructor
+ .withArguments(new ScoreHolderArgument.Multiple("players"))
+ .executes((sender, args) -> {
+ // Get player names by casting to Collection<String>
+ @SuppressWarnings("unchecked")
+ Collection<String> players = (Collection<String>) args.get("players");
+
+ for (String playerName : players) {
+ Bukkit.getPlayer(playerName).getInventory().addItem(new ItemStack(Material.DIAMOND, 3));
+ }
+ })
+ .register();
+
+CommandAPICommand("reward")
+ // We want multiple players, so we use the ScoreHolderArgument.Multiple constructor
+ .withArguments(ScoreHolderArgument.Multiple("players"))
+ .executes(CommandExecutor { _, args ->
+ // Get player names by casting to Collection<String>
+ val players = args["players"] as Collection<String>
+
+ for (playerName in players) {
+ Bukkit.getPlayer(playerName)?.inventory!!.addItem(ItemStack(Material.DIAMOND, 3))
+ }
+ })
+ .register()
+
+commandAPICommand("reward") {
+ // We want multiple players, so we use the scoreHolderArgumentMultiple method
+ scoreHolderArgumentMultiple("player")
+ anyExecutor { _, args ->
+ // Get player names by casting to Collection<String>
+ val players = args["player"] as Collection<String>
+
+ for (playerName in players) {
+ Bukkit.getPlayer(playerName)?.inventory!!.addItem(ItemStack(Material.DIAMOND, 3))
+ }
+ }
+}
+
+++Developer's Note:
+In the example above, we have our user use the
+@e[type=player]
entity selector to restrict theCollection<String>
so it only returns player names, which allows us to useBukkit.getPlayer(playerName)
. In practice, we cannot guarantee that such a selector will be used, so we could update the code to accept both entities and players. For example, we can differentiate between players and entities by using theUUID.fromString(String)
method:+Collection<String> entitiesAndPlayers = (Collection<String>) args.get(0); +for(String str : entitiesAndPlayers) { + try { + UUID uuid = UUID.fromString(str); + // Is a UUID, so it must by an entity + Bukkit.getEntity(uuid); + } catch(IllegalArgumentException exception) { + // Not a UUID, so it must be a player name + Bukkit.getPlayer(str); + } +} +
The ScoreboardSlotArgument
represents where scoreboard information is displayed. Since the Bukkit scoreboard DisplaySlot
is not able to represent the case where team colors are provided, the CommandAPI uses the ScoreboardSlot
wrapper class as the representation of the ScoreboardSlotArgument
.
ScoreboardSlot
wrapperThe ScoreboardSlot
wrapper class has 3 main methods:
enum ScoreboardSlot {
+
+ PLAYER_LIST,
+ SIDEBAR, // Unused, use the other SIDEBAR_TEAM_### values below
+ BELOW_NAME,
+ SIDEBAR_TEAM_BLACK,
+ SIDEBAR_TEAM_DARK_BLUE,
+ SIDEBAR_TEAM_DARK_GREEN,
+ SIDEBAR_TEAM_DARK_AQUA,
+ SIDEBAR_TEAM_DARK_RED,
+ SIDEBAR_TEAM_DARK_PURPLE,
+ SIDEBAR_TEAM_GOLD,
+ SIDEBAR_TEAM_GRAY,
+ SIDEBAR_TEAM_DARK_GRAY,
+ SIDEBAR_TEAM_BLUE,
+ SIDEBAR_TEAM_GREEN,
+ SIDEBAR_TEAM_AQUA,
+ SIDEBAR_TEAM_RED,
+ SIDEBAR_TEAM_LIGHT_PURPLE,
+ SIDEBAR_TEAM_YELLOW,
+ SIDEBAR_TEAM_WHITE;
+
+ public DisplaySlot getDisplaySlot();
+ public ChatColor getTeamColor();
+ public boolean hasTeamColor();
+
+ public NamespacedKey getKey();
+}
+
+The getDisplaySlot()
method returns the display slot that was chosen. Sidebar scoreboard colors can be accessed via ScoreboardSlot.SIDEBAR_TEAM_###
. You can also retrieve the color using the getTeamColor()
method.
Say we want to clear all objectives in a specific scoreboard slot. In this example, we will use the main server scoreboard, which is accessed using Bukkit.getScoreboardManager.getMainScoreboard()
. We want a command with the following syntax:
/clearobjectives <slot>
+
+We implement this simply by using the ScoreboardSlotArgument
as our argument, and then we can clear the slot using the scoreboard clearSlot(DisplaySlot)
method.
new CommandAPICommand("clearobjectives")
+ .withArguments(new ScoreboardSlotArgument("slot"))
+ .executes((sender, args) -> {
+ Scoreboard scoreboard = Bukkit.getScoreboardManager().getMainScoreboard();
+ DisplaySlot slot = ((ScoreboardSlot) args.get("slot")).getDisplaySlot();
+ scoreboard.clearSlot(slot);
+ })
+ .register();
+
+CommandAPICommand("clearobjectives")
+ .withArguments(ScoreboardSlotArgument("slot"))
+ .executes(CommandExecutor { _, args ->
+ val scoreboard = Bukkit.getScoreboardManager().mainScoreboard
+ val slot = (args["slot"] as ScoreboardSlot).displaySlot
+ scoreboard.clearSlot(slot)
+ })
+ .register()
+
+commandAPICommand("clearobjectives") {
+ scoreboardSlotArgument("slot")
+ anyExecutor { _, args ->
+ val scoreboard = Bukkit.getScoreboardManager().mainScoreboard
+ val slot = (args["slot"] as ScoreboardSlot).displaySlot
+ scoreboard.clearSlot(slot)
+ }
+}
+
+In the CommandAPI, objectives are split into two classes:
+ObjectiveArgument
class, which represents objectives as a wholeObjectiveCriteriaArgument
class, which represents objective criteriaThe objective argument refers to a single scoreboard objective.
+As an example, let's create a command to move an objective to a player's sidebar. To do this, we will use the following command syntax:
+/sidebar <objective>
+
+new CommandAPICommand("sidebar")
+ .withArguments(new ObjectiveArgument("objective"))
+ .executes((sender, args) -> {
+ Objective objective = (Objective) args.get("objective");
+
+ // Set display slot
+ objective.setDisplaySlot(DisplaySlot.SIDEBAR);
+ })
+ .register();
+
+CommandAPICommand("sidebar")
+ .withArguments(ObjectiveArgument("objective"))
+ .executes(CommandExecutor { _, args ->
+ val objective = args["objective"] as Objective
+
+ // Set display slot
+ objective.displaySlot = DisplaySlot.SIDEBAR
+ })
+ .register()
+
+commandAPICommand("sidebar") {
+ objectiveArgument("objective")
+ anyExecutor { _, args ->
+ val objective = args["objective"] as Objective
+
+ // Set display slot
+ objective.displaySlot = DisplaySlot.SIDEBAR
+ }
+}
+
+The ObjectiveCriteriaArgument
is fairly straight forward - it represents the criteria for an objective. Similar to Bukkit, the objective criteria is simply represented as a String
, so it must be casted to a String
when being used.
Say we wanted to create a command to unregister all objectives based on a given criteria. Let's create a command with the following form:
+/unregisterall <objective critera>
+
+To do this, we're going to take advantage of Bukkit's Scoreboard.getObjectivesByCriteria(String)
method
new CommandAPICommand("unregisterall")
+ .withArguments(new ObjectiveCriteriaArgument("objective criteria"))
+ .executes((sender, args) -> {
+ String objectiveCriteria = (String) args.get("objective criteria");
+ Set<Objective> objectives = Bukkit.getScoreboardManager().getMainScoreboard().getObjectivesByCriteria(objectiveCriteria);
+
+ // Unregister the objectives
+ for (Objective objective : objectives) {
+ objective.unregister();
+ }
+ })
+ .register();
+
+CommandAPICommand("unregisterall")
+ .withArguments(ObjectiveCriteriaArgument("objective criteria"))
+ .executes(CommandExecutor { _, args ->
+ val objectiveCriteria = args["objective criteria"] as String
+ val objectives = Bukkit.getScoreboardManager().mainScoreboard.getObjectivesByCriteria(objectiveCriteria)
+
+ // Unregister the objectives
+ for (objective in objectives) {
+ objective.unregister()
+ }
+ })
+ .register()
+
+commandAPICommand("unregisterall") {
+ objectiveCriteriaArgument("objective criteria")
+ anyExecutor { _, args ->
+ val objectiveCriteria = args["objective criteria"] as String
+ val objectives = Bukkit.getScoreboardManager().mainScoreboard.getObjectivesByCriteria(objectiveCriteria)
+
+ // Unregister the objectives
+ for (objective in objectives) {
+ objective.unregister()
+ }
+ }
+}
+
+The TeamArgument
class interacts with the Minecraft scoreboard and represents a team.
Let's say we want to create a command to toggle the state of friendly fire in a team. We want a command of the following form
+/togglepvp <team>
+
+To do this, given a team we want to use the setAllowFriendlyFire(boolean)
function.
new CommandAPICommand("togglepvp")
+ .withArguments(new TeamArgument("team"))
+ .executes((sender, args) -> {
+ Team team = (Team) args.get("team");
+
+ // Toggle pvp
+ team.setAllowFriendlyFire(team.allowFriendlyFire());
+ })
+ .register();
+
+CommandAPICommand("togglepvp")
+ .withArguments(TeamArgument("team"))
+ .executes(CommandExecutor { _, args ->
+ val team = args["team"] as Team
+
+ // Toggle pvp
+ team.setAllowFriendlyFire(team.allowFriendlyFire())
+ })
+ .register()
+
+commandAPICommand("togglepvp") {
+ teamArgument("team")
+ anyExecutor { _, args ->
+ val team = args["team"] as Team
+
+ // Toggle pvp
+ team.setAllowFriendlyFire(team.allowFriendlyFire())
+ }
+}
+
+The angle argument is used to represent the yaw (horizontal) angle in degrees. The value returned from this argument range from -180.0 (inclusive) to 180 (exclusive), with -180.0 being due north:
+\begin{align} +-1&80.0 \\ +&\hspace{0.1em}N \\ +&\uparrow \\ +90.0\ W \leftarrow &\hspace{0.75em}\rightarrow E\ -90.0 \\ +&\downarrow \\ +&\hspace{0.2em}S \\ +&0.0 \\ +\end{align}
+The ~
notation can be used to specify a rotation relative to the executor's yaw angle.
The AdvancementArgument
class represents in-game advancements. As expected, the AdvancementArgument
can be casted to Bukkit's Advancement
class.
Say we want to award a player an advancement. First, we need the syntax that our command will use:
+/award <player> <advancement>
+
+Since we require a player, we will use the PlayerArgument
for this example. Given a player, we can simply get the AdvancementProgress
for that player, and then award the criteria required to fully complete the provided advancement.
new CommandAPICommand("award")
+ .withArguments(new PlayerArgument("player"))
+ .withArguments(new AdvancementArgument("advancement"))
+ .executes((sender, args) -> {
+ Player target = (Player) args.get("player");
+ Advancement advancement = (Advancement) args.get("advancement");
+
+ // Award all criteria for the advancement
+ AdvancementProgress progress = target.getAdvancementProgress(advancement);
+ for (String criteria : advancement.getCriteria()) {
+ progress.awardCriteria(criteria);
+ }
+ })
+ .register();
+
+CommandAPICommand("award")
+ .withArguments(PlayerArgument("player"))
+ .withArguments(AdvancementArgument("advancement"))
+ .executes(CommandExecutor { _, args ->
+ val target = args["player"] as Player
+ val advancement = args["advancement"] as Advancement
+
+ // Award all criteria for the advancement
+ val progress = target.getAdvancementProgress(advancement)
+ for (criteria in advancement.criteria) {
+ progress.awardCriteria(criteria)
+ }
+ })
+ .register()
+
+commandAPICommand("award") {
+ playerArgument("player")
+ advancementArgument("advancement")
+ anyExecutor { _, args ->
+ val target = args["player"] as Player
+ val advancement = args["advancement"] as Advancement
+
+ // Award all criteria for the advancement
+ val progress = target.getAdvancementProgress(advancement)
+ for (criteria in advancement.criteria) {
+ progress.awardCriteria(criteria)
+ }
+ }
+}
+
+In Minecraft 1.16, they added the ability to refer to in-game biomes. The CommandAPI implements this using the BiomeArgument
. As expected, this returns Bukkit's Biome
enum when used.
When using the Biome
object, the CommandAPI will return null
if the specified Biome
could not be found, for example if a player submitted a biome from a client-side resourcepack. The CommandAPI does not return Biome.CUSTOM
from the BiomeArgument
.
Say you want to set the biome of the current chunk that a player is in. We can do this using the World.setBiome(x, y, z, biome)
method for a given world. We will use this command syntax to set the biome of our current chunk:
/setbiome <biome>
+
+And we can set the biome of the current chunk as expected:
+new CommandAPICommand("setbiome")
+ .withArguments(new BiomeArgument("biome"))
+ .executesPlayer((player, args) -> {
+ Biome biome = (Biome) args.get("biome");
+
+ Chunk chunk = player.getLocation().getChunk();
+ player.getWorld().setBiome(chunk.getX(), player.getLocation().getBlockY(), chunk.getZ(), biome);
+ })
+ .register();
+
+CommandAPICommand("setbiome")
+ .withArguments(BiomeArgument("biome"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val biome = args["biome"] as Biome
+
+ val chunk = player.location.chunk
+ player.world.setBiome(chunk.x, player.location.blockY, chunk.z, biome)
+ })
+ .register()
+
+commandAPICommand("setbiome") {
+ biomeArgument("biome")
+ playerExecutor { player, args ->
+ val biome = args["biome"] as Biome
+
+ val chunk = player.location.chunk
+ player.world.setBiome(chunk.x, player.location.blockY, chunk.z, biome)
+ }
+}
+
+The BiomeArgument
also supports returning a NamespacedKey
for custom biomes. This can be done by using the BiomeArgument.NamespacedKey
constructor instead of the normal BiomeArgument
constructor:
// Makes a BiomeArgument that returns a Biome
+new BiomeArgument("biome");
+
+// Makes a BiomeArgument that returns a NamespacedKey
+new BiomeArgument.NamespacedKey("biome");
+
+++Developer's Note:
+Spigot's support for custom biomes is really limited! If you have an example that lets you use custom biomes with namespaced keys, please open a GitHub issue, or reach out to us on Discord!
+
The BlockStateArgument
is used to represent data about blocks in the world. These refer to any blocks that have data or states, such as dispensers, signs, doors and pistons. The BlockStateArgument
creates a Bukkit BlockData
object when used.
++Developer's Note:
+Make sure to not confuse the cast type with
+BlockState
. The naming of this argument refers to the internal Minecraft vanilla argument naming convention - this argument casts toBlockData
and NOTBlockState
.
Say we want a simple command to set the block that you're looking at. We'll use the following command syntax:
+/set <block>
+
+And then we can simply set our block using setBlockData()
:
new CommandAPICommand("set")
+ .withArguments(new BlockStateArgument("block"))
+ .executesPlayer((player, args) -> {
+ BlockData blockdata = (BlockData) args.get("block");
+ Block targetBlock = player.getTargetBlockExact(256);
+
+ // Set the block, along with its data
+ targetBlock.setType(blockdata.getMaterial());
+ targetBlock.getState().setBlockData(blockdata);
+ })
+ .register();
+
+CommandAPICommand("set")
+ .withArguments(BlockStateArgument("block"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val blockdata = args["block"] as BlockData
+ val targetBlock = player.getTargetBlockExact(256)
+
+ // Set the block, along with its data
+ targetBlock?.type = blockdata.material
+ targetBlock?.state?.blockData = blockdata
+ })
+ .register()
+
+commandAPICommand("set") {
+ blockStateArgument("block")
+ playerExecutor { player, args ->
+ val blockdata = args["block"] as BlockData
+ val targetBlock = player.getTargetBlockExact(256)
+
+ // Set the block, along with its data
+ targetBlock?.type = blockdata.material
+ targetBlock?.state?.blockData = blockdata
+ }
+}
+
+The EnchantmentArgument
class lets users input a specific enchantment. As you would expect, the cast type is Bukkit's Enchantment
class.
Say we want to give a player an enchantment on the item that the player is currently holding. We will use the following command syntax:
+/enchantitem <enchantment> <level>
+
+Since most enchantment levels range between 1 and 5, we will also make use of the IntegerArgument
to restrict the level of the enchantment by usng its range constructor.
new CommandAPICommand("enchantitem")
+ .withArguments(new EnchantmentArgument("enchantment"))
+ .withArguments(new IntegerArgument("level", 1, 5))
+ .executesPlayer((player, args) -> {
+ Enchantment enchantment = (Enchantment) args.get("enchantment");
+ int level = (int) args.get("level");
+
+ // Add the enchantment
+ player.getInventory().getItemInMainHand().addEnchantment(enchantment, level);
+ })
+ .register();
+
+CommandAPICommand("enchantitem")
+ .withArguments(EnchantmentArgument("enchantment"))
+ .withArguments(IntegerArgument("level", 1, 5))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val enchantment = args["enchantment"] as Enchantment
+ val level = args["level"] as Int
+
+ // Add the enchantment
+ player.inventory.itemInMainHand.addEnchantment(enchantment, level)
+ })
+ .register()
+
+commandAPICommand("enchantitem") {
+ enchantmentArgument("enchantment")
+ integerArgument("level", 1, 5)
+ playerExecutor { player, args ->
+ val enchantment = args["enchantment"] as Enchantment
+ val level = args["level"] as Int
+
+ // Add the enchantment
+ player.inventory.itemInMainHand.addEnchantment(enchantment, level)
+ }
+}
+
+The ItemStackArgument
class represents in-game items. As expected, this should be casted to Bukkit's ItemStack
object. The ItemStack
which is returned by the ItemStackArgument
always has a size of 1.
Say we want to create a command that gives you items. For this command, we will use the following syntax:
+/item <itemstack>
+
+With this syntax, we can easily create our command:
+new CommandAPICommand("item")
+ .withArguments(new ItemStackArgument("itemStack"))
+ .executesPlayer((player, args) -> {
+ player.getInventory().addItem((ItemStack) args.get("itemStack"));
+ })
+ .register();
+
+CommandAPICommand("item")
+ .withArguments(ItemStackArgument("itemStack"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ player.inventory.addItem(args["itemStack"] as ItemStack)
+ })
+ .register()
+
+commandAPICommand("item") {
+ itemStackArgument("itemstack")
+ playerExecutor { player, args ->
+ player.inventory.addItem(args["itemstack"] as ItemStack)
+ }
+}
+
+The LootTableArgument
class can be used to get a Bukkit LootTable
object.
Say we wanted to write a command that populates a chest with some loot table contents. For this example, we'll use the following command:
+/giveloottable <loottable> <location>
+
+We ensure that the location provided is a container (such as a chest or shulkerbox) and then update the contents of that container:
+new CommandAPICommand("giveloottable")
+ .withArguments(new LootTableArgument("lootTable"))
+ .withArguments(new LocationArgument("location", LocationType.BLOCK_POSITION))
+ .executes((sender, args) -> {
+ LootTable lootTable = (LootTable) args.get("lootTable");
+ Location location = (Location) args.get("location");
+
+ BlockState state = location.getBlock().getState();
+
+ // Check if the input block is a container (e.g. chest)
+ if (state instanceof Container container && state instanceof Lootable lootable) {
+ // Apply the loot table to the chest
+ lootable.setLootTable(lootTable);
+ container.update();
+ }
+ })
+ .register();
+
+CommandAPICommand("giveloottable")
+ .withArguments(LootTableArgument("lootTable"))
+ .withArguments(LocationArgument("location", LocationType.BLOCK_POSITION))
+ .executes(CommandExecutor { _, args ->
+ val lootTable = args["lootTable"] as LootTable
+ val location = args["location"] as Location
+
+ val state = location.block.state
+
+ // Check if the input block is a container (e.g. chest)
+ if (state is Container && state is Lootable) {
+ // Apply the loot table to the chest
+ state.lootTable = lootTable
+ state.update()
+ }
+ })
+ .register()
+
+commandAPICommand("giveloottable") {
+ lootTableArgument("loottable")
+ locationArgument("location", LocationType.BLOCK_POSITION)
+ anyExecutor { _, args ->
+ val lootTable = args["loottable"] as LootTable
+ val location = args["location"] as Location
+
+ val state = location.block.state
+
+ // Check if the input block is a container (e.g. chest)
+ if (state is Container && state is Lootable) {
+ // Apply the loot table to the chest
+ state.lootTable = lootTable
+ state.update()
+ }
+ }
+}
+
+The CommandAPI's MathOperationArgument
is used to represent the Minecraft scoreboard arithmetic operation to alter scoreboard scores. Since there is no default representation in the Bukkit API, the CommandAPI provides the MathOperation
class to represent each operation:
Symbol (in Minecraft) | MathOperation enum value |
---|---|
\(+=\) | MathOperation.ADD |
\(-=\) | MathOperation.SUBTRACT |
\(*=\) | MathOperation.MULTIPLY |
\(/=\) | MathOperation.DIVIDE |
\(\%=\) | MathOperation.MOD |
\(=\) | MathOperation.ASSIGN |
\(<\) | MathOperation.MIN |
\(>\) | MathOperation.MAX |
\(><\) | MathOperation.SWAP |
The MathOperation
also has two methods:
public int apply(int val1, int val2);
+public float apply(float val1, float val2);
+
+These methods are used to provide a basic implementation of these math operations on a given input. Given the values val1
and val2
, these are the operation that the apply(val1, val2)
method performs:
MathOperation enum value | Result |
---|---|
MathOperation.ADD | val1 + val2 |
MathOperation.SUBTRACT | val1 - val2 |
MathOperation.MULTIPLY | val1 * val2 |
MathOperation.DIVIDE | val1 / val2 |
MathOperation.MOD | val1 % val2 |
MathOperation.ASSIGN | val2 |
MathOperation.MIN | Math.min(val1, val2) |
MathOperation.MAX | Math.max(val1, val2) |
MathOperation.SWAP | val2 |
Say we wanted to create a player's level. Typically, this is implemented in the following manner:
+/xp set <player> <level>
+/xp add <player> <levels>
+
+Using the MathOperationArgument
, we can extend the functionality of adding and setting a player's level by allowing the user to choose what operation they desire. To do this, we'll use the following syntax:
/changelevel <player> <operation> <value>
+
+As with any command, we declare our arguments, cast them properly and then we write our main code. In this example, we use the apply(int, int)
method from our MathOperation
to calculate the player's new level.
new CommandAPICommand("changelevel")
+ .withArguments(new PlayerArgument("player"))
+ .withArguments(new MathOperationArgument("operation"))
+ .withArguments(new IntegerArgument("value"))
+ .executes((sender, args) -> {
+ Player target = (Player) args.get("player");
+ MathOperation op = (MathOperation) args.get("operation");
+ int value = (int) args.get("value");
+
+ target.setLevel(op.apply(target.getLevel(), value));
+ })
+ .register();
+
+CommandAPICommand("changelevel")
+ .withArguments(PlayerArgument("player"))
+ .withArguments(MathOperationArgument("operation"))
+ .withArguments(IntegerArgument("value"))
+ .executes(CommandExecutor { _, args ->
+ val target = args["player"] as Player
+ val op = args["operation"] as MathOperation
+ val value = args["value"] as Int
+
+ target.level = op.apply(target.level, value)
+ })
+ .register()
+
+commandAPICommand("changelevel") {
+ playerArgument("player")
+ mathOperationArgument("operation")
+ integerArgument("value")
+ anyExecutor { _, args ->
+ val target = args["player"] as Player
+ val op = args["operation"] as MathOperation
+ val value = args["value"] as Int
+
+ target.level = op.apply(target.level, value)
+ }
+}
+
+There are various applications for the changelevel
command based on what the user inputs. For example:
To set the player Notch to level 10:
+/changelevel Notch = 10
+
+To double the player Notch's level:
+/changelevel Notch *= 2
+
+To set the player Notch's level to 20, or keep it as their current level if it is higher than 20:
+/changelevel Notch > 20
+
+NamespacedKey arguments represent Minecraft's resource locations, or namespaced keys. This argument is casted to Bukkit's NamespacedKey
object.
Namespaced keys are typically of the form namespace:key
. If no namespace is provided, the default namespace (minecraft
) will be used.
The ParticleArgument
class represents Minecraft particles. This is casted to the CommandAPI's ParticleData
class.
ParticleData
classThe ParticleData
class is a record that contains two values:
Particle particle
, which is the Bukkit enum Particle
representation of what particle was providedT data
, which represents any additional particle data which was provided.public record ParticleData<T>(Particle particle, T data);
+
+The T data
can be used in Bukkit's World.spawnParticle(Particle particle, Location location, int count, T data)
method.
Particle data depends on your version of Minecraft. In 1.20.5, Minecraft and Spigot updated their particle API and they are no longer compatible with each other. Information about how the CommandAPI uses particle data can be found using the links below:
+ +The particle argument requires additional data for a particle depending on what the particle is. Information about this can be found on the Argument types page on the MinecraftWiki. The following particles have additional data required to display them:
+Bukkit Particle | Minecraft particle | Arguments |
---|---|---|
BLOCK_CRACK | block | block block_id block block_id[block_state=value] |
BLOCK_MARKER | block_marker | block_marker block_id block_marker block_id[block_state=value] |
REDSTONE | dust | dust red green blue size |
DUST_COLOR_TRANSITION | dust_color_transition | dust_color_transition red1 green1 blue1 size red2 green2 blue2 |
FALLING_DUST | falling_dust | falling_dust block_id falling_dust block_id[block_state=value] |
ITEM_CRACK | item | item item_id item item_id{NBT} |
SCULK_CHARGE | sculk_charge | sculk_charge angle |
SHRIEK | shriek | shriek delay |
VIBRATION | vibration | vibration x y z ticks |
Because certain particles (in the table above) require additional data, it is not recommended to spawn a particle without its corresponding data. This can result in particles not showing due to missing requirements.
+Say we wanted to have a command that displayed particles at a player's location. We will use the following command syntax:
+/showparticle <particle>
+
+With this, we can simply spawn the particle using the World.spawnParticle(Particle, Location, int)
method:
new CommandAPICommand("showparticle")
+ .withArguments(new ParticleArgument("particle"))
+ .executesPlayer((player, args) -> {
+ ParticleData<?> particleData = (ParticleData<?>) args.get("particle");
+ player.getWorld().spawnParticle(particleData.particle(), player.getLocation(), 1);
+ })
+ .register();
+
+CommandAPICommand("showparticle")
+ .withArguments(ParticleArgument("particle"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val particleData = args["particle"] as ParticleData<Any>
+ player.world.spawnParticle(particleData.particle(), player.location, 1)
+ })
+ .register()
+
+commandAPICommand("showparticle") {
+ particleArgument("particle")
+ playerExecutor { player, args ->
+ val particleData = args["particle"] as ParticleData<Any>
+ player.world.spawnParticle(particleData.particle(), player.location, 1)
+ }
+}
+
+Running this can result in errors due to missing requirements. If you provide a particle that has additional requirements, Bukkit will throw an error and the particle will not be displayed. Instead, the example below should be used.
+We can fix the issues with the example above by providing the data of the argument using the ParticleData
record:
/showparticle <particle>
+
+In this case, we'll use the World.spawnParticle(Particle particle, Location location, int count, T data)
method which accepts some particle data:
new CommandAPICommand("showparticle")
+ .withArguments(new ParticleArgument("particle"))
+ .executesPlayer((player, args) -> {
+ ParticleData<?> particleData = (ParticleData<?>) args.get("particle");
+ player.getWorld().spawnParticle(particleData.particle(), player.getLocation(), 1, particleData.data());
+ })
+ .register();
+
+CommandAPICommand("showparticle")
+ .withArguments(ParticleArgument("particle"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val particleData = args["particle"] as ParticleData<Any>
+ player.world.spawnParticle(particleData.particle(), player.location, 1, particleData.data())
+ })
+ .register()
+
+commandAPICommand("showparticle") {
+ particleArgument("particle")
+ playerExecutor { player, args ->
+ val particleData = args["particle"] as ParticleData<Any>
+ player.world.spawnParticle(particleData.particle(), player.location, 1, particleData.data())
+ }
+}
+
+This can be used with commands such as:
+/showparticle minecraft:dust_color_transition 0 0 0 20 1 0 0
+/showparticle minecraft:block_marker diamond_block
+
+The vibration
particle will return a particle data of the Bukkit Vibration
class. In the Vibration
class, you can access the destination location using the Vibration.getDestination()
method, which returns a Vibration.Destination
instance. The CommandAPI will always return a Vibration.Destination.BlockDestination
instance, and will never return a Vibration.Destination.EntityDestination
instance. An example of accessing the location can be found below:
ParticleData<Vibration> particleData; // The particle data you get from your argument
+Location destination = ((BlockDestination) particleData.data().getDestination()).getLocation();
+
+The particle argument requires additional data for a particle depending on what the particle is. The following particles have additional data required to display them:
+ +Bukkit Particle | +Argument syntax | +
---|---|
BLOCK |
+
+ block{block_state:{Name:"block_name"}}+
|
+
BLOCK_MARKER |
+
+ block_marker{block_state:{Name:"block_name"}}+
|
+
DUST |
+
+ dust{color:[red,green,blue],scale:scale}+
|
+
DUST_COLOR_TRANSITION |
+
+ dust_color_transition{from_color:[red,green,blue],+
|
+
DUST_PILLAR |
+
+ dust_pillar{block_state:{Name:"block_name"}}+
|
+
ENTITY_EFFECT |
+
+ entity_effect{color:[red,green,blue,alpha]}+
|
+
FALLING_DUST |
+
+ falling_dust{block_state:{Name:"block_name"}}+
|
+
ITEM |
+
+ item{item:"item"}+
|
+
SCULK_CHARGE |
+
+ sculk_charge{roll:angle}+
|
+
SHRIEK |
+
+ shriek{delay:delay}+
|
+
VIBRATION |
+
+ vibration{destination:{type:"block",pos:[x,y,z]},+
|
+
Because certain particles (in the table above) require additional data, it is not recommended to spawn a particle without its corresponding data. This can result in particles not showing due to missing requirements.
+Say we wanted to have a command that displayed particles at a player's location. We will use the following command syntax:
+/showparticle <particle>
+
+With this, we can simply spawn the particle using the World.spawnParticle(Particle, Location, int)
method:
new CommandAPICommand("showparticle")
+ .withArguments(new ParticleArgument("particle"))
+ .executesPlayer((player, args) -> {
+ ParticleData<?> particleData = (ParticleData<?>) args.get("particle");
+ player.getWorld().spawnParticle(particleData.particle(), player.getLocation(), 1);
+ })
+ .register();
+
+CommandAPICommand("showparticle")
+ .withArguments(ParticleArgument("particle"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val particleData = args["particle"] as ParticleData<Any>
+ player.world.spawnParticle(particleData.particle(), player.location, 1)
+ })
+ .register()
+
+commandAPICommand("showparticle") {
+ particleArgument("particle")
+ playerExecutor { player, args ->
+ val particleData = args["particle"] as ParticleData<Any>
+ player.world.spawnParticle(particleData.particle(), player.location, 1)
+ }
+}
+
+Running this can result in errors due to missing requirements. If you provide a particle that has additional requirements, Bukkit will throw an error and the particle will not be displayed. Instead, the example below should be used.
+We can fix the issues with the example above by providing the data of the argument using the ParticleData
record:
/showparticle <particle>
+
+In this case, we'll use the World.spawnParticle(Particle particle, Location location, int count, T data)
method which accepts some particle data:
new CommandAPICommand("showparticle")
+ .withArguments(new ParticleArgument("particle"))
+ .executesPlayer((player, args) -> {
+ ParticleData<?> particleData = (ParticleData<?>) args.get("particle");
+ player.getWorld().spawnParticle(particleData.particle(), player.getLocation(), 1, particleData.data());
+ })
+ .register();
+
+CommandAPICommand("showparticle")
+ .withArguments(ParticleArgument("particle"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val particleData = args["particle"] as ParticleData<Any>
+ player.world.spawnParticle(particleData.particle(), player.location, 1, particleData.data())
+ })
+ .register()
+
+commandAPICommand("showparticle") {
+ particleArgument("particle")
+ playerExecutor { player, args ->
+ val particleData = args["particle"] as ParticleData<Any>
+ player.world.spawnParticle(particleData.particle(), player.location, 1, particleData.data())
+ }
+}
+
+This can be used with commands such as:
+/showparticle minecraft:dust_color_transition{from_color:[0.0,0.0,0.0],scale:20.0,to_color:[1.0,0.0,0.0]}
+/showparticle minecraft:block_marker{block_state:{Name:"diamond_block"}}
+
+The vibration
particle will return a particle data of the Bukkit Vibration
class. In the Vibration
class, you can access the destination location using the Vibration.getDestination()
method, which returns a Vibration.Destination
instance. The CommandAPI will always return a Vibration.Destination.BlockDestination
instance, and will never return a Vibration.Destination.EntityDestination
instance. An example of accessing the location can be found below:
ParticleData<Vibration> particleData; // The particle data you get from your argument
+Location destination = ((BlockDestination) particleData.data().getDestination()).getLocation();
+
+The PotionEffectArgument
class represents Minecraft potion effects. When used, this argument is casted to Bukkit's PotionEffectType
class, or alternatively a NamespacedKey
object if the PotionEffectArgument.NamespacedKey
argument is used to create a PotionEffectArgument
.
Say we wanted to have a command that gives a player a potion effect. For this command, we'll use the following syntax:
+/potion <target> <potion> <duration> <strength>
+
+In this example, we utilize some of the other arguments that we've described earlier, such as the PlayerArgument
and TimeArgument
. Since duration for the PotionEffect
constructor is in ticks, this is perfectly fit for the TimeArgument
, which is represented in ticks.
new CommandAPICommand("potion")
+ .withArguments(new PlayerArgument("target"))
+ .withArguments(new PotionEffectArgument("potion"))
+ .withArguments(new TimeArgument("duration"))
+ .withArguments(new IntegerArgument("strength"))
+ .executes((sender, args) -> {
+ Player target = (Player) args.get("target");
+ PotionEffectType potion = (PotionEffectType) args.get("potion");
+ int duration = (int) args.get("duration");
+ int strength = (int) args.get("strength");
+
+ // Add the potion effect to the target player
+ target.addPotionEffect(new PotionEffect(potion, duration, strength));
+ })
+ .register();
+
+new CommandAPICommand("potion")
+ .withArguments(new PlayerArgument("target"))
+ .withArguments(new PotionEffectArgument.NamespacedKey("potion"))
+ .withArguments(new TimeArgument("duration"))
+ .withArguments(new IntegerArgument("strength"))
+ .executes((sender, args) -> {
+ Player target = (Player) args.get("target");
+ NamespacedKey potionKey = (NamespacedKey) args.get("potion");
+ int duration = (int) args.get("duration");
+ int strength = (int) args.get("strength");
+
+ PotionEffectType potion = PotionEffectType.getByKey(potionKey);
+
+ // Add the potion effect to the target player
+ target.addPotionEffect(new PotionEffect(potion, duration, strength));
+ })
+ .register();
+
+CommandAPICommand("potion")
+ .withArguments(PlayerArgument("target"))
+ .withArguments(PotionEffectArgument("potion"))
+ .withArguments(TimeArgument("duration"))
+ .withArguments(IntegerArgument("strength"))
+ .executes(CommandExecutor { _, args ->
+ val target = args["target"] as Player
+ val potion = args["potion"] as PotionEffectType
+ val duration = args["duration"] as Int
+ val strength = args["strength"] as Int
+
+ // Add the potion effect to the target player
+ target.addPotionEffect(PotionEffect(potion, duration, strength))
+ })
+ .register()
+
+CommandAPICommand("potion")
+ .withArguments(PlayerArgument("target"))
+ .withArguments(PotionEffectArgument.NamespacedKey("potion"))
+ .withArguments(TimeArgument("duration"))
+ .withArguments(IntegerArgument("strength"))
+ .executes(CommandExecutor { _, args ->
+ val target = args["target"] as Player
+ val potionKey = args["potion"] as NamespacedKey
+ val duration = args["duration"] as Int
+ val strength = args["strength"] as Int
+
+ val potion = PotionEffectType.getByKey(potionKey)!!
+
+ // Add the potion effect to the target player
+ target.addPotionEffect(PotionEffect(potion, duration, strength))
+ })
+ .register()
+
+commandAPICommand("potion") {
+ playerArgument("target")
+ potionEffectArgument("potion")
+ timeArgument("duration")
+ integerArgument("strength")
+ anyExecutor { _, args ->
+ val target = args["target"] as Player
+ val potion = args["potion"] as PotionEffectType
+ val duration = args["duration"] as Int
+ val strength = args["strength"] as Int
+
+ // Add the potion effect to the target player
+ target.addPotionEffect(PotionEffect(potion, duration, strength))
+ }
+}
+
+commandAPICommand("potion") {
+ playerArgument("target")
+ potionEffectArgument("potion", true)
+ timeArgument("duration")
+ integerArgument("strength")
+ anyExecutor { _, args ->
+ val target = args["target"] as Player
+ val potionKey = args["potion"] as NamespacedKey
+ val duration = args["duration"] as Int
+ val strength = args["strength"] as Int
+
+ val potion = PotionEffectType.getByKey(potionKey)!!
+
+ // Add the potion effect to the target player
+ target.addPotionEffect(PotionEffect(potion, duration, strength))
+ }
+}
+
+The RecipeArgument
class lets you retrieve Bukkit's ComplexRecipe
object.
Say we want to give yourself the result of a specific recipe. Since Bukkit's Recipe
class contains the getResult()
method, we will use that in our example. We want to create the following command:
/giverecipe <recipe>
+
+As such, we easily implement it by specifying the RecipeArgument
, casting it and adding it to the player's inventory:
new CommandAPICommand("giverecipe")
+ .withArguments(new RecipeArgument("recipe"))
+ .executesPlayer((player, args) -> {
+ ComplexRecipe recipe = (ComplexRecipe) args.get("recipe");
+ player.getInventory().addItem(recipe.getResult());
+ })
+ .register();
+
+CommandAPICommand("giverecipe")
+ .withArguments(RecipeArgument("recipe"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val recipe = args["recipe"] as ComplexRecipe
+ player.inventory.addItem(recipe.result)
+ })
+ .register()
+
+commandAPICommand("giverecipe") {
+ recipeArgument("recipe")
+ playerExecutor { player, args ->
+ val recipe = args["recipe"] as ComplexRecipe
+ player.inventory.addItem(recipe.result)
+ }
+}
+
+In this example, we'll use the ComplexRecipe
's getKey()
method to write an example to to unlock a recipe for a player. For this command, we'll use the following syntax:
/unlockrecipe <player> <recipe>
+
+This is then implemented trivially as follows:
+new CommandAPICommand("unlockrecipe")
+ .withArguments(new PlayerArgument("player"))
+ .withArguments(new RecipeArgument("recipe"))
+ .executes((sender, args) -> {
+ Player target = (Player) args.get("player");
+ ComplexRecipe recipe = (ComplexRecipe) args.get("recipe");
+
+ target.discoverRecipe(recipe.getKey());
+ })
+ .register();
+
+CommandAPICommand("unlockrecipe")
+ .withArguments(PlayerArgument("player"))
+ .withArguments(RecipeArgument("recipe"))
+ .executes(CommandExecutor { _, args ->
+ val target = args["player"] as Player
+ val recipe = args["recipe"] as ComplexRecipe
+
+ target.discoverRecipe(recipe.key)
+ })
+ .register()
+
+commandAPICommand("unlockrecipe") {
+ playerArgument("player")
+ recipeArgument("recipe")
+ anyExecutor { _, args ->
+ val target = args["player"] as Player
+ val recipe = args["recipe"] as ComplexRecipe
+
+ target.discoverRecipe(recipe.key)
+ }
+}
+
+The SoundArgument
class allows a command sender to retrieve the Bukkit Sound
or NamespacedKey
object to represent in-game sound effects (such as mob sounds or ambient sound effects), as well as in-game music.
The SoundArgument
can return a Sound
or NamespacedKey
object. To return a Sound
object, simply use the SoundArgument
as normal. To return a NamespacedKey
object, use the SoundArgument.NamespacedKey
constructor instead:
// Makes a SoundArgument that returns a Sound
+new SoundArgument("sound");
+
+// Makes a SoundArgument that returns a NamespacedKey
+new SoundArgument.NamespacedKey("sound");
+
+When using the Sound
object, the CommandAPI will return null
if the specified Sound
could not be found. For this reason, it's recommended to use the NamespacedKey
object for optimal compatibility with client-side resourcepacks.
Say we want a simple command that plays a specific sound at your location. To do this, we will make the following command:
+/sound <sound>
+
+This command simply plays the provided sound to the current player:
+new CommandAPICommand("sound")
+ .withArguments(new SoundArgument("sound"))
+ .executesPlayer((player, args) -> {
+ player.getWorld().playSound(player.getLocation(), (Sound) args.get("sound"), 100.0f, 1.0f);
+ })
+ .register();
+
+new CommandAPICommand("sound")
+ .withArguments(new SoundArgument.NamespacedKey("sound"))
+ .executesPlayer((player, args) -> {
+ player.getWorld().playSound(player.getLocation(), ((NamespacedKey) args.get("sound")).asString(), 100.0f, 1.0f);
+ })
+ .register();
+
+CommandAPICommand("sound")
+ .withArguments(SoundArgument("sound"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ player.world.playSound(player.location, args["sound"] as Sound, 100.0f, 1.0f)
+ })
+ .register()
+
+CommandAPICommand("sound")
+ .withArguments(SoundArgument.NamespacedKey("sound"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ player.world.playSound(player.location, (args["sound"] as NamespacedKey).asString(), 100.0f, 1.0f)
+ })
+ .register()
+
+commandAPICommand("sound") {
+ soundArgument("sound")
+ playerExecutor { player, args ->
+ player.world.playSound(player.location, args["sound"] as Sound, 100.0f, 1.0f)
+ }
+}
+
+commandAPICommand("sound") {
+ soundArgument("sound", true)
+ playerExecutor { player, args ->
+ player.world.playSound(player.location, (args["sound"] as NamespacedKey).asString(), 100.0f, 1.0f)
+ }
+}
+
+The TimeArgument
class represents in-game time, in the number of in-game ticks. This allows command senders to specify a certain number of ticks in a simpler way, by including the characters d
to specify the numbers of days, s
to specify the number of seconds or t
to specify a number of ticks.
The CommandAPI converts the inputs provided by the command sender into a number of ticks as an integer.
+++Developer's Note:
+The
+TimeArgument
provides inputs such as2d
(2 in-game days),10s
(10 seconds) and20t
(20 ticks), but does not let you combine them, such as2d10s
.
Say we have a command bigmsg
that displays a title message to all players for a certain duration:
/bigmsg <duration> <message>
+
+new CommandAPICommand("bigmsg")
+ .withArguments(new TimeArgument("duration"))
+ .withArguments(new GreedyStringArgument("message"))
+ .executes((sender, args) -> {
+ // Duration in ticks
+ int duration = (int) args.get("duration");
+ String message = (String) args.get("message");
+
+ for (Player player : Bukkit.getOnlinePlayers()) {
+ // Display the message to all players, with the default fade in/out times (10 and 20).
+ player.sendTitle(message, "", 10, duration, 20);
+ }
+ })
+ .register();
+
+CommandAPICommand("bigmsg")
+ .withArguments(TimeArgument("duration"))
+ .withArguments(GreedyStringArgument("message"))
+ .executes(CommandExecutor { _, args ->
+ // Duration in ticks
+ val duration = args["duration"] as Int
+ val message = args["message"] as String
+
+ for (player in Bukkit.getOnlinePlayers()) {
+ // Display the message to all players, with the default fade in/out times (10 and 20).
+ player.sendTitle(message, "", 10, duration, 20)
+ }
+ })
+ .register()
+
+commandAPICommand("bigmsg") {
+ timeArgument("duration")
+ greedyStringArgument("message")
+ anyExecutor { _, args ->
+ // Duration in ticks
+ val duration = args["duration"] as Int
+ val message = args["message"] as String
+
+ for (player in Bukkit.getOnlinePlayers()) {
+ // Display the message to all players, with the default fade in/out times (10 and 20).
+ player.sendTitle(message, "", 10, duration, 20)
+ }
+ }
+}
+
+The UUID argument is used to uniquely identify players, entities and attribute modifiers. As a result, its cast type is Java's UUID
.
The WorldArgument
class allows a command sender to refer to a loaded Bukkit World
.
Say we want to unload a world on our Minecraft server. We want to create a command with the following syntax:
+/unloadworld <world>
+
+Using the world from the WorldArgument
, we can then unload the world safely using Bukkit.getServer().unloadWorld()
and passing true
(to save chunks):
new CommandAPICommand("unloadworld")
+ .withArguments(new WorldArgument("world"))
+ .executes((sender, args) -> {
+ World world = (World) args.get("world");
+
+ // Unload the world (and save the world's chunks)
+ Bukkit.getServer().unloadWorld(world, true);
+ })
+ .register();
+
+CommandAPICommand("unloadworld")
+ .withArguments(WorldArgument("world"))
+ .executes(CommandExecutor { sender, args ->
+ val world = args["world"] as World
+
+ // Unload the world (and save the world's chunks)
+ Bukkit.getServer().unloadWorld(world, true)
+ })
+ .register()
+
+commandAPICommand("unloadworld") {
+ worldArgument("world")
+ anyExecutor { sender, args ->
+ val world = args["world"] as World
+
+ // Unload the world (and save the world's chunks)
+ Bukkit.getServer().unloadWorld(world, true)
+ }
+}
+
+The BlockPredicateArgument
is used to represent a "test" for Minecraft blocks. This can consist of tags, such as the ones listed here on the MinecraftWiki, or individual blocks. If a block matches the tag or block, then the predicate is satisfied.
For example, if we were to use the predicate #leaves
, then the following blocks will be satisfied by that predicate: jungle_leaves
, oak_leaves
, spruce_leaves
, dark_oak_leaves
, acacia_leaves
, birch_leaves
.
When used, this argument must be casted to a Predicate<Block>
. As with other similar arguments with parameterized types, you can ignore Java's unchecked cast type safety warning.
Say you want to replace blocks in a given radius. To do this, we'll use the following command structure:
+/replace <radius> <fromBlock> <toBlock>
+
+Of course, we could simply use a BlockStateArgument
or even an ItemStackArgument
as our <fromBlock>
in order to get the material to replace, but the block predicate argument provides a test for a given block, which if satisfied, allows certain code to be executed.
First, we declare our arguments. We want to use the BlockPredicateArgument
since it also allows us to use Minecraft tags to identify blocks, as well as individual blocks. We then use BlockStateArgument
to set the block to a given type. The BlockStateArgument
also allows the user to provide any block data (e.g. contents of a chest or a stair's orientation).
Argument<?>[] arguments = new Argument<?>[] {
+ new IntegerArgument("radius"),
+ new BlockPredicateArgument("fromBlock"),
+ new BlockStateArgument("toBlock"),
+};
+
+val arguments = arrayOf<Argument<*>>(
+ IntegerArgument("radius"),
+ BlockPredicateArgument("fromBlock"),
+ BlockStateArgument("toBlock"),
+)
+
+We then register our /replace
command. First, we parse the arguments making sure to cast to Predicate<Block>
and BlockData
(and not BlockState
). After that, we use a few simple for loops to find the blocks within a radius sphere from the player.
In our most nested loop, we can then check if the block meets the requirements of our predicate. This is simply performed using predicate.test(block)
, and if satisfied, we can set the block's type.
Lastly, we register our command as normal using the register()
method.
new CommandAPICommand("replace")
+ .withArguments(arguments)
+ .executesPlayer((player, args) -> {
+
+ // Parse the arguments
+ int radius = (int) args.get("radius");
+ @SuppressWarnings("unchecked")
+ Predicate<Block> predicate = (Predicate<Block>) args.get("fromBlock");
+ BlockData blockData = (BlockData) args.get("toBlock");
+
+ // Find a (solid) sphere of blocks around the player with a given radius
+ Location center = player.getLocation();
+ for (int x = -radius; x <= radius; x++) {
+ for (int y = -radius; y <= radius; y++) {
+ for (int z = -radius; z <= radius; z++) {
+ if (Math.sqrt(((x * x) + (y * y) + (z * z))) <= radius) {
+ Block block = center.getWorld().getBlockAt(x + center.getBlockX(), y + center.getBlockY(), z + center.getBlockZ());
+
+ // If that block matches a block from the predicate, set it
+ if (predicate.test(block)) {
+ block.setType(blockData.getMaterial());
+ block.setBlockData(blockData);
+ }
+ }
+ }
+ }
+ }
+ return;
+ })
+ .register();
+
+CommandAPICommand("replace")
+ .withArguments(*arguments)
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+
+ // Parse the arguments
+ val radius = args["radius"] as Int
+ val predicate = args["fromBlock"] as Predicate<Block>
+ val blockData = args["toBlock"] as BlockData
+
+ // Find a (solid) sphere of blocks around the player with a given radius
+ val center = player.location // for (i in 1 until 11) { }
+ for (x in -radius until radius + 1) {
+ for (y in -radius until radius + 1) {
+ for (z in -radius until radius + 1) {
+ if (Math.sqrt((x * x + y * y + z * z).toDouble()) <= radius) {
+ val block = center.world.getBlockAt(x + center.blockX, y + center.blockY, z + center.blockZ)
+
+ // If that block matches a block from the predicate, set it
+ if (predicate.test(block)) {
+ block.type = blockData.material
+ block.blockData = blockData
+ }
+ }
+ }
+ }
+ }
+ })
+ .register()
+
+commandAPICommand("replace") {
+ arguments(*arguments)
+ playerExecutor { player, args ->
+ // Parse the arguments
+ val radius = args["radius"] as Int
+ val predicate = args["fromBlock"] as Predicate<Block>
+ val blockData = args["toBlock"] as BlockData
+
+ // Find a (solid) sphere of blocks around the player with a given radius
+ val center = player.location // for (i in 1 until 11) { }
+ for (x in -radius until radius + 1) {
+ for (y in -radius until radius + 1) {
+ for (z in -radius until radius + 1) {
+ if (Math.sqrt((x * x + y * y + z * z).toDouble()) <= radius) {
+ val block = center.world.getBlockAt(x + center.blockX, y + center.blockY, z + center.blockZ)
+
+ // If that block matches a block from the predicate, set it
+ if (predicate.test(block)) {
+ block.type = blockData.material
+ block.blockData = blockData
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+Similar to the BlockPredicateArgument
, the ItemStackPredicateArgument
is a way of performing predicate checks on ItemStack
objects. These can represent tags, such as the ones declared here on the MinecraftWiki, or individual items. The cast type for this argument is Predicate<ItemStack>
.
Say we wanted to remove items in your inventory (I know, the /clear
command does this, but this is the only example I could come up with). To do this, we'll use the following command syntax:
/rem <item>
+
+We implement this with a simple for loop over the player's inventory and remove items that satisfy the predicate.
+// Register our command
+new CommandAPICommand("rem")
+ .withArguments(new ItemStackPredicateArgument("items"))
+ .executesPlayer((player, args) -> {
+
+ // Get our predicate
+ @SuppressWarnings("unchecked")
+ Predicate<ItemStack> predicate = (Predicate<ItemStack>) args.get("items");
+
+ for (ItemStack item : player.getInventory()) {
+ if (predicate.test(item)) {
+ player.getInventory().remove(item);
+ }
+ }
+ })
+ .register();
+
+// Register our command
+CommandAPICommand("rem")
+ .withArguments(ItemStackPredicateArgument("items"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+
+ // Get our predicate
+ val predicate = args["items"] as Predicate<ItemStack>
+
+ for (item in player.inventory) {
+ if (predicate.test(item)) {
+ player.inventory.remove(item)
+ }
+ }
+ })
+ .register()
+
+// Register our command
+commandAPICommand("rem") {
+ itemStackPredicateArgument("items")
+ playerExecutor { player, args ->
+ // Get our predicate
+ val predicate = args["items"] as Predicate<ItemStack>
+
+ for (item in player.inventory) {
+ if (predicate.test(item)) {
+ player.inventory.remove(item)
+ }
+ }
+ }
+}
+
+The CommandAPI includes support for NBT compound arguments using an NBT API. The usage for the NBTCompoundArgument
depends on whether you are using the CommandAPI plugin (using a CommandAPI.jar
file in your plugins/
folder), or are shading the CommandAPI (including the compiled CommandAPI code in your own plugin).
By default, the CommandAPI plugin includes a copy of the NBT API by tr7zw in dev.jorel.commandapi.nbtapi
. No additional set up is required and it can be used directly out the box.
In order to use the NBTCompoundArgument
, you will have to use an NBT API that can create an NBT Compound object from an Object
(ideally a net.minecraft.nbt.NBTTagCompound
object). Examples of NBT APIs that can do this are (these are not sponsored in any way):
new NBTContainer(Object)
constructorBefore the NBTCompoundArgument
can be used, the CommandAPI needs to know what implementation of an NBT Compound object you're going to use. This is specified in the onLoad()
sequence, where your CommandAPI's config is set up, by using the following method:
<T> CommandAPIConfig initializeNBTAPI(Class<T> nbtContainerClass, Function<Object, T> nbtContainerConstructor);
+
+The initializeNBTAPI(Class<T>, Function<Object, T>)
takes in two arguments:
Class<T>
- The class that will be your NBT Compound implementation. This is also the type that the CommandAPI will return when the NBTCompoundArgument
is used.
Function<Object, T>
- A function that takes in an object and returns the specified NBT Compound implementation. This could be a constructor or a static method, for example.
Say we want to use the NBT API as our implementation of NBT compounds. First, we have to shade the NBT API into our project (view the official documentation for how to do this for Maven or Gradle).
+Now, we can configure the CommandAPI using the CommandAPI.onLoad()
method to use the NBTContainer
class, and the NBTContainer
constructor that takes in an Object
:
@Override
+public void onLoad() {
+ CommandAPI.onLoad(new CommandAPIBukkitConfig(this)
+ .initializeNBTAPI(NBTContainer.class, NBTContainer::new)
+ );
+}
+
+override fun onLoad() {
+ CommandAPI.onLoad(CommandAPIBukkitConfig(this)
+ .initializeNBTAPI(NBTContainer::class.java, ::NBTContainer)
+ )
+}
+
+++Confused with the
+::new
syntax? Read more about method references to a constructor here.
We're now able to use the NBTContainer
as our implemented type for the NBTCompoundArgument
!
Since the underlying implementation of the NBTCompoundArgument
can change (e.g. NBTContainer
if you're using the NBT API), the type of your NBT compound implementation has to be declared in angle brackets.
new CommandAPICommand("award")
+ .withArguments(new NBTCompoundArgument<NBTContainer>("nbt"))
+ .executes((sender, args) -> {
+ NBTContainer nbt = (NBTContainer) args.get("nbt");
+
+ // Do something with "nbt" here...
+ })
+ .register();
+
+CommandAPICommand("award")
+ .withArguments(NBTCompoundArgument<NBTContainer>("nbt"))
+ .executes(CommandExecutor { _, args ->
+ val nbt = args["nbt"] as NBTContainer
+
+ // Do something with "nbt" here...
+ })
+ .register()
+
+commandAPICommand("award") {
+ nbtCompoundArgument<NBTContainer>("nbt")
+ anyExecutor { _, args ->
+ val nbt = args["nbt"] as NBTContainer
+
+ // Do something with "nbt" here...
+ }
+}
+
+++Developer's Note:
+If you believe you can supply a suitable example for this page, feel free to send an example on the CommandAPI issues page!
+
Literal arguments are used to represent "forced options" for a command. For instance, take Minecraft's /gamemode
command. The syntax consists of the following:
/gamemode <mode>
+/gamemode <mode> <player>
+
+It consists of a gamemode, followed by an optional player argument. The list of gamemodes are as follows:
+/gamemode survival
+/gamemode creative
+/gamemode adventure
+/gamemode spectator
+
+Unlike regular commands (as those implemented by Bukkit for example), these four options are "hardcoded" - they're not "suggestions". The user can only enter one of these four examples, no other values are allowed.
+Developer's Note:
+There is a simpler alternative to the LiteralArgument
class! Instead of having to deal with arguments not being present in the array of arguments, consider using the much more intuitive MultiLiteralArgument
, which is described in more detail here!
Unlike regular arguments that are shown in this chapter, the literal argument is technically not an argument. Due to this fact, literal arguments are unlisted by default. In other words, the literal argument is not present in the CommandArguments args
for the command declaration.
To illustrate the behavior of literal arguments, we create a command of the following form:
+/mycommand <literal> <text>
+
+As an example, let's declare the literal "hello" as a valid literal for this command. When we retrieve the result from args.get(0)
, it returns the value of the TextArgument
, as opposed to the literal "hello":
new CommandAPICommand("mycommand")
+ .withArguments(new LiteralArgument("hello"))
+ .withArguments(new TextArgument("text"))
+ .executes((sender, args) -> {
+ // This gives the variable "text" the contents of the TextArgument, and not the literal "hello"
+ String text = (String) args.get(0);
+ })
+ .register();
+
+CommandAPICommand("mycommand")
+ .withArguments(LiteralArgument("hello"))
+ .withArguments(TextArgument("text"))
+ .executes(CommandExecutor { _, args ->
+ // This gives the variable "text" the contents of the TextArgument, and not the literal "hello"
+ val text = args[0] as String
+ })
+ .register()
+
+commandAPICommand("mycommand") {
+ literalArgument("hello")
+ textArgument("text")
+ anyExecutor { _, args ->
+ // This gives the variable "text" the contents of the TextArgument, and not the literal "hello"
+ val text = args[0] as String
+ }
+}
+
+The LiteralArgument
class also provides the LiteralArgument.of()
and LiteralArgument.literal()
helper methods which can be used as an alternative way to declare literal arguments:
new CommandAPICommand("mycommand")
+ .withArguments(LiteralArgument.of("hello"))
+ .withArguments(new TextArgument("text"))
+ .executes((sender, args) -> {
+ // This gives the variable "text" the contents of the TextArgument, and not the literal "hello"
+ String text = (String) args.get(0);
+ })
+ .register();
+
+new CommandAPICommand("mycommand")
+ .withArguments(LiteralArgument.literal("hello"))
+ .withArguments(new TextArgument("text"))
+ .executes((sender, args) -> {
+ // This gives the variable "text" the contents of the TextArgument, and not the literal "hello"
+ String text = (String) args.get(0);
+ })
+ .register();
+
+CommandAPICommand("mycommand")
+ .withArguments(LiteralArgument.of("hello"))
+ .withArguments(TextArgument("text"))
+ .executes(CommandExecutor { _, args ->
+ val text = args[0] as String
+ })
+ .register()
+
+CommandAPICommand("mycommand")
+ .withArguments(LiteralArgument.literal("hello"))
+ .withArguments(TextArgument("text"))
+ .executes(CommandExecutor { _, args ->
+ val text = args[0] as String
+ })
+ .register()
+
+commandAPICommand("mycommand") {
+ argument(LiteralArgument.of("hello"))
+ textArgument("text")
+ anyExecutor { _, args ->
+ val text = args[0] as String
+ }
+}
+
+commandAPICommand("mycommand") {
+ argument(LiteralArgument.literal("hello"))
+ textArgument("text")
+ anyExecutor { _, args ->
+ val text = args[0] as String
+ }
+}
+
+If I were to run the following command:
+/mycommand hello goodbye
+
+The value of text
in the code above would be "goodbye".
This is a demonstration of how you could create a command similar to Minecraft's /gamemode
command by using literal arguments. To do this, we are effectively registering 4 separate commands, each called /gamemode
, but with different literal arguments.
// Create a map of gamemode names to their respective objects
+HashMap<String, GameMode> gamemodes = new HashMap<>();
+gamemodes.put("adventure", GameMode.ADVENTURE);
+gamemodes.put("creative", GameMode.CREATIVE);
+gamemodes.put("spectator", GameMode.SPECTATOR);
+gamemodes.put("survival", GameMode.SURVIVAL);
+
+// Iterate over the map
+for(Entry<String, GameMode> entry : gamemodes.entrySet()) {
+
+ // Register the command as usual
+ new CommandAPICommand("changegamemode")
+ .withArguments(new LiteralArgument(entry.getKey()))
+ .executesPlayer((player, args) -> {
+ // Retrieve the object from the map via the key and NOT the args[]
+ player.setGameMode(entry.getValue());
+ })
+ .register();
+}
+
+// Create a map of gamemode names to their respective objects
+val gamemodes = mapOf(
+ "adventure" to GameMode.ADVENTURE,
+ "creative" to GameMode.CREATIVE,
+ "spectator" to GameMode.SPECTATOR,
+ "survival" to GameMode.SURVIVAL
+)
+
+// Iterate over the map
+for ((key, _) in gamemodes) {
+
+ // Register the command as usual
+ CommandAPICommand("changegamemode")
+ .withArguments(LiteralArgument(key))
+ .executesPlayer(PlayerCommandExecutor { player, _ ->
+ // Retrieve the object from the map via the key and NOT the args[]
+ player.gameMode = gamemodes[key]!!
+ })
+ .register()
+}
+
+// Create a map of gamemode names to their respective objects
+val gamemodes = mapOf(
+ "adventure" to GameMode.ADVENTURE,
+ "creative" to GameMode.CREATIVE,
+ "spectator" to GameMode.SPECTATOR,
+ "survival" to GameMode.SURVIVAL
+)
+
+// Iterate over the map
+for ((key, _) in gamemodes) {
+
+ // Register the command as usual
+ commandAPICommand("changegamemode") {
+ literalArgument(key)
+ playerExecutor { player, args ->
+ // Retrieve the object from the map via the key and NOT the args[]
+ player.gameMode = gamemodes[key]!!
+ }
+ }
+
+}
+
+Note how, since we don't have access to the literal from args
, we must access the provided gamemode from elsewhere.
Literal arguments require a string in the constructor. If the literal is an empty String or is null, the CommandAPI will throw a BadLiteralException
.
Because literal arguments are "hardcoded", each literal is effectively mapped to a single command. This is shown when using the configuration option create-dispatcher-json: true
which shows the JSON result of registered commands. For instance, take the /defaultgamemode
command:
"defaultgamemode": {
+ "type": "literal",
+ "children": {
+ "adventure": {
+ "type": "literal",
+ "executable": true
+ },
+ "creative": {
+ "type": "literal",
+ "executable": true
+ },
+ "spectator": {
+ "type": "literal",
+ "executable": true
+ },
+ "survival": {
+ "type": "literal",
+ "executable": true
+ }
+ }
+},
+
+Each option produces a new "command" in the tree of commands. This means that having exceptionally large lists of literals, or nested literals (e.g. /command <literal1> <literal2>
) can cause very large trees which cannot be sent to the clients (it can cause clients to crash).
++Developer's Note:
+Take care when using literal arguments. If your list of arguments is exceptionally large, or contains many nested arguments, the server may be unable to send the command information to the client. If many command argument choices are required, consider using a
+StringArgument
and using.replaceSuggestions()
to create your own list of required arguments.
So far, we've described normal arguments and literal arguments. We've described the nuances with literal arguments and how they're not really "arguments", so they don't appear in the CommandArguments args
for commands.
Now forget all of that. Multi literal arguments are the same as literal arguments but they do appear in the CommandArguments args
for commands (i.e. they are listed). Multi literal arguments are just a way better alternative to literal arguments. The multi literal argument constructor allows you to provide a String nodeName
and a String... literals
of possible values which you can use for your command declaration.
The multi literal argument has all of the same benefits of a regular literal argument - they are hardcoded options that the user must enter - they don't allow other values.
+Developer's Note:
+For 9.1.0, all previously existing MultiLiteralArgument
constructors have been deprecated! They will be removed in a future version.
The new constructor looks like this:
+public MultiLiteralArgument(String nodeName, String... literals)
+
+In this example, we'll show how to use multi literals to declare Minecraft's /gamemode
command. As you can see from the example code below, the argument declaration and command declaration is the same as if you were declaring any normal argument or command.
new CommandAPICommand("gamemode")
+ .withArguments(new MultiLiteralArgument("gamemodes", "adventure", "creative", "spectator", "survival"))
+ .executesPlayer((player, args) -> {
+ // The literal string that the player enters IS available in the args[]
+ switch ((String) args.get("gamemodes")) {
+ case "adventure":
+ player.setGameMode(GameMode.ADVENTURE);
+ break;
+ case "creative":
+ player.setGameMode(GameMode.CREATIVE);
+ break;
+ case "spectator":
+ player.setGameMode(GameMode.SPECTATOR);
+ break;
+ case "survival":
+ player.setGameMode(GameMode.SURVIVAL);
+ break;
+ default:
+ player.sendMessage("Invalid gamemode: " + args.get("gamemodes"));
+ break;
+ }
+ })
+ .register();
+
+CommandAPICommand("gamemode")
+ .withArguments(MultiLiteralArgument("gamemodes", "adventure", "creative", "spectator", "survival"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ // The literal string that the player enters IS available in the args[]
+ when (args["gamemodes"] as String) {
+ "adventure" -> player.gameMode = GameMode.ADVENTURE
+ "creative" -> player.gameMode = GameMode.CREATIVE
+ "spectator" -> player.gameMode = GameMode.SPECTATOR
+ "survival" -> player.gameMode = GameMode.SURVIVAL
+ }
+ })
+ .register()
+
+commandAPICommand("gamemode") {
+ multiLiteralArgument(nodeName = "gamemodes", "adventure", "creative", "spectator", "survival") // Adding this for now, needed because ambiguous methods exist
+ playerExecutor { player, args ->
+ // The literal string that the player enters IS available in the args[]
+ when (args["gamemodes"] as String) {
+ "adventure" -> player.gameMode = GameMode.ADVENTURE
+ "creative" -> player.gameMode = GameMode.CREATIVE
+ "spectator" -> player.gameMode = GameMode.SPECTATOR
+ "survival" -> player.gameMode = GameMode.SURVIVAL
+ }
+ }
+}
+
+An important thing to note is that we don't have to implement a default
case for the above switch
statements, because the CommandAPI will only permit valid options of a MultiLiteralArgument
to reach the executor. If the user enters an invalid option, the command doesn't run.
List arguments allows users to provide a list of values. This argument can take on two forms:
+GreedyStringArgument
, so the greedy string argument rule applies - this argument can only be used at the end of an argument list.TextArgument
, so this argument can be used anywhere in an argument list, but its contents must be surrounded with quotes ("
).ListArgumentBuilder
Unlike other arguments, because this argument can be interpreted in various different ways, this argument can only be created using a ListArgumentBuilder
, instead of directly accessing the ListArgument
constructor. The ListArgumentBuilder
loosely follows the following format:
\begin{align} +&\quad\texttt{Create a ListArgumentBuilder} \\ +\rightarrow&\quad\texttt{(Provide the list delimiter)} \\ +\rightarrow&\quad\texttt{Provide the list to pull suggestions from} \\ +\rightarrow&\quad\texttt{Provide the mapper of the list items to a string} \\ +\rightarrow&\quad\texttt{Build the ListArgument} +\end{align}
+First, you have to create a ListArgumentBuilder
parameterized over the type that the list will generate. For example, if you want to create a list of Strings, you would use new ListArgumentBuilder<String>
.
nodeName
parameter represents the name of the node to use for the argument.delimiter
argument specifies the delimiter (separator) to use between entries. If a delimiter is not provided, a space " "
will be used as the delimiter.public ListArgumentBuilder<T>(String nodeName);
+public ListArgumentBuilder<T>(String nodeName, String delimiter);
+
+$$\downarrow$$
+++Allowing duplicates (Optional)
+If you want your users to enter duplicate entries in your list, you can use the
+allowDuplicates
method to set whether duplicates are allowed. By default, duplicates are disabled.When duplicates are enabled, items that have been entered before can be displayed again in the list of suggestions:
++ +ListArgumentBuilder.allowDuplicates(true); +
When duplicates are disabled, items that have already been entered will not appear in the list of suggestions:
++ +ListArgumentBuilder.allowDuplicates(false); +
$$\downarrow$$
+The ListArgument
requires a list that the list argument can pull suggestions and validation from. The ListArgument
does not support values which are not present in the provided list. There are three methods that can be used to provide a list for the ListArgument
:
Providing an immutable list (a list that doesn't change) using the Collection<T>
parameter:
public ListArgumentBuilder withList(Collection<T> list);
+
+Providing a list that is determined when suggestions are being displayed to the user and before the command has been executed using the Supplier<Collection<T>>
parameter:
public ListArgumentBuilder withList(Supplier<Collection<T>> list);
+
+Providing a list that is determined when suggestions are being displayed to the user and before the command has been executed, that also depends on the SuggestionInfo
present when running the command, using the Function<SuggestionInfo<CommandSender>, Collection<T>>
parameter:
public ListArgumentBuilder withList(Function<SuggestionInfo<CommandSender>, Collection<T>> list);
+
+$$\downarrow$$
+In order to display suggestions, the ListArgument
needs to know how to convert a list entry to a string. For example, a Location
may be converted into "x,y,z"
. The ListArgumentBuilder
provides three methods for providing a mapping function:
The withStringMapper()
method converts the object to a string using the object's .toString()
method. If the object is null, this method will populate it with the string "null"
:
public ListArgumentBuilder withStringMapper();
+
+The withMapper()
method requires a function that maps the object to a string:
public ListArgumentBuilder withMapper(Function<T, String> mapper);
+
+The withStringTooltipMapper()
method requires a function that maps the object to an IStringTooltip
. This allows you to also provide hover tooltips for the current item:
public ListArgumentBuilder withStringTooltipMapper(Function<T, IStringTooltip> mapper);
+
+$$\downarrow$$
+ListArgumentBuilder
To finish building the ListArgument
, call the buildGreedy()
or buildText()
method. The buildGreedy()
method will treat the list argument as a greedy string, which means you can only use this list argument at the end of the list of arguments you are declaring for the command. If you use the buildText()
instead, you can use the list argument anywhere (and multiple times), but the list must be surrounded with quotation characters ("
).
public ListArgument<T> buildGreedy();
+public ListArgument<T> buildText();
+
+Say you wanted to give yourself multiple items in a single command. For this command, we'll use the following syntax, which lets you provide the number of items to give, and a list of materials:
+/multigive <amount> <materials>
+
+To do this, we create a command with an IntegerArgument
to specify the amount (between 1 and 64), and a ListArgument
that accepts a list of Material
objects. We use the ListArgumentBuilder
to provide a list of materials as well as a mapping function that converts the material's name to a lowercase string. By default, we use a space delimiter (separator) for arguments in the list.
new CommandAPICommand("multigive")
+ .withArguments(new IntegerArgument("amount", 1, 64))
+ .withArguments(new ListArgumentBuilder<Material>("materials")
+ .withList(List.of(Material.values()))
+ .withMapper(material -> material.name().toLowerCase())
+ .buildGreedy()
+ )
+ .executesPlayer((player, args) -> {
+ int amount = (int) args.get("amount");
+ List<Material> theList = (List<Material>) args.get("materials");
+
+ for (Material item : theList) {
+ player.getInventory().addItem(new ItemStack(item, amount));
+ }
+ })
+ .register();
+
+CommandAPICommand("multigive")
+ .withArguments(IntegerArgument("amount", 1, 64))
+ .withArguments(ListArgumentBuilder<Material>("materials")
+ .withList(Material.values().toList())
+ .withMapper { material -> material.name.lowercase() }
+ .buildGreedy()
+ )
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val amount = args["amount"] as Int
+ val theList = args["materials"] as List<Material>
+
+ for (item in theList) {
+ player.inventory.addItem(ItemStack(item, amount))
+ }
+ })
+ .register()
+
+commandAPICommand("multigive") {
+ integerArgument("amount", 1, 64)
+ argument(ListArgumentBuilder<Material>("materials")
+ .withList(Material.values().toList())
+ .withMapper { material -> material.name.lowercase() }
+ .buildGreedy()
+ )
+ playerExecutor { player, args ->
+ val amount = args["amount"] as Int
+ val theList = args["materials"] as List<Material>
+
+ for (item in theList) {
+ player.inventory.addItem(ItemStack(item, amount))
+ }
+ }
+}
+
+A MapArgument
can be used to provide a map of values. This argument uses an underlying GreedyStringArgument
which means that this argument can only be used at the end of the argument list.
+It returns a LinkedHashMap
object.
MapArgumentBuilder
Similar to the ListArgument
, this argument also uses a builder class to construct it.
\begin{align} +&\quad\texttt{Create a MapArgumentBuilder and possibly provide the delimiter or separator} \\ +\rightarrow&\quad\texttt{Provide the mapper from a string to an object of the provided key type} \\ +\rightarrow&\quad\texttt{Provide the mapper from a string to an object of the provided value type} \\ +\rightarrow&\quad\texttt{Provide the list with keys to pull suggestions from} \\ +\rightarrow&\quad\texttt{Provide the list with values to pull suggestions from} \\ +\rightarrow&\quad\texttt{Build the MapArgument} +\end{align}
+MapArgument
To start building the argument, you first have to construct a MapArgumentBuilder
parameterized over the types the key and the value are supposed to have.
+If you wanted to construct a MapArgument
that returns a LinkedHashMap<String, Integer>
you would construct the MapArgumentBuilder
like this:
new MapArgumentBuilder<String, Integer>
+
+The MapArgumentBuilder
has three possible constructors:
public MapArgumentBuilder<K, V>(String nodeName);
+public MapArgumentBuilder<K, V>(String nodeName, char delimiter);
+public MapArgumentBuilder<K, V>(String nodeName, char delimiter, String separator)
+
+nodeName
: This parameter determines the node name of the MapArgument
delimiter
: This parameter determines the delimiter. This separates a key from a value (key:value
). When not provided, it defaults to a colon (:
)separator
: This parameter determines the separator. This separates one key-value pair from another (key:value key:value
). When not provided, it defaults to a space (
)$$\downarrow$$
+The mapper functions are used to parse the argument when entered. Because a GreedyStringArgument
+returns a String
, we need a way to convert a String
into an object specified by the type parameters.
When providing mappers, you first need to provide the key mapper:
+public MapArgumentBuilder withKeyMapper(StringParser<K>);
+
+You then have to provide the value mapper:
+public MapArgumentBuilder withValueMapper(StringParser<V>);
+
+StringParser
is a functional interface with the following definition:
@FunctionalInterface
+public interface StringParser<T> {
+ /**
+ * A method that turns a String into an object of type T.
+ *
+ * @param s The String to parse
+ * @return The resulting parsed object
+ * @throws WrapperCommandSyntaxException If there is a problem with the syntax of the String that prevents it from being turned into an object of type T.
+ */
+ T parse(String s) throws WrapperCommandSyntaxException;
+}
+
+This signature allows you to throw exceptions using the CommandAPI.fail...
methods if the given String cannot be parsed (see Handling command failures).
$$\downarrow$$
+When providing suggestions you have the choice whether players are allowed to enter any key/value pair or only key/value pairs specified by the MapArgument
.
+To accomplish this the MapArgumentBuilder
provides different methods.
Similar to the mappers, you first have to provide the key suggestions:
+public MapArgumentBuilder withKeyList(List<String> keyList);
+
+public MapArgumentBuilder withoutKeyList();
+
+Next, you have to provide the value suggestions. In addition to the two possibilities presented for the key suggestions, here you also have the possibility to define +whether a value can be written multiple times.
+public MapArgumentBuilder withValueList(List<String> valueList);
+
+public MapArgumentBuilder withValueList(List<String> valueList, boolean allowValueDuplicates);
+
+public MapArgumentBuilder withoutValueList();
+
+public MapArgumentBuilder withoutValueList(boolean allowDuplicates)
+
+If you choose to allow a value to be written multiple times you have to set allowValueDuplicates
to true
. By default, it is set to false
and
+does not allow values to be written multiple times.
$$\downarrow$$
+MapArgument
To finish building the MapArgument
, you have to call the build()
method. This will return a new MapArgument
object.
public MapArgument<K, V> build();
+
+Let's say we want to create a command that we can execute to send multiple players messages without typing the command more than once. For that, we create a command with the following syntax:
+/sendmessage <message>
+
+To implement that, we create a command that uses a MapArgument
and use Player
objects as keys and String
objects as values:
new CommandAPICommand("sendmessage")
+ // Parameter 'delimiter' is missing, delimiter will be a colon
+ // Parameter 'separator' is missing, separator will be a space
+ .withArguments(new MapArgumentBuilder<Player, String>("message")
+
+ // Providing a key mapper to convert a String into a Player
+ .withKeyMapper(Bukkit::getPlayer)
+
+ // Providing a value mapper to leave the message how it was sent
+ .withValueMapper(s -> s)
+
+ // Providing a list of player names to be used as keys
+ .withKeyList(Bukkit.getOnlinePlayers().stream().map(Player::getName).toList())
+
+ // Don't provide a list of values so messages can be chosen without restrictions
+ // Allow duplicates in case the same message should be sent to different players
+ .withoutValueList(true)
+
+ // Build the MapArgument
+ .build()
+ )
+ .executesPlayer((player, args) -> {
+ // The MapArgument returns a LinkedHashMap
+ LinkedHashMap<Player, String> map = (LinkedHashMap<Player, String>) args.get("message");
+
+ // Sending the messages to the players
+ for (Entry<Player, String> messageRecipients : map.entrySet()) {
+ messageRecipients.getKey().sendMessage(messageRecipients.getValue());
+ }
+ })
+ .register();
+
+CommandAPICommand("sendmessage")
+ // Parameter 'delimiter' is missing, delimiter will be a colon
+ // Parameter 'separator' is missing, separator will be a space
+ .withArguments(MapArgumentBuilder<Player, String>("message")
+
+ // Providing a key mapper to convert a String into a Player
+ .withKeyMapper { s: String -> Bukkit.getPlayer(s) }
+
+ // Providing a value mapper to leave the message how it was sent
+ .withValueMapper { s: String -> s }
+
+ // Providing a list of player names to be used as keys
+ .withKeyList(Bukkit.getOnlinePlayers().map { player: Player -> player.name }.toList())
+
+ // Don't provide a list of values so messages can be chosen without restrictions
+ // Allow duplicates in case the same message should be sent to different players
+ .withoutValueList(true)
+
+ // Build the MapArgument
+ .build()
+ )
+ .executesPlayer(PlayerCommandExecutor { _, args ->
+ // The MapArgument returns a LinkedHashMap
+ val map: LinkedHashMap<Player, String> = args["message"] as LinkedHashMap<Player, String>
+
+ // Sending the messages to the players
+ for (messageRecipient in map.keys) {
+ messageRecipient.sendMessage(map[messageRecipient]!!)
+ }
+ })
+ .register()
+
+commandAPICommand("sendmessage") {
+ // Parameter 'delimiter' is missing, delimiter will be a colon
+ // Parameter 'separator' is missing, separator will be a space
+ argument(MapArgumentBuilder<Player, String>("message")
+
+ // Providing a key mapper to convert a String into a Player
+ .withKeyMapper { s: String -> Bukkit.getPlayer(s) }
+
+ // Providing a value mapper to leave the message how it was sent
+ .withValueMapper { s: String -> s }
+
+ // Providing a list of player names to be used as keys
+ .withKeyList(Bukkit.getOnlinePlayers().map { player: Player -> player.name }.toList())
+
+ // Don't provide a list of values so messages can be chosen without restrictions
+ // Allow duplicates in case the same message should be sent to different players
+ .withoutValueList(true)
+
+ // Build the MapArgument
+ .build()
+ )
+ playerExecutor { _, args ->
+ // The MapArgument returns a LinkedHashMap
+ val map: LinkedHashMap<Player, String> = args["message"] as LinkedHashMap<Player, String>
+
+ // Sending the messages to the players
+ for (messageRecipient in map.keys) {
+ messageRecipient.sendMessage(map[messageRecipient]!!)
+ }
+ }
+}
+
+Developer's Note:
+The MapArgument
is very strict and doesn't have room for any errors. A key must always be followed by the delimiter, then a value. One value and the next key must always be separated by the separator. Both keys and values also have the option to be surrounded by quotes.
For example, let's say you are on a server with two players, Player1
and Player2
. We want to send both of them the message Hello, <playerName>!
+To do that, we use the previously declared sendmessage
command like this:
/sendmessage Player1:"Hello, Player1!" Player2:"Hello, Player2!"
+
+A colon is used as the delimiter and a space as the separator because those are the defaults, and neither was specified in the MapArgumentBuilder
constructor. Since the separator was a space, the messages were surrounded by quotes to avoid the spaces inside them from being misinterpreted as the start of the next key-value pair.
Command arguments allows users to provide an executable server command. The CommandArgument
class lets you specify:
Using the CommandArgument
will return a CommandResult
, which contains a Bukkit Command
instance representing the command to be executed, and a String[]
of command arguments.
The CommandResult
record contains the following methods:
public record CommandResult {
+ Command command();
+ String[] args();
+
+ boolean execute(CommandSender target);
+}
+
+These methods can be used to retrieve information about the command that was provided by the user:
+Command command();
+
+command()
returns the Bukkit Command
instance that the user provided. For example, if a player provided /mycommand hello world
, then command()
will represent the /mycommand
command.
String[] args();
+
+args()
returns an array of string argument inputs that were provided to the command. For example, if a player provided /mycommand hello world
, then args()
will be the following:
[ "hello", "world" ]
+
+boolean execute(CommandSender target);
+
+execute(CommandSender)
runs the Bukkit Command
using the arguments contained in the CommandResult
as the given CommandSender
. It returns true if the command dispatch succeeded, and false if it failed. Using this method is equivalent to running the following:
result.command().execute(target, result.command().getLabel(), result.args());
+
+Arbitrary commands let the user enter any command that they have permission to execute. To use arbitrary commands, you just need to use the CommandArgument
normally.
We want to create a /sudo
command which lets you execute a command as another online player.
To do this, we want to use the following command syntax:
+/sudo <target> <command>
+
+In this example, we want to be able to run any arbitrary command, so we will simply use the CommandArgument
on its own (without using suggestions). Using the CommandArgument
generates a CommandResult
and we can use the .command()
and .args()
methods above to access the command and arguments. We can make use of the Command.execute()
method to execute our command and use the target player as the command sender.
new CommandAPICommand("sudo")
+ .withArguments(new PlayerArgument("target"))
+ .withArguments(new CommandArgument("command"))
+ .executes((sender, args) -> {
+ Player target = (Player) args.get("target");
+ CommandResult command = (CommandResult) args.get("command");
+
+ command.execute(target);
+ })
+ .register();
+
+CommandAPICommand("sudo")
+ .withArguments(PlayerArgument("target"))
+ .withArguments(CommandArgument("command"))
+ .executes(CommandExecutor { _, args ->
+ val target = args["target"] as Player
+ val command = args["command"] as CommandResult
+
+ command.execute(target)
+ })
+ .register()
+
+commandAPICommand("sudo") {
+ playerArgument("target")
+ commandArgument("command")
+ anyExecutor { _, args ->
+ val target = args["target"] as Player
+ val command = args["command"] as CommandResult
+
+ command.execute(target)
+ }
+}
+
+Restricted commands allows you to restrict what commands a user is allowed to submit in the CommandArgument
. Commands can be restricted by replacing the CommandArgument
's suggestions using the replaceSuggestions()
method. For better fine-tuning of what commands a user can submit, commands can also be restricted by using suggestion branches.
To demonstrate restricting commands, let's create a command argument that allows players to enter one of the following commands:
+/tp <player> <target>
+/give <player> <item> <amount>
+
+Let's also add a restriction that the player can only use diamonds or dirt for the /give
command, and they can only specify an amount if they selected dirt. Overall, our command argument should allow players to follow this path:
\begin{gather} +\texttt{(start)}\\ +\swarrow\hspace{2cm}\searrow\\ +\swarrow\hspace{3.4cm}\searrow\\ +\texttt{tp}\hspace{4cm}\texttt{give}\\ +\swarrow\hspace{6cm}\searrow\\ +\texttt{player}\hspace{6cm}\texttt{player}\\ +\swarrow\hspace{7cm}\swarrow\hspace{2cm}\searrow\\ +\texttt{target}\hspace{5cm}\texttt{diamond}\hspace{3cm}\texttt{dirt}\\ +\hspace{6.7cm}\texttt{minecraft:diamond}\hspace{3cm}\texttt{minecraft:dirt}\\ +\hspace{7.5cm}\hspace{4cm}\downarrow\\ +\hspace{7.5cm}\hspace{4cm}\texttt{(amount)}\\ +\end{gather}
+In our diagram above, we have two main branches: /tp
and /give
. The /tp
branch has player
followed by target
, and the /give
branch has player
and then that branches off into two new sections.
We can implement our /tp
branch using the SuggestionsBranch.suggest()
method, then provide argument suggestions for our options. In this case, we have tp
and then a list of online players. We include the list of online players twice, because we need suggestions for <player>
as well as <target>
:
SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("tp"),
+ ArgumentSuggestions.strings(info -> Bukkit.getOnlinePlayers().stream().map(Player::getName).toArray(String[]::new)),
+ ArgumentSuggestions.strings(info -> Bukkit.getOnlinePlayers().stream().map(Player::getName).toArray(String[]::new))
+)
+
+SuggestionsBranch.suggest<CommandSender>(
+ ArgumentSuggestions.strings("tp"),
+ ArgumentSuggestions.strings { _ -> Bukkit.getOnlinePlayers().map{ it.name }.toTypedArray() },
+ ArgumentSuggestions.strings { _ -> Bukkit.getOnlinePlayers().map{ it.name }.toTypedArray() }
+)
+
+For the /give
branch, we can use a similar thing, but we need to tell the CommandArgument that the /give
command branches into "diamond" and "dirt" suggestions. We can do this by using the .branch()
method to add a new nested list of suggestions:
SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("give"),
+ ArgumentSuggestions.strings(info -> Bukkit.getOnlinePlayers().stream().map(Player::getName).toArray(String[]::new))
+).branch(
+ SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("diamond", "minecraft:diamond"),
+ ArgumentSuggestions.empty()
+ ),
+ SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("dirt", "minecraft:dirt"),
+ null,
+ ArgumentSuggestions.empty()
+ )
+)
+
+SuggestionsBranch.suggest<CommandSender>(
+ ArgumentSuggestions.strings("give"),
+ ArgumentSuggestions.strings { _ -> Bukkit.getOnlinePlayers().map{ it.name }.toTypedArray() }
+).branch(
+ SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("diamond", "minecraft:diamond"),
+ ArgumentSuggestions.empty()
+ ),
+ SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("dirt", "minecraft:dirt"),
+ null,
+ ArgumentSuggestions.empty()
+ )
+)
+
+Adding everything together, we get this fully completed CommandArgument:
+new CommandArgument("command")
+ .branchSuggestions(
+ SuggestionsBranch.<CommandSender>suggest(
+ ArgumentSuggestions.strings("give"),
+ ArgumentSuggestions.strings(info -> Bukkit.getOnlinePlayers().stream().map(Player::getName).toArray(String[]::new))
+ ).branch(
+ SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("diamond", "minecraft:diamond"),
+ ArgumentSuggestions.empty()
+ ),
+ SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("dirt", "minecraft:dirt"),
+ null,
+ ArgumentSuggestions.empty()
+ )
+ ),
+ SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("tp"),
+ ArgumentSuggestions.strings(info -> Bukkit.getOnlinePlayers().stream().map(Player::getName).toArray(String[]::new)),
+ ArgumentSuggestions.strings(info -> Bukkit.getOnlinePlayers().stream().map(Player::getName).toArray(String[]::new))
+ )
+ );
+
+CommandArgument("command")
+ .branchSuggestions(
+ SuggestionsBranch.suggest<CommandSender>(
+ ArgumentSuggestions.strings("give"),
+ ArgumentSuggestions.strings { _ -> Bukkit.getOnlinePlayers().map{ it.name }.toTypedArray() }
+ ).branch(
+ SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("diamond", "minecraft:diamond"),
+ ArgumentSuggestions.empty()
+ ),
+ SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("dirt", "minecraft:dirt"),
+ null,
+ ArgumentSuggestions.empty()
+ )
+ ),
+ SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("tp"),
+ ArgumentSuggestions.strings { _ -> Bukkit.getOnlinePlayers().map{ it.name }.toTypedArray() },
+ ArgumentSuggestions.strings { _ -> Bukkit.getOnlinePlayers().map{ it.name }.toTypedArray() }
+ )
+ )
+
+In the above example about restricted commands, we used null
and ArgumentSuggestions.empty()
in our SuggestionsBranch.suggest()
method. These special suggestions have specific effects when used in suggestions for the CommandArgument
.
Null suggestions ensure that the suggestions at the current position will not be overridden. In the case of the CommandArgument
, this means that the default command suggestions will be provided. For example, if we have the following null
entry in our suggestions, users are allowed to enter a value if they choose to do so, meaning that the examples below are all valid:
SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("give"),
+ null,
+ ArgumentSuggestions.empty()
+)
+
+/give dirt
+/give diamond
+/give apple
+
+Ending the command argument with nothing is also equivalent to using null
, for example the following suggestion branch allows any of the following commands:
SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("give"),
+ ArgumentSuggestions.strings("dirt", "minecraft:dirt")
+)
+
+/give dirt
+/give dirt 10
+/give dirt 10 name:Hello
+
+Empty suggestions that are provided using ArgumentSuggestions.empty()
tell the CommandArgument
to stop accepting further suggestions. This "ends" the command. Using the following example, this allows the user to enter /give diamond
and only /give diamond
- users cannot enter any other commands.
SuggestionsBranch.suggest(
+ ArgumentSuggestions.strings("give"),
+ ArgumentSuggestions.strings("diamond", "minecraft:diamond"),
+ ArgumentSuggestions.empty()
+)
+
+These commands are valid:
+/give diamond
+/give minecraft:diamond
+
+These commands are not valid:
+/give
+/give diamond 10
+
+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 a LocationArgument
, 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.
+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 a CustomArgument<Player, ...>
, then when parsing the arguments for the command, you would cast it to a Player
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 a StringArgument
, you'd have CustomArgument<..., String>
.
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();
+ CommandArguments 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 a Player
, but can also be a console command sender if using a Paper server.
CommandArguments previousArgs();
+
+previousArgs()
represents the 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, the input()
would return "hello"
.
B currentInput();
+
+currentInput()
represents the current input, as parsed by the base argument. For example, if your base argument was an IntegerArgument
, the return type of currentInput()
would be an int
.
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("world")).getSpawnLocation());
+ })
+ .register();
+
+CommandAPICommand("tpworld")
+ .withArguments(worldArgument("world"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ player.teleport((args["world"] as World).spawnLocation)
+ })
+ .register()
+
+commandAPICommand("tpworld") {
+ worldArgument("world") // This method is actually also built into the Kotlin DSL
+ playerExecutor { player, args ->
+ player.teleport((args["world"] 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.
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 |
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();
+
+The CommandAPI has support to use Minecraft's functions within your plugins. This is handled by using a class provided by the CommandAPI called FunctionWrapper
, which allows you to execute functions. The CommandAPI also provides support to let you run your own commands within Minecraft function files.
++Developer's Note:
+Minecraft 1.16+ change the way that datapacks are loaded on the server, so that they load before plugins are enabled. This means that non-vanilla commands that are declared in functions and tags will be detected as invalid, causing the server to throw a lot of errors at the very start.
+The CommandAPI reloads datapacks once the server has finished loading using all declared commands, therefore the error messages at the start of the server can be ignored.
+
In order to use a command from your plugin in a .mcfunction
file, you must register your command in your plugin's onLoad()
method, instead of the onEnable()
method. Failure to do so will not allow the command to be registered for Minecraft functions, causing the function file to fail to load during the server startup phase.
++Developer's Note:
+In short, if you want to register a command which can be used in Minecraft functions, register it in your plugin's
+onLoad()
method.
Say we have a command /killall
that simply kills all entities in all worlds on the server. If we were to register this in our onLoad()
method, this would allow us to use the /killall
command in Minecraft functions and tags.
public class Main extends JavaPlugin {
+
+ @Override
+ public void onLoad() {
+ // Commands which will be used in Minecraft functions are registered here
+
+ new CommandAPICommand("killall")
+ .executes((sender, args) -> {
+ // Kills all enemies in all worlds
+ Bukkit.getWorlds().forEach(w -> w.getLivingEntities().forEach(e -> e.setHealth(0)));
+ })
+ .register();
+ }
+
+ @Override
+ public void onEnable() {
+ // Register all other commands here
+ }
+}
+
+class Main : JavaPlugin() {
+
+ override fun onLoad() {
+ // Commands which will be used in Minecraft functions are registered here
+
+ CommandAPICommand("killall")
+ .executes(CommandExecutor { _, _ ->
+ // Kills all enemies in all worlds
+ Bukkit.getWorlds().forEach { world -> world.livingEntities.forEach { entity -> entity.health = 0.0 } }
+ })
+ .register()
+ }
+
+ override fun onEnable() {
+ // Register all other commands here
+ }
+
+}
+
+++Developer's Note:
+Most developers can completely skip this section. This is for those that are unfamiliar with functions and tags and is less about how the CommandAPI works.
+
This section explains how functions are declared and set up for use in a Minecraft server. This is ideal for server owners who've never set up functions, or developers (like the Command API's creator) that has no idea how to set this sort of thing up.
+Functions are text files (with the .mcfunction
extension) which contains lists of functions which are executed one after another. Each line of the file is a valid Minecraft command. Say we have text.mcfunction
:
killall
+say Killed all living entities on the server
+
+This will run the custom command killall (as declared in Example - Registering command for use in a function), and then broadcast a message to all players stating that all entities were killed.
+Tags are json files which contain a list of functions. Tags let you run multiple functions at a time. Say we have a tag called mytag.json
:
{
+ "values": [
+ "mycustomnamespace:test",
+ "mycustomnamespace:test2"
+ ]
+}
+
+This will run the function test
and the function test2
, which are in the namespace mycustomnamespace
.
The following hierarchy explains where functions and tags go. In this diagram, the two functions test
and test2
are in a directory called functions
. There is also a tag called mytag
which is placed in the tags
directory under functions
. These are all under the namespace called mycustomnamespace
server/
+├── world/
+│ ├── advancements/
+│ ├── data/
+│ ├── datapacks/
+│ │ └── bukkit/
+│ │ ├── pack.mcmeta
+│ │ └── data/
+│ │ └── mycustomnamespace/
+│ │ ├── functions/
+│ │ │ ├── test.mcfunction
+│ │ │ └── test2.mcfunction
+│ │ └── tags/
+│ │ └── functions/
+│ │ └── mytag.json
+│ └── ...
+├── world_nether/
+├── world_the_end/
+├── ...
+└── spigot.jar
+
+To execute the test
function, you would run the following command:
/function mycustomnamespace:test
+
+To execute the mytag
tag, you would run the following command:
/function #mycustomnamespace:mytag
+
+To represent Minecraft functions and tags, the CommandAPI uses the SimpleFunctionWrapper
class. Simply put, this class represents one Minecraft function, which are defined in .mcfunction
files.
++Developer's Note
+The
+SimpleFunctionWrapper
class represents a Minecraft function. In order to represent a Minecraft "tag", which is a collection of Minecraft functions, the CommandAPI simply uses aSimpleFunctionWrapper[]
.
The SimpleFunctionWrapper
class has the following methods:
class SimpleFunctionWrapper implements Keyed {
+
+ // Methods that creates SimpleFunctionWrapper instances
+ static SimpleFunctionWrapper getFunction(NamespacedKey key);
+ static SimpleFunctionWrapper[] getTag(NamespacedKey key);
+
+ // Methods that query the Minecraft server
+ static Set<NamespacedKey> getFunctions();
+ static Set<NamespacedKey> getTags();
+
+ // Methods for using the SimpleFunctionWrapper
+ int run(CommandSender sender);
+
+ // Utility functions
+ String[] getCommands();
+ NamespacedKey getKey();
+}
+
+The getFunction(NamespacedKey)
function is used to get a function that has been declared in a datapack and is loaded on the server.
The getTag(NamespacedKey)
function is used to get a Tag that has been declared in a datapack and is loaded on the server. This returns a SimpleFunctionWrapper[]
, since a tag is simply an ordered collection of functions. When using this method, the #
symbol which is typically used at the start of the tag's name is not needed.
The methods getFunctions()
and getTags()
simply return a set of NamespacedKey
objects which are the names of functions or tags that have been declared by all datapacks on the server.
As of CommandAPI 9.3.0 (compatible with Minecraft versions 1.20.3 and 1.20.4), calling run(CommandSender)
will always return a value of 1
, regardless of whether the command succeeds, fails, or returns a result.
This method simply runs the current SimpleFunctionWrapper
as the provided command sender. The method will return a numerical result value, stating whether it succeeds or returns a result. This is documented in more detail here and here. For example:
As of CommandAPI 9.3.0 (compatible with Minecraft versions 1.20.3 and 1.20.4), calling getCommands()
will always return an empty String[]
. At the time of writing, it is not possible to extract the function command list in 1.20.3+.
The getCommands()
method returns a String[]
that contains the list of commands that the Minecraft function "holds". In other words, running this Minecraft function is as simple as iterating through its commands and running them in order. The commands that this String[]
holds are the raw strings that this function represents - in other words, it can include things such as @p
and ~ ~ ~
instead of "filled in" values.
The CommandAPI includes the FunctionWrapper
class which is a wrapper for Minecraft's functions. It allows you to execute the commands that are represented by the respective .mcfunction
file.
The FunctionWrapper
class is an extension of the SimpleFunctionWrapper
class. It is a SimpleFunctionWrapper
which has been constructed from an existing command sender when a command is used. This means that the command sender has already been "baked into" the FunctionWrapper
object, allowing you to run it without having to provide a command sender.
The FunctionWrapper
class has the following methods:
class FunctionWrapper extends SimpleFunctionWrapper {
+
+ // Methods specific to this class
+ int run();
+ int runAs(Entity e);
+
+ // Methods inherited from SimpleFunctionWrapper
+ static SimpleFunctionWrapper getFunction(NamespacedKey key);
+ static SimpleFunctionWrapper[] getTag(NamespacedKey key);
+ static Set<NamespacedKey> getFunctions();
+ static Set<NamespacedKey> getTags();
+ int run(CommandSender sender);
+ String[] getCommands();
+ NamespacedKey getKey();
+}
+
+These methods allow you to interact with the Minecraft function that this class wraps.
+run()
As of CommandAPI 9.3.0 (compatible with Minecraft versions 1.20.3 and 1.20.4), calling run()
will always return a value of 1
, regardless of whether the command succeeds, fails, or returns a result.
The run()
method runs the function. The command executor that runs this function is the command executor that was used to retrieve it. For example, if a player in-game populated this argument, then the player will be filled in for @p
and the player's location would be used for things such as ~ ~ ~
:
new CommandAPICommand("runfunc")
+ .withArguments(new FunctionArgument("function"))
+ .executes((sender, args) -> {
+ FunctionWrapper[] functions = (FunctionWrapper[]) args.get("function");
+ for (FunctionWrapper function : functions) {
+ function.run(); // The command executor in this case is 'sender'
+ }
+ })
+ .register();
+
+CommandAPICommand("runfunc")
+ .withArguments(FunctionArgument("function"))
+ .executes(CommandExecutor { _, args ->
+ val functions = args["function"] as Array<FunctionWrapper>
+ for (function in functions) {
+ function.run() // The command executor in this case is 'sender'
+ }
+ })
+ .register()
+
+runAs(Entity)
As of CommandAPI 9.3.0 (compatible with Minecraft versions 1.20.3 and 1.20.4), calling runAs(Entity)
will always return a value of 1
, regardless of whether the command succeeds, fails, or returns a result.
The runAs(Entity)
is the same as the run()
method, but it allows you to change the command executor to another entity.
The FunctionArgument
class is used to represent a function or a tag in Minecraft. When retrieving an instance of the argument, it will return a FunctionWrapper[]
, where each FunctionWrapper
consists of a Minecraft function.
Therefore, if a user supplies a single function, the FunctionWrapper[]
will be of size 1, and if the user supplies a tag which can consist of multiple functions, the FunctionWrapper[]
will consist of the array of functions as declared by that tag.
Since it's a little difficult to demonstrate a custom use for the FunctionArgument
, we will show how you can implement Vanilla Minecraft's /function
command. In this example, we want a command that uses the following syntax:
/runfunction <function>
+
+When provided with a function, it will execute that function. If instead a tag is provided, it will execute that tag (i.e. execute all functions declared in that tag).
+new CommandAPICommand("runfunction")
+ .withArguments(new FunctionArgument("function"))
+ .executes((sender, args) -> {
+ FunctionWrapper[] functions = (FunctionWrapper[]) args.get("function");
+
+ // Run all functions in our FunctionWrapper[]
+ for (FunctionWrapper function : functions) {
+ function.run();
+ }
+ })
+ .register();
+
+CommandAPICommand("runfunction")
+ .withArguments(FunctionArgument("function"))
+ .executes(CommandExecutor { _, args ->
+ val functions = args["function"] as Array<FunctionWrapper>
+
+ // Run all functions in our FunctionWrapper[]
+ for (function in functions) {
+ function.run()
+ }
+ })
+ .register()
+
+commandAPICommand("runfunction") {
+ functionArgument("function")
+ anyExecutor { _, args ->
+ val functions = args["function"] as Array<FunctionWrapper>
+
+ // Run all functions in our FunctionWrapper[]
+ for (function in functions) {
+ function.run()
+ }
+ }
+}
+
+Permissions let you control which players are allowed to execute which commands. This is handled using the CommandPermission
class, which has the following uses:
Permission | What it does |
---|---|
CommandPermission.OP | Requires OP to execute the command |
CommandPermission.NONE | Anyone can execute the command |
CommandPermission.fromString("my.permission") | Requires a specific permission node to execute the command |
In addition to the CommandPermission
class, there are two different ways to assign permissions (compared to the simple CommandSender.hasPermission()
method that is provided by Bukkit), by using the withPermission
method for arguments or for commands.
The withPermission
method can take two values:
CommandPermission
, which represents a permission such as OP
or NONE
String
, which will be converted automatically to a CommandPermission
using CommandPermission.fromString()
To add a permission to a command, you can use the withPermission(CommandPermission)
or withPermission(String)
method when declaring a command.
Say we created a command /god
that sets a player as being invulnerable. Since this is a pretty non-survival command, we want to restrict who can run this command. As such, we want our player to have the permission command.god
in order to run this command. To do this, we simply use the withPermission(CommandPermission)
method from our command builder:
// Register the /god command with the permission node "command.god"
+new CommandAPICommand("god")
+ .withPermission(CommandPermission.fromString("command.god"))
+ .executesPlayer((player, args) -> {
+ player.setInvulnerable(true);
+ player.sendMessage("God mode enabled");
+ })
+ .register();
+
+// Register the /god command with the permission node "command.god"
+CommandAPICommand("god")
+ .withPermission(CommandPermission.fromString("command.god"))
+ .executesPlayer(PlayerCommandExecutor { player, _ ->
+ player.isInvulnerable = true
+ })
+ .register()
+
+As stated above, it is possible to assign a permission using a String instead of using CommandPermission.fromString()
:
// Register the /god command with the permission node "command.god", without creating a CommandPermission
+new CommandAPICommand("god")
+ .withPermission("command.god")
+ .executesPlayer((player, args) -> {
+ player.setInvulnerable(true);
+ player.sendMessage("God mode enabled");
+ })
+ .register();
+
+// Register the /god command with the permission node "command.god", without creating a CommandPermission
+CommandAPICommand("god")
+ .withPermission("command.god")
+ .executesPlayer(PlayerCommandExecutor { player, _ ->
+ player.isInvulnerable = true
+ })
+ .register()
+
+For further fine-tuning of permission management, the CommandAPI allows you to add permissions to individual arguments. This prevents the user from executing a command with a specific argument if they do not have a specific permission.
+This is done by using the withPermission(CommandPermission)
method at the end of an argument.
If a player does not have the required permission:
+For example, say we're registering a command /kill
:
/kill - Kills yourself
+/kill <target> - Kills a target player
+
+We first declare the command as normal. Nothing fancy is going on here:
+// Register /kill command normally. Since no permissions are applied, anyone can run this command
+new CommandAPICommand("kill")
+ .executesPlayer((player, args) -> {
+ player.setHealth(0);
+ })
+ .register();
+
+// Register /kill command normally. Since no permissions are applied, anyone can run this command
+CommandAPICommand("kill")
+ .executesPlayer(PlayerCommandExecutor { player, _ ->
+ player.health = 0.0
+ })
+ .register()
+
+Now we declare our command with arguments. We use a PlayerArgument
and apply the permission to the argument. After that, we register our command as normal:
// Adds the OP permission to the "target" argument. The sender requires OP to execute /kill <target>
+new CommandAPICommand("kill")
+ .withArguments(new PlayerArgument("target").withPermission(CommandPermission.OP))
+ .executesPlayer((player, args) -> {
+ ((Player) args.get("target")).setHealth(0);
+ })
+ .register();
+
+// Adds the OP permission to the "target" argument. The sender requires OP to execute /kill <target>
+CommandAPICommand("kill")
+ .withArguments(PlayerArgument("target").withPermission(CommandPermission.OP))
+ .executesPlayer(PlayerCommandExecutor { _, args ->
+ (args["target"] as Player).health = 0.0
+ })
+ .register()
+
+++Developer's Note:
+As you can see, there are multiple ways of applying permissions to commands with arguments. In the
+/god
command shown above, the permission was applied to the whole command. In the/kill
command shown above, the permission was applied to the argument.There's not really much difference between the two methods, but I personally would use argument permissions as it has greater control over arguments.
+
Child-based permissions allow you to group permissions together.
+We achieve this by laying out our permission groups in the plugin.yml
file which Bukkit registers as valid permissions.
+When the CommandAPI checks if our player has a permission, Bukkit considers if they have the child of a permission as well.
+This not only keeps permissions easier to manage, it also makes your code cleaner and gives you a nice place to lay out all of your permissions,
+detailing what they do and what other permissions inherit them.
For example, say we're registering a command /economy
:
/economy - shows your own balance | economy.self
+/economy <target> - shows you another players balance | economy.other
+/economy give <target> <amount> - gives the target a set amount of money | economy.admin.give
+/economy reset <target> <amount> - resets the targets balance | economy.admin.reset
+
+We first declare the command as normal. Nothing fancy is going on here:
+// /economy - requires the permission "economy.self" to execute
+new CommandAPICommand("economy")
+ .withPermission("economy.self") // The important part of this example
+ .executesPlayer((player, args) -> {
+ // send the executor their own balance here.
+ })
+ .register();
+
+// /economy <target> - requires the permission "economy.other" to execute
+new CommandAPICommand("economy")
+ .withPermission("economy.other") // The important part of this example
+ .withArguments(new PlayerArgument("target"))
+ .executesPlayer((player, args) -> {
+ Player target = (Player) args.get("target");
+
+ // Send a message to the executor with the target's balance
+ player.sendMessage(target.getName() + "'s balance: " + Economy.getBalance(target));
+ })
+ .register();
+
+// /economy give <target> <amount> - requires the permission "economy.admin.give" to execute
+new CommandAPICommand("economy")
+ .withPermission("economy.admin.give") // The important part of this example
+ .withArguments(new PlayerArgument("target"))
+ .withArguments(new DoubleArgument("amount"))
+ .executesPlayer((player, args) -> {
+ Player target = (Player) args.get("target");
+ double amount = (Double) args.get("amount");
+
+ // Update the target player's balance
+ Economy.updateBalance(target, amount);
+ })
+ .register();
+
+// /economy reset <target> - requires the permission "economy.admin.reset" to execute
+new CommandAPICommand("economy")
+ .withPermission("economy.admin.reset") // The important part of this example
+ .withArguments(new PlayerArgument("target"))
+ .executesPlayer((player, args) -> {
+ Player target = (Player) args.get("target");
+
+ // Reset target balance here
+ Economy.resetBalance(target);
+ })
+ .register();
+
+// /economy - requires the permission "economy.self" to exectue
+CommandAPICommand("economy")
+ .withPermission("economy.self")
+ .executesPlayer(PlayerCommandExecutor { player, _ ->
+ // send the executor their own balance here.
+ })
+ .register()
+
+// /economy <target> - requires the permission "economy.other" to execute
+CommandAPICommand("economy")
+ .withPermission("economy.other") // The important part of this example
+ .withArguments(PlayerArgument("target"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val target = args["target"] as Player
+ // send the executor the targets balance here.
+ })
+ .register()
+
+// /economy give <target> <amount> - requires the permission "economy.admin.give" to execute
+CommandAPICommand("economy")
+ .withPermission("economy.admin.give") // The important part of this example
+ .withArguments(PlayerArgument("target"))
+ .withArguments(DoubleArgument("amount"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val target = args["target"] as Player
+ val amount = args["amount"] as Double
+ // update the targets balance here
+ })
+ .register()
+
+// /economy reset <target> - requires the permission "economy.admin.reset" to execute
+CommandAPICommand("economy")
+ .withPermission("economy.admin.reset") // The important part of this example
+ .withArguments(PlayerArgument("target"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val target = args["target"] as Player
+ // reset the targets balance here
+ })
+ .register()
+
+In our plugin.yml we can also set up our permissions for example...
+permissions:
+ economy.*:
+ description: Gives the user full access to the economy commands
+ children:
+ economy.other: true
+ economy.admin.*: true
+
+ economy.self:
+ description: Allows the user to view their own balance
+ economy.other:
+ description: Allows the user to another players balance
+ children:
+ economy.self: true
+
+ economy.admin.*:
+ description: Gives the user access to all of the admin commands
+ children:
+ economy.admin.give: true
+ economy.admin.reset: true
+ economy.admin.give:
+ description: Gives the user access to /economy give <target> <amount>
+ economy.admin.reset:
+ description: Gives the user access to /economy reset <target>
+
+This setup of children allows us to give a player less permissions, but have them access more features.
+Since economy.*
inherits economy.admin.*
which inherits economy.admin.give
, a player with the permission economy.*
will be able to execute /economy give <target> <amount>
without them directly having the economy.admin.give
permission node.
+This also works with economy.other
, if a player has economy.other
they will inherit economy
.
Requirements is a feature that allows you to put a constraint on commands and arguments. Similar to permissions, a requirement is something that must be fulfilled in order to use a given command or argument.
+This section is broken up into four parts:
+Please don't skip the section on updating requirements - the last section is necessary to get requirements to work as you'd want!
+To add a requirement to a command, similar to adding permissions to commands, use the withRequirement
method:
CommandAPICommand withRequirement(Predicate<CommandSender> sender);
+
+The withRequirement
method requires a predicate that determines if the sender is able to run the command - if the predicate is satisfied, then the command sender will be able to execute that command.
Say we have a perks-based command system that depends on a player's level. For example, if a player has over 30 levels of experience, they would then be able to run a command that lets them repair the item in their hand in exchange for 30 levels. As such, we'll use the following command syntax:
+/repair
+
+We want to put a requirement on this command that the player needs to have at least 30 levels of experience in order to run the command - if the player has less than 30 levels, the player should not be able to run the command. The easiest way to make the player not able to run the command is to literally tell the user that the command doesn't exist. That's what requirements do in the CommandAPI:
+new CommandAPICommand("repair")
+ .withRequirement(sender -> ((Player) sender).getLevel() >= 30)
+ .executesPlayer((player, args) -> {
+
+ // Repair the item back to full durability
+ ItemStack is = player.getInventory().getItemInMainHand();
+ ItemMeta itemMeta = is.getItemMeta();
+ if (itemMeta instanceof Damageable damageable) {
+ damageable.setDamage(0);
+ is.setItemMeta(itemMeta);
+ }
+
+ // Subtract 30 levels
+ player.setLevel(player.getLevel() - 30);
+ })
+ .register();
+
+CommandAPICommand("repair")
+ .withRequirement { (it as Player).level >= 30 }
+ .executesPlayer(PlayerCommandExecutor { player, _ ->
+
+ // Repair the item back to full durability
+ val item = player.inventory.itemInMainHand
+ val itemMeta = item.itemMeta
+ if (itemMeta is Damageable) {
+ itemMeta.setDamage(0)
+ item.setItemMeta(itemMeta)
+ }
+
+ // Subtract 30 levels
+ player.setLevel(player.level - 30)
+ })
+ .register()
+
+It's important to note that in this example, we case the sender
to a player
for the requirement method. We know that the sender is definitely a player because we use executesPlayer()
, which ensures that this is the case. Now that we've got this, we need to make sure we update the player's requirements when their exp changes. This is covered in more detail in the section about updating requirements below.
In a similar way that you can restrict certain arguments by adding permissions to them, you can restrict them by using arbitrary predicates by using the withRequirement
method on the arguments themselves.
Let's say that we're working on a plugin that has a system to form groups of players called "parties". If you are not already in a party, you can create one of your own and if you are in a party, you can teleport to any other member in your party.
+For this example, we'll use the following command syntax:
+/party create <partyName>
+/party tp <player>
+
+To represent our party in code, we'll use a simple Map
called partyMembers
which maps the player's UUID to the name of their registered party:
Map<UUID, String> partyMembers = new HashMap<>();
+
+val partyMembers = mutableMapOf<UUID, String>()
+
+To begin with, let's create the /party create <partyName>
command. First, we must declare our arguments:
List<Argument<?>> arguments = new ArrayList<>();
+
+// The "create" literal, with a requirement that a player must have a party
+arguments.add(new LiteralArgument("create")
+ .withRequirement(sender -> !partyMembers.containsKey(((Player) sender).getUniqueId()))
+);
+
+arguments.add(new StringArgument("partyName"));
+
+var arguments = mutableListOf<Argument<*>>()
+
+// The "create" literal, with a requirement that a player must have a party
+arguments.add(LiteralArgument("create")
+ .withRequirement { !partyMembers.containsKey((it as Player).uniqueId) }
+)
+
+arguments.add(StringArgument("partyName"))
+
+In this argument declaration, we put a requirement on the literal create
, where the player does not have a party. In other words, if the player does not have a party, they are allowed to run /party create <partyName>
. If a player already has a party, then they won't be allowed to run this command.
Now that we've declared our arguments, we can now declare our main command /party create <partyName>
. We populate it with the arguments, and we create an entry in our partyMembers
with the player's UUID and the name of the party that they created. Since this updates the requirements of the player, we'll have to make sure we update it (which is covered in more detail in the section about updating requirements below) - until then, I'll omit this from the code:
new CommandAPICommand("party")
+ .withArguments(arguments)
+ .executesPlayer((player, args) -> {
+
+ // Get the name of the party to create
+ String partyName = (String) args.get("partyName");
+
+ partyMembers.put(player.getUniqueId(), partyName);
+ })
+ .register();
+
+CommandAPICommand("party")
+ .withArguments(*arguments.toTypedArray())
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+
+ // Get the name of the party to create
+ val partyName = args["partyName"] as String
+
+ partyMembers[player.uniqueId] = partyName
+ })
+ .register()
+
+So now we've added the ability to create a party if we're not already in it. Now we need to implement our party tp <player>
command. Again, we must start by declaring our arguments:
arguments = new ArrayList<>();
+arguments.add(new LiteralArgument("tp")
+ .withRequirement(sender -> partyMembers.containsKey(((Player) sender).getUniqueId()))
+);
+
+arguments.add(new PlayerArgument("player")
+ .replaceSafeSuggestions(SafeSuggestions.suggest(info -> {
+
+ // Store the list of party members to teleport to
+ List<Player> playersToTeleportTo = new ArrayList<>();
+
+ String partyName = partyMembers.get(((Player) info.sender()).getUniqueId());
+
+ // Find the party members
+ for (Entry<UUID, String> entry : partyMembers.entrySet()) {
+
+ // Ignore yourself
+ if (entry.getKey().equals(((Player) info.sender()).getUniqueId())) {
+ continue;
+ } else {
+ // If the party member is in the same party as you
+ if (entry.getValue().equals(partyName)) {
+ Player target = Bukkit.getPlayer(entry.getKey());
+ if (target.isOnline()) {
+ // Add them if they are online
+ playersToTeleportTo.add(target);
+ }
+ }
+ }
+ }
+
+ return playersToTeleportTo.toArray(new Player[0]);
+ })));
+
+arguments = mutableListOf<Argument<*>>()
+arguments.add(LiteralArgument("tp")
+ .withRequirement { partyMembers.containsKey((it as Player).uniqueId) })
+
+arguments.add(PlayerArgument("player")
+ .replaceSafeSuggestions(SafeSuggestions.suggest { info ->
+
+ // Store the list of party members to teleport to
+ val playersToTeleportTo = mutableListOf<Player>()
+
+ val partyName = partyMembers[(info.sender() as Player).uniqueId]
+
+ // Find the party members
+ for ((uuid, party) in partyMembers) {
+
+ // Ignore yourself
+ if (uuid == (info.sender() as Player).uniqueId) {
+ continue
+ } else {
+ // If the party member is in the same party as you
+ if (party == partyName) {
+ val target = Bukkit.getPlayer(uuid)!!
+ if (target.isOnline) {
+ // Add them if they are online
+ playersToTeleportTo.add(target)
+ }
+ }
+ }
+ }
+
+ playersToTeleportTo.toTypedArray()
+ }))
+
+Notice something here? There's some code repetition for the withRequirement
method - this is the same predicate that we used earlier, except we remove the negation. If you are interested, you can view the section Predicate tips for a method to improve code reuse.
Once the arguments have been declared, we can now implement our party teleportation command:
+new CommandAPICommand("party")
+ .withArguments(arguments)
+ .executesPlayer((player, args) -> {
+ Player target = (Player) args.get("player");
+ player.teleport(target);
+ })
+ .register();
+
+CommandAPICommand("party")
+ .withArguments(arguments)
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val target = args["player"] as Player
+ player.teleport(target)
+ })
+ .register()
+
+What's important to note in this example is that if you spend the time to set up the arguments properly, it severely decreases the amount of code required to write your command. This makes the commands you declare easier to understand and follow and you don't end up having to put all of these checks in the body of your command executor.
+Finally, the part you've all been waiting for - how to update requirements. With the way requirements work, they need to be updated manually. To illustrate why this is the case, I'll explain using the example of the /repair command:
+When a player joins the game, the server tells the client the list of all commands that the client can run (don't worry, this is completely normal, as declared here). Let's say that the player has joined and has less than 30 levels.
+When a player has less than 30 levels, they are unable to execute the /repair
command, because the list of commands that the server sent to the client did not contain the /repair
command. Eventually, the player will fight some mobs or mine some ores and eventually will reach 30 levels. Despite this, the player's client doesn't actually know that they're now able to use the /repair
command until the server tells them. As such, the server needs to somehow update the requirements that a player has so a player knows they can run the command.
The CommandAPI handles this in a very simple method call:
+CommandAPI.updateRequirements(player);
+
+Developer's Note:
+The CommandAPI.updateRequirements(player);
method can be used anywhere, except for the withRequirement
method. Using it inside this method will crash the server. This is by design - just make sure you don't use it within the withRequirement
method and everything will be fine!
To illustrate how to use this, we'll go over the two examples above:
+In the example of requirements with the /repair command, we needed to ensure that the player's requirements update when their experience changes. To do this, we'll simply use a normal event to check this:
+@EventHandler
+public void onExpChange(PlayerExpChangeEvent event) {
+ CommandAPI.updateRequirements(event.getPlayer());
+}
+
+And of course, you have to ensure that this event is registered in your onEnable()
method:
@Override
+public void onEnable() {
+ getServer().getPluginManager().registerEvents(this, this);
+}
+
+++Developer's Note:
+I'm assuming you already know how to register events and don't need me to go into great detail how to do so, take the code above with a pinch of salt - I know how much everyone likes to divide their event handlers and listeners to organise their code.
+
In the example for a party creation, we declared two commands:
+/party create <partyName>
+/party tp <player>
+
+When a player creates a new party, we need to ensure that their requirements are updated when they create the party. As such, we simply add this to our party creation command executor:
+new CommandAPICommand("party")
+ .withArguments(arguments)
+ .executesPlayer((player, args) -> {
+
+ // Get the name of the party to create
+ String partyName = (String) args.get("partyName");
+
+ partyMembers.put(player.getUniqueId(), partyName);
+
+ CommandAPI.updateRequirements(player);
+ })
+ .register();
+
+CommandAPICommand("party")
+ .withArguments(arguments)
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+
+ // Get the name of the party to create
+ val partyName = args["partyName"] as String
+
+ partyMembers[player.uniqueId] = partyName
+
+ CommandAPI.updateRequirements(player)
+ })
+ .register()
+
+That's it!
+The CommandAPI lets you handle multiple requirements really easily! The withRequirement
method can be called multiple times, so you don't have to worry about shoving everything in one expression.
For example, you can apply multiple requirements for a command by calling the withRequirement
method multiple times:
new CommandAPICommand("someCommand")
+ .withRequirement(sender -> ((Player) sender).getLevel() >= 30)
+ .withRequirement(sender -> ((Player) sender).getInventory().contains(Material.DIAMOND_PICKAXE))
+ .withRequirement(sender -> ((Player) sender).isInvulnerable())
+ .executesPlayer((player, args) -> {
+ // Code goes here
+ })
+ .register();
+
+CommandAPICommand("someCommand")
+ .withRequirement { (it as Player).level >= 30 }
+ .withRequirement { (it as Player).inventory.contains(Material.DIAMOND_PICKAXE) }
+ .withRequirement { (it as Player).isInvulnerable() }
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ // Code goes here
+ })
+ .register()
+
+Aliases for commands can be added by using the withAliases()
method when registering a command. Aliases allow you to run the same command with a different 'name' from the original registered command name.
In this example, we register the command /getpos
that returns the command sender's location. We apply the aliases /getposition
, /getloc
, /getlocation
and /whereami
as well, using the withAliases()
method.
new CommandAPICommand("getpos")
+ // Declare your aliases
+ .withAliases("getposition", "getloc", "getlocation", "whereami")
+
+ // Declare your implementation
+ .executesEntity((entity, args) -> {
+ entity.sendMessage(String.format("You are at %d, %d, %d",
+ entity.getLocation().getBlockX(),
+ entity.getLocation().getBlockY(),
+ entity.getLocation().getBlockZ())
+ );
+ })
+ .executesCommandBlock((block, args) -> {
+ block.sendMessage(String.format("You are at %d, %d, %d",
+ block.getBlock().getLocation().getBlockX(),
+ block.getBlock().getLocation().getBlockY(),
+ block.getBlock().getLocation().getBlockZ())
+ );
+ })
+
+ // Register the command
+ .register();
+
+CommandAPICommand("getpos")
+ // Declare your aliases
+ .withAliases("getposition", "getloc", "getlocation", "whereami")
+
+ // Declare your implementation
+ .executesEntity(EntityCommandExecutor { entity, _ ->
+ val loc = entity.location
+ entity.sendMessage("You are at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ}")
+ })
+ .executesCommandBlock(CommandBlockCommandExecutor { block, _ ->
+ val loc = block.block.location
+ block.sendMessage("You are at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ}")
+ })
+
+ // Register the command
+ .register()
+
+commandAPICommand("getpos") {
+ // Declare your aliases
+ withAliases("getposition", "getloc", "getlocation", "whereami")
+
+ // Declare your implementation
+ entityExecutor { entity, _ ->
+ val loc = entity.location
+ entity.sendMessage("You are at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ}")
+ }
+ commandBlockExecutor { block, _ ->
+ val loc = block.block.location
+ block.sendMessage("You are at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ}")
+ }
+}
+
+Help topics can be added to your command using the withHelp()
, withShortDescription()
, withFullDescription()
or withUsage()
methods when registering a command. Help allows users to understand what your command does and provides them with a list of usage forms to aid in writing a command.
A help topic consists of two mains parts:
+This can be seen with the following example, for a command /mycmd
. This example has the short description "Says hi", and a full description "Broadcasts hi to everyone on the server". The short help is shown in the help list, which (in this example) is viewed using /help 5
. The full description is shown for the help for the command on its own, which is viewed using /help mycmd
:
The CommandAPI has three methods to register parts of a help. The withShortDescription()
sets the short description for the command, the withFullDescription()
sets the full description for the command and withHelp()
is a simple way to set both the short and full description at the same time. The withHelp()
method is the recommended method to use to set the help for a command.
If no short description is provided, the CommandAPI will attempt to use the full description if one is present. Note that this may be truncated automatically, so it is recommended to provide your own short description.
+These are the following methods that the CommandAPI provides to set the help topic for a command:
+CommandAPICommand withShortDescription(String description);
+
+The withShortDescription
method simply sets the short description for the command. In the above screenshot, the short description is "Says hi".
CommandAPICommand withFullDescription(String description);
+
+The withFullDescription
method sets the full description for the command. In the above screenshot, the full description is "Broadcasts hi to everyone on the server".
CommandAPICommand withHelp(String shortDescription, String fullDescription);
+
+The withHelp
method sets both the short description and the full description at the same time.
In this simple example, we implement the above screenshot's help topic. We register a command /mycmd
and use the withShortDescription
and withFullDescription
methods to create a help topic:
new CommandAPICommand("mycmd")
+ .withShortDescription("Says hi")
+ .withFullDescription("Broadcasts hi to everyone on the server")
+ .executes((sender, args) -> {
+ Bukkit.broadcastMessage("Hi!");
+ })
+ .register();
+
+CommandAPICommand("mycmd")
+ .withShortDescription("Says hi")
+ .withFullDescription("Broadcasts hi to everyone on the server")
+ .executes(CommandExecutor { _, _ ->
+ Bukkit.broadcastMessage("Hi!")
+ })
+ .register()
+
+commandAPICommand("mycmd") {
+ withShortDescription("Says hi")
+ withFullDescription("Broadcasts hi to everyone on the server")
+ anyExecutor { _, _ ->
+ Bukkit.broadcastMessage("Hi!")
+ }
+}
+
+We could also register this command using the withHelp
method instead:
new CommandAPICommand("mycmd")
+ .withHelp("Says hi", "Broadcasts hi to everyone on the server")
+ .executes((sender, args) -> {
+ Bukkit.broadcastMessage("Hi!");
+ })
+ .register();
+
+CommandAPICommand("mycmd")
+ .withHelp("Says hi", "Broadcasts hi to everyone on the server")
+ .executes(CommandExecutor { _, _ ->
+ Bukkit.broadcastMessage("Hi!")
+ })
+ .register()
+
+commandAPICommand("mycmd") {
+ withHelp("Says hi", "Broadcasts hi to everyone on the server")
+ anyExecutor { _, _ ->
+ Bukkit.broadcastMessage("Hi!")
+ }
+}
+
+
+For more control over help topics, the CommandAPI offers the following method, which allows you to provide your own HelpTopic
object:
CommandAPICommand withHelp(HelpTopic helpTopic);
+
+In this example, we implement locale-specific help so players can see help in their desired language. To do this, we must make use of the Bukkit HelpTopic
object which gives us more control over the content of help that is displayed to a player:
public HelpTopic makeHelp(String command) {
+ return new HelpTopic() {
+
+ @Override
+ public String getShortText() {
+ return "Says hi";
+ }
+
+ @Override
+ public String getFullText(CommandSender forWho) {
+ String helpText = "";
+ if (forWho instanceof Player player) {
+ // Make use of the player's locale to make language-specific help!
+ Locale playerLocale = player.locale();
+ if (playerLocale.getLanguage().equals("en")) {
+ helpText = "Broadcasts \"Hi!\" to everyone on the server";
+ } else if (playerLocale.getLanguage().equals("de")) {
+ helpText = "Sendet \"Hi!\" an alle auf dem Server";
+ }
+ } else {
+ helpText = "Broadcasts \"Hi!\" to everyone on the server";
+ }
+ return helpText;
+ }
+
+ // Allow anyone to see this help topic
+ @Override
+ public boolean canSee(CommandSender player) {
+ return true;
+ }
+ };
+}
+
+fun makeHelp(command: String): HelpTopic = object: HelpTopic() {
+ override fun getShortText(): String = "Says hi"
+
+ override fun getFullText(forWho: CommandSender): String {
+ var helpText = ""
+ if (forWho is Player) {
+ // Make use of the player's locale to make language-specific help!
+ val playerLocale = forWho.locale()
+ if (playerLocale.getLanguage() == "en") {
+ helpText = "Broadcasts \"Hi!\" to everyone on the server"
+ } else if (playerLocale.getLanguage() == "de") {
+ helpText = "Sendet \"Hi!\" an alle auf dem Server"
+ }
+ } else {
+ helpText = "Broadcasts \"Hi!\" to everyone on the server"
+ }
+ return helpText
+ }
+
+ // Allow anyone to see this help topic
+ override fun canSee(player: CommandSender): Boolean = true
+}
+
+We then add our new HelpTopic
to the command using the withHelp
method:
new CommandAPICommand("mycmd")
+ .withHelp(makeHelp("mycmd"))
+ .executes((sender, args) -> {
+ Bukkit.broadcastMessage("Hi!");
+ })
+ .register();
+
+return CommandAPICommand("mycmd")
+ .withHelp(makeHelp("mycmd"))
+ .executes(CommandExecutor { _, _ ->
+ Bukkit.broadcastMessage("Hi!")
+ })
+ .register()
+
+When registering a command, there also is a command usage generated. The CommandAPI provides a way to customise this usage by providing the withUsage()
method:
CommandAPICommand withUsage(String... usage)
+
+In this example, we want to showcase how usage generation displays the usage vs. how a custom usage displays the usage:
+/command <help> <admin|user|moderator|vip>
+/command <reload> <commandsystem|config|server>
+
+This is how it would get displayed:
+Usage:
+- /command <help> <admin>
+- /command <help> <user>
+- /command <help> <moderator>
+- /command <help> <vip>
+- /command <reload> <commandsystem>
+- /command <reload> <config>
+- /command <reload> <server>
+
+Now, we are implementing the withUsage()
method:
new CommandAPICommand("...")
+ .withUsage(
+ "/command <help> <section>",
+ "/command <reload> <system>"
+ )
+
+By using withUsage()
like that, the CommandAPI will produce this usage:
Usage:
+- /command <help> <section>
+- /command <reload> <system>
+
+Subcommands is another method for registering commands that makes use of creating multiple different CommandAPICommand
instances. Given a CommandAPICommand
, we can add a subcommand by using the following method:
CommandAPICommand withSubcommand(CommandAPICommand subcommand);
+CommandAPICommand withSubcommands(CommandAPICommand... subcommands);
+
+Using subcommands has no disadvantages to using regular commands with the LiteralArgument
or MultiLiteralArgument
, and should be slightly more intuitive to implement if you've used other command frameworks before.
Say we wanted to write a permission management system. To do this, we'll use the following command structsyntaxure:
+/perm group add <permission> <groupName>
+/perm group remove <permission> <groupName>
+/perm user add <permission> <userName>
+/perm user remove <permission> <userName>
+
+Let's start with the simplest example - the /perm group ...
command. We have one command which is the following:
add <permission> <groupName>
+
+We can implement this by creating a CommandAPICommand
with the command name add
:
CommandAPICommand groupAdd = new CommandAPICommand("add")
+ .withArguments(new StringArgument("permission"))
+ .withArguments(new StringArgument("groupName"))
+ .executes((sender, args) -> {
+ // perm group add code
+ });
+
+val groupAdd = CommandAPICommand("add")
+ .withArguments(StringArgument("permission"))
+ .withArguments(StringArgument("groupName"))
+ .executes(CommandExecutor { sender, args ->
+ // perm group add code
+ })
+
+val groupAdd = subcommand("add") {
+ stringArgument("permission")
+ stringArgument("groupName")
+ anyExecutor { sender, args ->
+ // perm group add code
+ }
+}
+
+Similarly, we have another part remove <permission> <groupName>
. We can declare this similar to our add
command. Once we've done that, we can now join everything up together. Here, we create a command group
which adds the two other subcommands:
CommandAPICommand groupRemove = new CommandAPICommand("remove")
+ .withArguments(new StringArgument("permission"))
+ .withArguments(new StringArgument("groupName"))
+ .executes((sender, args) -> {
+ // perm group remove code
+ });
+
+CommandAPICommand group = new CommandAPICommand("group")
+ .withSubcommand(groupAdd)
+ .withSubcommand(groupRemove);
+
+val groupRemove = CommandAPICommand("remove")
+ .withArguments(StringArgument("permission"))
+ .withArguments(StringArgument("groupName"))
+ .executes(CommandExecutor { sender, args ->
+ // perm group remove code
+ })
+
+val group = CommandAPICommand("group")
+ .withSubcommand(groupAdd)
+ .withSubcommand(groupRemove)
+
+val groupRemove = subcommand("remove") {
+ stringArgument("permission")
+ stringArgument("groupName")
+ anyExecutor { sender, args ->
+ // perm group remove code
+ }
+}
+
+val group = subcommand("group") {
+ subcommand(groupAdd)
+ subcommand(groupRemove)
+}
+
+Finally, we can link everything up together to the perm
command and register the whole thing together:
new CommandAPICommand("perm")
+ .withSubcommand(group)
+ .register();
+
+CommandAPICommand("perm")
+ .withSubcommand(group)
+ .register()
+
+commandAPICommand("perm") {
+ subcommand(group)
+}
+
+Another, more intuitive method, is to shove everything in one go without creating lots of variables all over the place:
+new CommandAPICommand("perm")
+ .withSubcommand(new CommandAPICommand("group")
+ .withSubcommand(new CommandAPICommand("add")
+ .withArguments(new StringArgument("permission"))
+ .withArguments(new StringArgument("groupName"))
+ .executes((sender, args) -> {
+ // perm group add code
+ })
+ )
+ .withSubcommand(new CommandAPICommand("remove")
+ .withArguments(new StringArgument("permission"))
+ .withArguments(new StringArgument("groupName"))
+ .executes((sender, args) -> {
+ // perm group remove code
+ })
+ )
+ )
+ .withSubcommand(new CommandAPICommand("user")
+ .withSubcommand(new CommandAPICommand("add")
+ .withArguments(new StringArgument("permission"))
+ .withArguments(new StringArgument("userName"))
+ .executes((sender, args) -> {
+ // perm user add code
+ })
+ )
+ .withSubcommand(new CommandAPICommand("remove")
+ .withArguments(new StringArgument("permission"))
+ .withArguments(new StringArgument("userName"))
+ .executes((sender, args) -> {
+ // perm user remove code
+ })
+ )
+ )
+ .register();
+
+CommandAPICommand("perm")
+ .withSubcommand(CommandAPICommand("group")
+ .withSubcommand(CommandAPICommand("add")
+ .withArguments(StringArgument("permission"))
+ .withArguments(StringArgument("groupName"))
+ .executes(CommandExecutor { sender, args ->
+ // perm group add code
+ })
+ )
+ .withSubcommand(CommandAPICommand("remove")
+ .withArguments(StringArgument("permission"))
+ .withArguments(StringArgument("groupName"))
+ .executes(CommandExecutor { sender, args ->
+ // perm group remove code
+ })
+ )
+ )
+ .withSubcommand(CommandAPICommand("user")
+ .withSubcommand(CommandAPICommand("add")
+ .withArguments(StringArgument("permission"))
+ .withArguments(StringArgument("userName"))
+ .executes(CommandExecutor { sender, args ->
+ // perm user add code
+ })
+ )
+ .withSubcommand(CommandAPICommand("remove")
+ .withArguments(StringArgument("permission"))
+ .withArguments(StringArgument("userName"))
+ .executes(CommandExecutor { sender, args ->
+ // perm user remove code
+ })
+ )
+ )
+ .register()
+
+commandAPICommand("perm") {
+ subcommand("group") {
+ subcommand("add") {
+ stringArgument("permission")
+ stringArgument("groupName")
+ anyExecutor { sender, args ->
+ // perm group add code
+ }
+ }
+ subcommand("remove") {
+ stringArgument("permission")
+ stringArgument("groupName")
+ anyExecutor { sender, args ->
+ // perm group remove code
+ }
+ }
+ }
+ subcommand("user") {
+ subcommand("add") {
+ stringArgument("permission")
+ stringArgument("userName")
+ anyExecutor { sender, args ->
+ // perm user add code
+ }
+ }
+ subcommand("remove") {
+ stringArgument("permission")
+ stringArgument("userName")
+ anyExecutor { sender, args ->
+ // perm user add code
+ }
+ }
+ }
+}
+
+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.
executes()
and then()
methodsThe 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.
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();
+
+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.
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("target");
+ target.sendMessage("Hi");
+ }))
+ .register();
+
+CommandTree("sayhi")
+ .executes(CommandExecutor { sender, _ ->
+ sender.sendMessage("Hi!")
+ })
+ .then(PlayerArgument("target")
+ .executes(CommandExecutor { _, args ->
+ val target = args["target"] 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.
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("line_number");
+ String text = (String) args.get("text");
+ 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("line_number");
+ 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("line_number");
+ 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("line_number");
+ 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["line_number"] as Int
+ val text = args["text"] 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["line_number"] 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["line_number"] 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["line_number"] as Int
+ sign.setLine(line_number - 1, player.getMetadata("copied_sign_text")[0].asString())
+ sign.update(true)
+ })))
+ .register()
+
+The CommandAPI also includes a very small lightweight annotation-based command framework. This works very differently compared to previous commands shown in this documentation and it is less feature-rich than registering commands using the other methods.
+In short, the CommandAPI's annotation-based system:
+++Developer's Note:
+Currently, the annotation framework is in its infancy, so any suggestions or improvements are heavily appreciated!
+
Developer's Note:
+As of the time of writing, annotation-based commands are not compatible with the Kotlin programming language! The CommandAPI does have the Kotlin DSL instead, which is leaner, cleaner and provides a much more Kotliny experience!
+Before we go into too much detail, let's take a look at an example of what this annotation framework looks like, and compare this to the existing method.
+Let's say we're writing a plugin with the capability to create warps to places on the server. To do this, we'll make a simple command /warp
, defined as follows:
/warp - Shows help
+/warp <warp> - Teleports a player to <warp>
+/warp create <name> - Creates a new warp <name> at the player's location
+
+Using the regular CommandAPI, this is one way we can create this command. In the code below, we use StringArguments to represent the warp names. To teleport to a warp, we also populate it with suggestions (deferred so it updates), and also use a subcommand to represent /warp create
:
Map<String, Location> warps = new HashMap<>();
+
+// /warp
+new CommandAPICommand("warp")
+ .executes((sender, args) -> {
+ sender.sendMessage("--- Warp help ---");
+ sender.sendMessage("/warp - Show this help");
+ sender.sendMessage("/warp <warp> - Teleport to <warp>");
+ sender.sendMessage("/warp create <warpname> - Creates a warp at your current location");
+ })
+ .register();
+
+// /warp <warp>
+new CommandAPICommand("warp")
+ .withArguments(new StringArgument("warp").replaceSuggestions(ArgumentSuggestions.strings(info ->
+ warps.keySet().toArray(new String[0])
+ )))
+ .executesPlayer((player, args) -> {
+ player.teleport(warps.get((String) args.get(0)));
+ })
+ .register();
+
+// /warp create <warpname>
+new CommandAPICommand("warp")
+ .withSubcommand(
+ new CommandAPICommand("create")
+ .withPermission("warps.create")
+ .withArguments(new StringArgument("warpname"))
+ .executesPlayer((player, args) -> {
+ warps.put((String) args.get(0), player.getLocation());
+ })
+ )
+ .register();
+
+Seems fairly straightforward, given everything else covered in this documentation. Now let's compare it to using annotations!
+I think it's best to show the example and the explain it afterwards:
+@Command("warp")
+public class WarpCommand {
+
+ // List of warp names and their locations
+ static Map<String, Location> warps = new HashMap<>();
+
+ @Default
+ public static void warp(CommandSender sender) {
+ sender.sendMessage("--- Warp help ---");
+ sender.sendMessage("/warp - Show this help");
+ sender.sendMessage("/warp <warp> - Teleport to <warp>");
+ sender.sendMessage("/warp create <warpname> - Creates a warp at your current location");
+ }
+
+ @Default
+ public static void warp(Player player, @AStringArgument String warpName) {
+ player.teleport(warps.get(warpName));
+ }
+
+ @Subcommand("create")
+ @Permission("warps.create")
+ public static void createWarp(Player player, @AStringArgument String warpName) {
+ warps.put(warpName, player.getLocation());
+ }
+
+}
+
+CommandAPI.registerCommand(WarpCommand.class);
+
+As we can see, the code certainly looks very different to the normal registration method. Let's take it apart piece by piece to see what exactly is going on here.
+@Command("warp")
+public class WarpCommand {
+
+Firstly, we declare our command warp
. To do this, we use the @Command
annotation and simply state the name of the command in the annotation. This annotation is attached to the class WarpCommand
, which indicates that the whole class WarpCommand
will be housing our command.
The annotation framework is designed in such a way that an entire command is represented by a single class. This provides a more modular approach to command declaration that allows you to easily contain the methods of a command in one location.
+@Default
+public static void warp(CommandSender sender) {
+ sender.sendMessage("--- Warp help ---");
+ sender.sendMessage("/warp - Show this help");
+ sender.sendMessage("/warp <warp> - Teleport to <warp>");
+ sender.sendMessage("/warp create <warpname> - Creates a warp at your current location");
+}
+
+Here, declare the main command implementation using the @Default
annotation. The @Default
annotation informs the CommandAPI that the method it is attached to does not have any subcommands. This is effectively the same as registering a regular command without using .withSubcommand()
.
Here, we simply write what happens when no arguments are run (i.e. the user just runs /warp
on its own). As such, we don't include any parameters to our method.
@Default
+public static void warp(Player player, @AStringArgument String warpName) {
+ player.teleport(warps.get(warpName));
+}
+
+We also have a second @Default
annotated method, which handles our /warp <warp>
command. Because this isn't a subcommand (the warp to teleport to is not a subcommand, it's an argument), we still using the @Default
annotation. In this method, we include an argument with this command by using the @AStringArgument
annotation. This argument uses the StringArgument
class, and the name of this argument is "warpName", which is extracted from the name of the variable. Simply put, the Annotation for an argument is A followed by the name of the argument. This is synonymous with using the following:
new StringArgument("warp")
+
+It's also very important to note the parameters for this method. The first parameter is a Player
object, which represents our command sender. The CommandAPI's annotation system uses the fact that the command sender is a Player
object and automatically ensures that anyone using the command must be a Player
. In other words, non-players (such as the console or command blocks), would be unable to execute this command.
The second argument is a String
object, which represents the result of our argument "warp". The CommandAPI's annotation system can also infer the return type of the argument that is provided to it (in this case, a StringArgument
will produce a String
) and will automatically cast and provide the result to that parameter.
@Subcommand("create")
+@Permission("warps.create")
+public static void createWarp(Player player, @AStringArgument String warpName) {
+ warps.put(warpName, player.getLocation());
+}
+
+Lastly, we declare a subcommand to allow us to run /warp create <name>
. To do this, we simply use the @Subcommand
annotation. In this example, we also apply a permission node that is required to run the command by using the @Permission
annotation. The rest is fairly straight forward - we declare an argument, in this case it's another StringArgument
, so we use @AStringArgument
and then declare everything else in a similar fashion to the default command executor.
Registering the command is fairly simple and is a one liner:
+CommandAPI.registerCommand(WarpCommand.class);
+
+This line can be placed in your onEnable()
or onLoad()
method like you were registering a normal command.
This page outlines in detail the list of all annotations that the CommandAPI's annotation-based command system includes.
+@Command
("commandName")The @Command
annotation is used to declare a command. The parameter is the name of the command that will be registered.
@Command("warp")
+public class WarpCommand {
+
+@Alias({...})
The @Alias
annotation is used to declare a list of aliases for a command. The parameter is a list of aliases which can be used for the command.
@Command("teleport")
+@Alias({"tp", "tele"})
+public class TeleportCommand {
+
+@Permission("permissionNode")
The @Permission
annotation is used to add a permission node to a command. Users that want to run this command must have this permission. The parameter is the permission node required to run the command.
@Command("teleport")
+@Permission("myplugin.tp")
+class TeleportCommand {
+
+@NeedsOp
The @NeedsOp
annotation is used to indicate that a command needs to have operator privileges to run it. This annotation has no parameters.
@Command("teleport")
+@NeedsOp
+class TeleportCommand {
+
+@Help("Full description")
The @Help
annotation is used to add a help topic to a command. This annotation can take two forms:
A simple form which just uses a string which is used as the full description for a command:
+@Command("teleport")
+@Help("Teleports yourself to another location")
+class TeleportCommand {
+
+A form with two parameters value
and shortDescription
, to provide the full description (value
) and short description (shortDescription
) content for a command:
@Command("teleport")
+@Help(value = "Teleports yourself to another location", shortDescription = "TP to a location")
+class TeleportCommand {
+
+To use annotations on methods, methods must be static.
+@Default
The @Default
annotation indicates that the method is not a subcommand. This acts in a similar way to regular Bukkit commands. Commands with the @Default
annotation can be used to run the main code when the command named with the @Command
annotation is stated, such as the following:
@Default
+public static void warp(CommandSender sender) {
+ sender.sendMessage("--- Warp help ---");
+ sender.sendMessage("/warp - Show this help");
+ sender.sendMessage("/warp <warp> - Teleport to <warp>");
+ sender.sendMessage("/warp create <warpname> - Creates a warp at your current location");
+}
+
+The @Default
annotation does not mean that the command can't have arguments! Arguments can still be used and declared as shown:
@Default
+public static void warp(Player player, @AStringArgument String warpName) {
+ player.teleport(warps.get(warpName));
+}
+
+@Subcommand
The @Subcommand
simply tells the CommandAPI that the declared method is a subcommand. This acts in a similar way to the regular CommandAPI's .withSubcommand()
method. The subcommand annotation can take in a single string which is the name of the subcommand:
@Subcommand("create")
+@Permission("warps.create")
+public static void createWarp(Player player, @AStringArgument String warpName) {
+ warps.put(warpName, player.getLocation());
+}
+
+Or, it can take in a list of strings which represent the aliases that can also be used for the declared subcommand:
+@Subcommand({"teleport", "tp"})
+public static void teleport(Player player, @APlayerArgument OfflinePlayer target) {
+ if(target.isOnline() && target instanceof Player onlineTarget) {
+ player.teleport(onlineTarget);
+ }
+}
+
+@Permission
The @Permission
annotation can also be used on methods to indicate that a permission is required to execute a command.
@Subcommand("create")
+@Permission("warps.create")
+public static void createWarp(Player player, @AStringArgument String warpName) {
+ warps.put(warpName, player.getLocation());
+}
+
+@NeedsOp
The @NeedsOp
annotation can also be used on methods to indicate that the user must be an operator to run the command.
The annotations for arguments are really simple, there's just two things you need to know:
+To use an annotation argument, just add the letter A
(for 'annotation') at the beginning of it! For example:
\begin{align} +\texttt{StringArgument}&\xrightarrow{A}\texttt{@AStringArgument}\\ +\texttt{PlayerArgument}&\xrightarrow{A}\texttt{@APlayerArgument}\\ +\texttt{AdvancementArgument}&\xrightarrow{A}\texttt{@AAdvancementArgument}\\ +&\hspace{0.75em}\vdots +\end{align}
+For example, we use @AStringArgument
to indicate that this command takes a StringArgument
as its first parameter:
@Default
+public static void warp(Player player, @AStringArgument String warpName) {
+ player.teleport(warps.get(warpName));
+}
+
+warpName
.Certain argument annotations have extra parameters that can be supplied to provide additional customization:
+The following numerical arguments can take both a min
and max
value. Both of these are completely optional. This indicates the range of values (inclusive) that is valid for this argument. For example:
@Default
+public static void command(CommandSender sender,
+ @ADoubleArgument(min = 0.0, max = 10.0) double someDouble,
+ @AFloatArgument(min = 5.0f, max = 10.0f) float someFloat,
+ @AIntegerArgument(max = 100) int someInt,
+ @ALongArgument(min = -10) long someLong
+) {
+ // Command implementation here
+}
+
+Both the LiteralArgument
and MultiLiteralArgument
can be used. When these are used, the name of the variable assigned to the parameter is ignored and not used as the argument's name.
For the @ALiteralArgument
annotation, the parameter is the literal to be used for the command. For the @AMultiLiteralArgument
, the parameter can be an array of multiple literals to use:
@Default
+public static void command(CommandSender sender,
+ @ALiteralArgument("myliteral") String literal,
+ @AMultiLiteralArgument({"literal", "anotherliteral"}) String multipleLiterals
+) {
+ // Command implementation here
+}
+
+The LocationArgument
, Location2DArgument
, EntitySelectorArgument
and ScoreHolderArgument
can all take an extra parameter in their constructors. As a result, the annotation-equivalent of these arguments also allow you to provide the parameter in the annotation:
@Default
+public static void command(CommandSender sender,
+ @ALocationArgument(LocationType.BLOCK_POSITION) Location location,
+ @ALocation2DArgument(LocationType.PRECISE_POSITION) Location location2d,
+ @AEntitySelectorArgument.ManyEntities Collection<Entity> entities,
+ @AScoreHolderArgument.Multiple Collection<String> scoreHolders
+) {
+ // Command implementation here
+}
+
+Registering annotation-based commands is really simple. To do this, we use the following method, where className
is the name of a class with a @Command
annotation:
CommandAPI.registerCommand(className)
+
+Say we have a simple command /warp
that is defined as follows:
@Command("warp")
+public class WarpCommand {
+
+ // List of warp names and their locations
+ static Map<String, Location> warps = new HashMap<>();
+
+ @Default
+ public static void warp(CommandSender sender) {
+ sender.sendMessage("--- Warp help ---");
+ sender.sendMessage("/warp - Show this help");
+ sender.sendMessage("/warp <warp> - Teleport to <warp>");
+ sender.sendMessage("/warp create <warpname> - Creates a warp at your current location");
+ }
+
+ @Default
+ public static void warp(Player player, @AStringArgument String warpName) {
+ player.teleport(warps.get(warpName));
+ }
+
+ @Subcommand("create")
+ @Permission("warps.create")
+ public static void createWarp(Player player, @AStringArgument String warpName) {
+ warps.put(warpName, player.getLocation());
+ }
+
+}
+
+We can register this in our onLoad()
method so we can use this command from within Minecraft functions:
public class MyPlugin extends JavaPlugin {
+
+ @Override
+ public void onLoad() {
+ CommandAPI.registerCommand(WarpCommand.class);
+ }
+
+}
+
+The CommandAPI also provides an alternative way of making commands when using Kotlin to develop your plugins: A DSL!
+This DSL provides many methods to easily add arguments to your command structure. Examples of the DSL can be found here.
+To install the DSL, you need to add the commandapi-bukkit-kotlin
dependency into your pom.xml
or your build.gradle
, making sure to specify the server flavor you are developing for:
<dependencies>
+ <dependency>
+ <groupId>dev.jorel</groupId>
+ <artifactId>commandapi-bukkit-kotlin</artifactId>
+ <version>9.5.2</version>
+ </dependency>
+</dependencies>
+
+Next, you need to add Kotlin to your project. For this, you first need to add the dependency:
+<dependencies>
+ <dependency>
+ <groupId>org.jetbrains.kotlin</groupId>
+ <artifactId>kotlin-stdlib</artifactId>
+ <version>1.9.0</version>
+ </dependency>
+</dependencies>
+
+Finally, you need to add the kotlin-maven-plugin
:
<build>
+ <plugins>
+ <plugin>
+ <groupId>org.jetbrains.kotlin</groupId>
+ <artifactId>kotlin-maven-plugin</artifactId>
+ <version>1.9.0</version>
+ <executions>
+ <execution>
+ <id>compile</id>
+ <phase>compile</phase>
+ <goals>
+ <goal>compile</goal>
+ </goals>
+ </execution>
+ <execution>
+ <id>test-compile</id>
+ <phase>test-compile</phase>
+ <goals>
+ <goal>test-compile</goal>
+ </goals>
+ </execution>
+ </executions>
+ <configuration>
+ <jvmTarget>16</jvmTarget>
+ </configuration>
+ </plugin>
+ </plugins>
+</build>
+
+First, you need to add the repository:
+repositories {
+ mavenCentral()
+}
+
+repositories {
+ mavenCentral()
+}
+
+Next, you need to add the dependency:
+dependencies {
+ implementation "dev.jorel:commandapi-bukkit-kotlin:9.5.2"
+}
+
+dependencies {
+ implementation("dev.jorel:commandapi-bukkit-kotlin:9.5.2")
+}
+
+You also need to add Kotlin to your project. For this, you first need to add the Kotlin plugin:
+plugins {
+ id "org.jetbrains.kotlin.jvm" version "1.9.0"
+}
+
+plugins {
+ kotlin("jvm") version "1.9.0"
+}
+
+Next, you need to add the dependency (you should already have added the mavenCentral()
repository to your project):
dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib"
+}
+
+dependencies {
+ implementation("org.jetbrains.kotlin:kotlin-stdlib")
+}
+
+Then, you need to configure the Java version to build against:
+kotlin {
+ jvmToolchain(16)
+}
+
+kotlin {
+ jvmToolchain(16)
+}
+
+As a first example and to take a first look at the Kotlin DSL syntax, we will first create a simple command to send messages to a player.
+We want to create a command that lets us send a message to a player. To do this, we want to register a command with the following syntax:
+/sendmessageto <player> <msg>
+
+We can then use the following command registration:
+commandTree("sendmessageto") {
+ playerArgument("player") { // Defines a new PlayerArgument("player")
+ greedyStringArgument("msg") { // Defines a new GreedyStringArgument("msg")
+ anyExecutor { _, args -> // Command can be executed by anyone and anything (such as entities, the console, etc.)
+ val player: Player = args["player"] as Player
+ val message: String = args["msg"] as String
+ player.sendMessage(message)
+ }
+ }
+ }
+}
+
+commandAPICommand("sendmessageto") {
+ playerArgument("player") // Defines a new PlayerArgument("player")
+ greedyStringArgument("msg") // Defines a new GreedyStringArgument("msg")
+ anyExecutor { _, args -> // Command can be executed by anyone and anything (such as entities, the console, etc.)
+ val player: Player = args["player"] as Player
+ val message: String = args["msg"] as String
+ player.sendMessage(message)
+ }
+}
+
+Here you can see some interesting things:
+.register()
method when using the DSLThe Kotlin DSL also provides executors to execute your command. You've seen the anyExecutor
in the example above.
To find out, which DSL executor corresponds to "normal" executors, you can refer to the table below:
+DSL normal executor | DSL resulting executor | DSL normal execution info | DSL resulting execution info | "normal" Executor |
---|---|---|---|---|
anyExecutor() | anyResultingExecutor() | anyExecutionInfo() | anyResultingExecutionInfo | executes() |
playerExecutor() | playerResultingExecutor() | playerExecutionInfo() | playerResultingExecutionInfo() | executesPlayer() |
entityExecutor() | entityResultingExecutor() | entityExecutionInfo() | entityResultingExecutionInfo() | executesEntity() |
consoleExecutor() | consoleResultingExecutor() | consoleExecutionInfo() | consoleResultingExecutionInfo() | executesConsole() |
commandBlockExecutor() | commandBlockResultingExecutor() | commandBlockExecutionInfo() | commandBlockResultingExecutionInfo() | executesCommandBlock() |
proxyExecutor() | proxyResultingExecutor() | proxyExecutionInfo() | proxyResultingExecutionInfo() | executesProxy() |
nativeExecutor() | nativeResultingExecutor() | nativeExecutionInfo() | nativeResultingExecutionInfo() | executesNative() |
remoteConsoleExecutor() | remoteConsoleResultingExecutor() | remoteConsoleExecutionInfo() | remoteConsoleResultingExecutionInfo() | executesRemoteConsole() |
The DSL implements almost every argument with a method. You've seen the playerArgument()
and the greedyStringArgument()
method in the example at the top of this page.
The way arguments are implemented is pretty straight forward: It's basically the argument class' name, but as a method. So if you wanted to use a ItemStackArgument
in your command, you would use the itemStackArgument()
method of the DSL.
One thing to note is that the DSL also features every existing constructor. This means if you want to use an IntegerArgument
with a minimum of 0
and a maximum of 10
, you normally would implement it like this:
new IntegerArgument("integer", 0, 10)
+
+However, when using this DSL it is implemented like this:
+integerArgument("integer", 0, 10)
+
+Developer's Note:
+There are a few arguments not having a method which directly corresponds to their respective argument.
+These arguments most likely use a builder pattern and because of that require further implementation by the user.
+To use these arguments, the DSL also provides the argument()
method which takes in any argument as a parameter.
When using the DSL, you might want to modify the behaviour of certain arguments by adding requirements or suggestions to them.
+To give you a general idea how you could accomplish that, the sendMessageTo
command is adding a broadcast option which should only be executed by server operators.
commandTree("sendMessageTo") {
+ playerArgument("player") {
+ greedyStringArgument("msg") {
+ playerExecutor { _, args ->
+ val player: Player = args["player"] as Player
+ val message: String = args["msg"] as String
+ player.sendMessage(message)
+ }
+ }
+ }
+ literalArgument("broadcast") {
+ withRequirement { sender: CommandSender -> sender.isOp } // Applies the requirement to the broadcast literal argument
+ /* add more methods here that modify argument behaviour */
+ greedyStringArgument("msg") {
+ playerExecutor { _, args ->
+ val message: String = args["msg"] as String
+ Bukkit.broadcastMessage(message)
+ }
+ }
+ }
+}
+
+commandAPICommand("sendMessageTo") {
+ playerArgument("player")
+ greedyStringArgument("msg")
+ playerExecutor { _, args ->
+ val player: Player = args["player"] as Player
+ val message: String = args["msg"] as String
+ player.sendMessage(message)
+ }
+}
+
+commandAPICommand("sendMessageTo") {
+ literalArgument("broadcast") {
+ withRequirement { sender: CommandSender -> sender.isOp } // Applies the requirement to the broadcast literal argument
+ /* add more methods here that modify argument behaviour */
+ }
+ greedyStringArgument("msg")
+ playerExecutor { _, args ->
+ val message: String = args["msg"] as String
+ Bukkit.broadcastMessage(message)
+ }
+}
+
+Notice how you can just add the requirement in a CommandTree by adding it to the argument block where you also define the next arguments and the executor.
+However, when modifying the behaviour of an argument in a CommandAPICommand you have to add an extra block where you can implement the additional behaviour.
+Expanding on the previous example where we added a requirement to a single argument, we now also want to add a requirement to a whole command.
+This works similar to how argument behaviour is modified in a CommandTree:
+commandTree("commandRequirement") {
+ withRequirement { sender: CommandSender -> sender.isOp}
+ playerExecutor { player, _ ->
+ player.sendMessage("This command can only be executed by players who are server operators.")
+ }
+}
+
+commandAPICommand("commandRequirement") {
+ withRequirement { sender: CommandSender -> sender.isOp}
+ playerExecutor { player, _ ->
+ player.sendMessage("This command can only be executed by players who are server operators.")
+ }
+}
+
+Now, a few more examples are shown to demonstrate the use of this DSL a little more:
+We want to create a /give
command with the following syntax:
/optionalArgument give <item>
+/optionalArgument give <item> <amount>
+
+To declare an argument as optional you need to set the optional
value to true
:
commandTree("optionalArgument") {
+ literalArgument("give") {
+ itemStackArgument("item") {
+ integerArgument("amount", optional = true) {
+ playerExecutor { player, args ->
+ // This command will let you execute:
+ // "/optionalArgument give minecraft:stick"
+ // "/optionalArgument give minecraft:stick 5"
+ val itemStack: ItemStack = args["item"] as ItemStack
+ val amount: Int = args.getOptional("amount").orElse(1) as Int
+ itemStack.amount = amount
+ player.inventory.addItem(itemStack)
+ }
+ }
+ }
+ }
+}
+
+commandAPICommand("optionalArgument") {
+ literalArgument("give")
+ itemStackArgument("item")
+ integerArgument("amount", optional = true) // This sets the argument as optional
+ playerExecutor { player, args ->
+ // This command will let you execute:
+ // "/optionalArgument give minecraft:stick"
+ // "/optionalArgument give minecraft:stick 5"
+ val itemStack: ItemStack = args["item"] as ItemStack
+ val amount: Int = args.getOptional("amount").orElse(1) as Int
+ itemStack.amount = amount
+ player.inventory.addItem(itemStack)
+ }
+}
+
+We want to create a command with the following syntax to demonstrate replacing suggestions using the Kotlin DSL:
+/replaceSuggestions <strings>
+
+Replacing suggestions works similar to how you would add a requirement to an argument as shown in Editing arguments.
+You just have to use the replaceSuggestions
method this time:
commandTree("replaceSuggestions") {
+ stringArgument("strings") {
+ replaceSuggestions(ArgumentSuggestions.strings("one", "two", "three")) // Replaces the suggestions for the "strings" StringArgument
+ playerExecutor { player, args ->
+ player.sendMessage("You chose option ${args["strings"] as String}!")
+ }
+ }
+}
+
+commandAPICommand("replaceSuggestions") {
+ stringArgument("strings") {
+ replaceSuggestions(ArgumentSuggestions.strings("one", "two", "three")) // Replaces the suggestions for the "strings" StringArgument
+ }
+ playerExecutor { player, args ->
+ player.sendMessage("You chose option ${args["strings"] as String}!")
+ }
+}
+
+The CommandAPI offers an additional way to access arguments when using Kotlin: delegated properties. With delegated properties, there are two possible dependencies you can use:
+commandapi-core-kotlin
Support for delegated properties has been added to the commandapi-core-kotlin
module. If you want to use delegated properties, you need to add this dependency.
commandapi-bukkit-kotlin
If you are already using the Kotlin DSL to create your commands, you can already use delegated properties. commandapi-core-kotlin
is included in commandapi-bukkit-kotlin
.
To be able to access arguments by using delegated properties, your variable name needs to match the node name of the argument. This could look like this:
+CommandAPICommand("mycommand")
+ .withArguments(StringArgument("string"))
+ .withArguments(PlayerArgument("target"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val string: String by args
+ val target: Player by args
+ // Implementation...
+ })
+ .register()
+
+commandAPICommand("mycommand") {
+ stringArgument("string")
+ playerArgument("target")
+ playerExecutor { player, args ->
+ val string: String by args
+ val target: Player by args
+ // Implementation...
+ }
+}
+
+++Developer's Note:
+The CommandAPI hasn't been released for Velocity yet. +We do, however, offer snapshot builds. This small section on Velocity will outline how to get the snapshot builds and what limitations the CommandAPI currently has on Velocity.
+This page focuses on outlining how to set up the CommandAPI for Velocity. It expects that you are already familiar with how to set up a Velocity plugin.
+
Because we do not have an official release yet, the snapshot builds are not published in the Maven Central repository. Instead you need to add our snapshot repository:
+<repositories>
+ <repository>
+ <id>oss.sonatype.org-snapshot</id>
+ <url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
+ </repository>
+</repositories>
+
+repositories {
+ maven {
+ url = "https://s01.oss.sonatype.org/content/repositories/snapshots"
+ }
+}
+
+repositories {
+ maven {
+ url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots")
+ }
+}
+
+As mentioned, Velocity can only be accessed with snapshot builds. These snapshot build version are following standard semantic versioning and thus have the -SNAPSHOT
suffix:
<dependencies>
+ <dependency>
+ <groupId>dev.jorel</groupId>
+ <artifactId>commandapi-velocity-shade</artifactId>
+ <version>9.5.0-SNAPSHOT</version>
+ </dependency>
+</dependencies>
+
+dependencies {
+ implementation "dev.jorel:commandapi-velocity-shade:9.5.0-SNAPSHOT"
+}
+
+dependencies {
+ implementation("dev.jorel:commandapi-velocity-shade:9.5.0-SNAPSHOT")
+}
+
+The CommandAPI requires two steps: loading and enabling. We will perform these steps in Velocity's loading stages, construction and initialization. These two stages are explained in their documentation. +We will perform the CommandAPI's loading step in the construction phase first:
+@Inject
+public ExamplePlugin(ProxyServer server, Logger logger) {
+ this.server = server;
+ this.logger = logger;
+
+ CommandAPI.onLoad(new CommandAPIVelocityConfig(server, this));
+}
+
+Next, we want to utilize Velocity's ProxyInitializeEvent
to perform the CommandAPI's enabling step:
@Subscribe
+public void onProxyInitialization(ProxyInitializeEvent event) {
+ // Any CommandAPI command registrations...
+ CommandAPI.onEnable();
+}
+
+The CommandAPI currently only offers support for a very limited amount of arguments on Velocity. This is because arguments are primarily implemented on the backend servers. +However, the CommandAPI offers access for the primitive type arguments:
+IntegerArgument
LongArgument
FloatArgument
DoubleArgument
BooleanArgument
StringArgument
TextArgument
GreedyStringArgument
LiteralArgument
MultiLiteralArgument
Command registration works the same way as it does in Bukkit. To visualize this, we want to register a simple command that generates a random number between a chosen minimum and a chosen maximum value:
+We want to register the command /randomnumber
with the following syntax:
/randomnumber <min> <max>
+
+To accomplish that, we register the command like this:
+new CommandAPICommand("randomnumber")
+ .withArguments(new IntegerArgument("min"))
+ .withArguments(new IntegerArgument("max"))
+ .executesPlayer((player, args) -> {
+ int min = (int) args.get("min");
+ int max = (int) args.get("max");
+ Random random = ThreadLocalRandom.current();
+ int randomNumber = random.nextInt(min, max);
+ player.sendMessage(Component.text().content("Your random number is: " + randomNumber));
+ })
+ .register();
+
+CommandAPICommand("randomnumber")
+ .withArguments(IntegerArgument("min"))
+ .withArguments(IntegerArgument("max"))
+ .executesPlayer(PlayerCommandExecutor { player, args ->
+ val min = args["min"] as Int
+ val max = args["max"] as Int
+ val random = ThreadLocalRandom.current()
+ val randomNumber = random.nextInt(min, max)
+ player.sendMessage(Component.text().content("Your random number is: $randomNumber"))
+ })
+ .register()
+
+++Developer's Note:
+If you're a server owner, you're probably lost! This section is for developer command conversion. If you're looking for how to convert plugins with the
+config.yml
file, you want 2. Configuration for server owners.
The CommandAPI has the ability to convert plugin commands to vanilla Minecraft commands using its config.yml
's plugins-to-convert
option. Nevertheless, the API for command conversion is not hidden and you're free to use it as you see fit!
Before you continue, let's clear up a few naming conventions which is used in the following sections!
+boolean onCommand(CommandSender ... )
methodTo register all commands that are declared by a target plugin, the Converter.convert(Plugin)
method can be used. This attempts to register all commands declared in a target plugin's plugin.yml
file, as well as any aliases or permissions stated in the plugin.yml
file.
Say you have some plugin.yml
file for a target plugin that adds some basic functionality to a server. The target plugin in this example is called "TargetPlugin":
name: TargetPlugin
+main: some.random.package.Main
+version: 1.0
+commands:
+ gmc:
+ aliases: gm1
+ gms:
+ i:
+ permission: item.permission
+
+As you can see, it declares 3 commands: /gmc
, /gms
and /i
. We can now begin writing your plugin that uses the CommandAPI converter. We will call this plugin "YourPlugin":
public class YourPlugin extends JavaPlugin {
+
+ @Override
+ public void onEnable() {
+ Converter.convert((JavaPlugin) Bukkit.getPluginManager().getPlugin("TargetPlugin"));
+ // Other code goes here...
+ }
+
+}
+
+class YourPlugin : JavaPlugin() {
+
+ override fun onEnable() {
+ Converter.convert(Bukkit.getPluginManager().getPlugin("TargetPlugin") as JavaPlugin)
+ // Other code goes here...
+ }
+
+}
+
+When this is run, the commands /gmc
, /gm1
, /gms
and /i
will all be registered by the CommandAPI.
In addition to converting all commands from a target plugin, the CommandAPI allows you to convert single commands at a time using the following methods from the Converter
class:
public static convert(Plugin plugin, String cmdName);
+public static convert(Plugin plugin, String cmdName, List<Argument> arguments);
+public static convert(Plugin plugin, String cmdName, Argument... arguments);
+
+In these commands, the plugin
refers to the plugin which has the command you want to convert and cmdName
is the name of the command declared in the target plugin's plugin.yml
file (just the main command, not the aliases!).
The List<Argument>
or Argument...
can be used to provide argument checks that lets you apply the command UI to a bukkit command.
Say we want to convert EssentialsX's /speed
command using the CommandAPI. The plugin.yml
entry for the /speed
command is the following:
speed:
+ description: Change your speed limits.
+ usage: /<command> [type] <speed> [player]
+ aliases: [flyspeed,eflyspeed,fspeed,efspeed,espeed,walkspeed,ewalkspeed,wspeed,ewspeed]
+
+From this, we can determine that there are the following commands, where "walk" and "fly" are the different types that the command can take:
+/speed <speed>
+/speed <speed> <target>
+/speed <walk/fly> <speed>
+/speed <walk/fly> <speed> <target>
+
+With the EssentialsX plugin, the <speed>
value can only take numbers between 0 and 10. As such, we'll ensure to apply these limits using the IntegerArgument
. In addition, since the speed type can only be "walk" or "fly", we'll add that to our converter as well using a MultiLiteralArgument
:
JavaPlugin essentials = (JavaPlugin) Bukkit.getPluginManager().getPlugin("Essentials");
+
+// /speed <speed>
+Converter.convert(essentials, "speed", new IntegerArgument("speed", 0, 10));
+
+// /speed <target>
+Converter.convert(essentials, "speed", new PlayerArgument("target"));
+
+// /speed <walk/fly> <speed>
+Converter.convert(essentials, "speed",
+ new MultiLiteralArgument("modes", "walk", "fly"),
+ new IntegerArgument("speed", 0, 10)
+);
+
+// /speed <walk/fly> <speed> <target>
+Converter.convert(essentials, "speed",
+ new MultiLiteralArgument("modes", "walk", "fly"),
+ new IntegerArgument("speed", 0, 10),
+ new PlayerArgument("target")
+);
+
+val essentials = Bukkit.getPluginManager().getPlugin("Essentials") as JavaPlugin
+
+// /speed <speed>
+Converter.convert(essentials, "speed", IntegerArgument("speed", 0, 10))
+
+// /speed <target>
+Converter.convert(essentials, "speed", PlayerArgument("target"))
+
+// /speed <walk/fly> <speed>
+Converter.convert(essentials, "speed",
+ MultiLiteralArgument("modes", "walk", "fly"),
+ IntegerArgument("speed", 0, 10)
+)
+
+// /speed <walk/fly> <speed> <target>
+Converter.convert(essentials, "speed",
+ MultiLiteralArgument("modes", "walk", "fly"),
+ IntegerArgument("speed", 0, 10),
+ PlayerArgument("target")
+)
+
+Formally, the CommandAPI does not support plugin reloading. This includes, but is not limited to:
+/reload
command which reloads all plugins on the serverIn general, using the /reload
command is not advised. Here's some useful resources from various Bukkit/Spigot/Paper developers:
The CommandAPI is not like normal Bukkit/Spigot/Paper plugins. It directly accesses all of the nitty-gritty Vanilla Minecraft code to convert and expose Minecraft's internal command framework into a Bukkit-API friendly interface for you to use. As the CommandAPI hooks directly into Vanilla Minecraft code, and /reload
is a Bukkit feature, using /reload
can cause Vanilla Minecraft's internal system to become unstable. If you are having issues with /reload
, seriously reconsider shutting your server down correctly and restarting it, instead of running /reload
.
Despite this, there is one way to get reloading to work using the onDisable()
method in your plugin. If you register a command in your onLoad()
or onEnable()
method, by unregistering the command in your onDisable()
method, this allows the CommandAPI to properly register the command again when the server reloads.
Developer's Note:
+Despite the fact that you can do this, I cannot stress enough that this is not recommended, due to the fact that functions/tags in datapacks do not work with /reload
, even if you unregister the command.
In this example, we add support for reloading the server via /reload
by unregistering the command in the onDisable()
method. Note that force-unregistering is not required for this:
public class MyPlugin extends JavaPlugin {
+
+ @Override
+ public void onEnable() {
+ new CommandAPICommand("ping")
+ .executes((sender, args) -> {
+ sender.sendMessage("Pong!");
+ })
+ .register();
+ }
+
+ @Override
+ public void onDisable() {
+ CommandAPI.unregister("ping");
+ }
+
+}
+
+The CommandAPI does a lot of stuff "behind the scenes". This internal CommandAPI section will go into detail about what the CommandAPI does, how it's implemented and why it has been implemented like that.
+The CommandAPI's arguments are representations of the different arguments that the Minecraft Command Data protocol handles. These are outlined in the table below:
+Identifier | CommandAPI argument |
---|---|
brigadier:bool | BooleanArgument |
brigadier:double | DoubleArgument |
brigadier:float | FloatArgument |
brigadier:integer | IntegerArgument |
brigadier:long | LongArgument |
brigadier:string | StringArgument TextArgument GreedyStringArgument CustomArgument<T> |
minecraft:angle | AngleArgument |
minecraft:block_pos | LocationArgument ( LocationType.BLOCK_POSITION ) |
minecraft:block_predicate | BlockPredicateArgument |
minecraft:block_state | BlockStateArgument |
minecraft:color | ChatColorArgument |
minecraft:column_pos | Location2DArgument ( LocationType.BLOCK_POSITION ) |
minecraft:component | ChatComponentArgument |
minecraft:dimension | WorldArgument |
minecraft:entity | EntitySelectorArgument |
minecraft:entity_anchor | |
minecraft:entity_summon | EntityTypeArgument |
minecraft:float_range | FloatRangeArgument |
minecraft:function | FunctionArgument |
minecraft:game_profile | PlayerArgument |
minecraft:game_profile | OfflinePlayerArgument |
minecraft:int_range | IntegerRangeArgument |
minecraft:item_enchantment | EnchantmentArgument |
minecraft:item_predicate | ItemStackPredicateArgument |
minecraft:item_slot | |
minecraft:item_stack | ItemStackArgument |
minecraft:message | ChatArgument |
minecraft:mob_effect | PotionEffectArgument |
minecraft:nbt | |
minecraft:nbt_compound_tag | NBTCompoundArgument |
minecraft:nbt_path | |
minecraft:nbt_tag | |
minecraft:objective | ObjectiveArgument |
minecraft:objective_criteria | ObjectiveCriteriaArgument |
minecraft:operation | MathOperationArgument |
minecraft:particle | ParticleArgument |
minecraft:resource_location | AdvancementArgument BiomeArgument CustomArgument<T> LootTableArgument NamespacedKeyArgument RecipeArgument SoundArgument |
minecraft:rotation | RotationArgument |
minecraft:score_holder | ScoreHolderArgument |
minecraft:scoreboard_slot | ScoreboardSlotArgument |
minecraft:swizzle | AxisArgument |
minecraft:team | TeamArgument |
minecraft:time | TimeArgument |
minecraft:uuid | UUIDArgument |
minecraft:vec2 | Location2DArgument ( LocationType.PRECISE_POSITION ) |
minecraft:vec3 | LocationArgument ( LocationType.PRECISE_POSITION ) |
There are a few arguments that aren't implemented. Here's why:
+minecraft:entity_anchor
- This argument only has two values: eyes
and feet
. It's incredibly unnecessary for any other purpose and is easier to implement with a MultiLiteralArgument
.
minecraft:item_slot
- Bukkit's implementation of item slot numbers differs very wildly to Minecraft's implementation of item slot numbers. This difference makes it near-impossible to have a suitable middle-ground for item slot numbers that ensures that invalid numbers cannot be passed to the wrong inventory type. An implementation of this would require a rewrite of the current system to maintain proper inventory slot access safety.
minecraft:nbt
, minecraft:nbt_path
, minecraft:nbt_tag
- You've got the NBTCompoundArgument
, that's good enough, right? ¯\_(ツ)_/¯
During the initialization of Minecraft 1.16+ servers, the CommandAPI uses a custom datapack reloading sequence as opposed to the normal Vanilla Minecraft datapack reloading method. The CommandAPI's method uses the server's current command dispatcher object as opposed to a new one, which allows datapacks to use commands registered by the CommandAPI. This can be invoked using the following method:
+CommandAPI.reloadDatapacks();
+
+The CommandAPI doesn't store the CommandAPICommand
objects during the main running of the server because it simply doesn't need to. Instead, it stores a list of RegisteredCommand
objects which are defined as the following, which should be fairly self-explanatory:
public record RegisteredCommand {
+ String commandName();
+ List<String> argsAsStr();
+ Optional<String> shortDescription();
+ Optional<String> fullDescription();
+ Optional<String[]> usageDescription();
+ String[] aliases();
+ CommandPermission permission();
+}
+
+The argsAsStr()
method returns a list of arguments in a string format, of the form argName:SimpleClassName
, where argName
is the name of the argument (the argument's node name) and SimpleClassName
is the name of the argument class that was used to construct it (such as IntegerArgument
).
A List<RegisteredCommand>
can be acquired using the following method:
CommandAPI.getRegisteredCommands();
+
+++Note that this list does not update when commands are unregistered, only when commands are registered.
+
So far, we've been using only the CommandAPI to register commands. As a result, this makes the CommandAPI's features limited by whatever the CommandAPI has implemented. To push past these limits, the CommandAPI includes some extra methods to help with invoking brigadier methods. Of course, to use these methods, brigadier is required. The brigadier dependency's installation instructions can be found here.
+++Developer's Note:
+For those that are unaware, brigadier is Mojang's command parser and dispatching framework. This is what the CommandAPI wraps around and is the main underlying source of its functionality.
+
The CommandAPI has been designed in such a way that you shouldn't have to access NMS in order to make use of the more "advanced" arguments and features - if you find that NMS is required to do something, please make a new issue!
+The CommandAPI offers the following methods in the dev.jorel.commandapi.Brigadier
class:
public static CommandDispatcher getCommandDispatcher();
+public static RootCommandNode getRootNode();
+public static LiteralArgumentBuilder fromLiteralArgument(LiteralArgument literalArgument);
+public static RedirectModifier fromPredicate(BiPredicate<CommandSender, Object[]> predicate, List<Argument> args);
+public static Command fromCommand(CommandAPICommand command);
+public static RequiredArgumentBuilder fromArgument(List<Argument> args, Argument<?> argument);
+public static RequiredArgumentBuilder fromArgument(Argument argument);
+public static SuggestionProvider toSuggestions(Argument<?> argument, List<Argument> args);
+public static Object[] parseArguments(CommandContext cmdCtx, List<Argument> args);
+public static Object getBrigadierSourceFromCommandSender(CommandSender sender);
+public static CommandSender getBukkitCommandSenderFromContext(CommandContext cmdCtx);
+
+Briefly, here's what each of these functions do (you can view the JavaDocs for more information):
+Method | Description |
---|---|
getCommandDispatcher | Returns the Minecraft command dispatcher graph |
getRootNode | Returns the root node of the command dispatcher. This is equivalent to using getCommandDispatcher().getRoot(); |
fromLiteralArgument | Creates a LiteralArgumentBuilder from a LiteralArgument |
fromPredicate | Converts a predicate and some arguments into a RedirectModifier . This can be used for the fork method in brigadier's ArgumentBuilder |
fromCommand | Converts a CommandAPICommand into a brigadier Command object |
fromArgument | Converts an argument, or a list of arguments, into a RequiredArgumentBuilder |
toSuggestions | Converts an argument's suggestions into brigadier's SuggestionProvider , with a list of previously declared arguments |
parseArguments | Parses a list of CommandAPI arguments into their respective objects for a provided CommandContext |
getBrigadierSourceFromCommandSender | Converts a Bukkit CommandSender into the NMS command sender source object |
getBukkitCommandSenderFromContext | Converts a Brigadier CommandContext into a Bukkit CommandSender |
I hope these examples help understand how the CommandAPI can help with registering more "powerful" commands with the use of brigadier as well! Please bear with with it - these examples can be long, but I'm certain that they've been explained well and will be useful!
+Say we wanted to add a predicate to the /execute
command. In this example, we'll create a predicate which handles random chances. To illustrate this, we want to be able to run commands such as:
/execute if randomchance 1 4 run say Hello!
+
+In this scenario, if we ran this command, we would expect "Hello!" to appear in the chat with a \(\frac{1}{4}\) chance. In particular, this is what we're trying to achieve:
+We want to create a predicate (true/false value) for the following syntax:
+randomchance <numerator> <denominator>
+
+We also want this predicate to come after execute if
:
\begin{gather} +\texttt{execute}\\ +\downarrow\\ +\texttt{if}\\ +\downarrow\\ +\texttt{randomchance <numerator}\texttt{> <denominator}\texttt{>} +\end{gather}
+After entering our predicate, we want to route back to execute
(because the argument after execute
is run
, which is used in our example command above):
\begin{gather} +\texttt{execute}\\ +\downarrow\\ +\texttt{if}\\ +\downarrow\\ +\texttt{randomchance <numerator}\texttt{> <denominator}\texttt{>}\\ +\downarrow\\ +\texttt{execute} +\end{gather}
+Now that we've established what we want, we can finally begin writing the code! First we want to create a literal randomchance
. It's a literal because literal values don't change (similar to say run
or if
from the /execute
command). To create a literal, we'll use the fromLiteralArgument
method described above, and then build it using the .build()
method:
// Register literal "randomchance"
+LiteralCommandNode randomChance = Brigadier.fromLiteralArgument(new LiteralArgument("randomchance")).build();
+
+// Register literal "randomchance"
+val randomChance: LiteralCommandNode<Any> = Brigadier.fromLiteralArgument(LiteralArgument("randomchance")).build()
+
+With that completed, we can now create our "argument" to this predicate. To do this, we'll use the regular declaration of arguments that we would normally use for commands. In this example, because we're computing \(\frac{numerator}{denominator}\), we want our numerator to be 0 or greater and our denominator to be 1 or greater (we don't want any negative numbers or division by zero!):
+// Declare arguments like normal
+Argument<Integer> numeratorArgument = new IntegerArgument("numerator", 0);
+Argument<Integer> denominatorArgument = new IntegerArgument("denominator", 1);
+
+List<Argument> arguments = new ArrayList<>();
+arguments.add(numeratorArgument);
+arguments.add(denominatorArgument);
+
+// Declare arguments like normal
+val numeratorArgument = IntegerArgument("numerator", 0)
+val denominatorArgument = IntegerArgument("denominator", 1)
+
+val arguments = listOf<Argument<*>>(numeratorArgument, denominatorArgument)
+
+Now we're going to get into the very nitty-gritty part - the predicate declaration. First, we'll create some variables numerator
and denominator
to represent the brigadier instances of these arguments. This can be handled by using the Brigadier.argBuildOf
function:
ArgumentBuilder numerator = Brigadier.fromArgument(numeratorArgument);
+ArgumentBuilder denominator = Brigadier.fromArgument(denominatorArgument)
+
+val numerator = Brigadier.fromArgument(numeratorArgument)
+val denominator = Brigadier.fromArgument(denominatorArgument)
+
+Now we'll define our predicate. Since this is sort of a "meta-command" (it directly affects the outcome of the run
command), we need to use the ArgumentBuilder
's fork
method. Remember that after we run this predicate, we want to link back to execute
again, so our first argument is the CommandNode
for execute
, which we can get using Brigadier.getRootNode().getChild("execute")
. Then, we can simply use Brigadier.fromPredicate
to finish our declaration:
ArgumentBuilder denominator = Brigadier.fromArgument(denominatorArgument)
+ // Fork redirecting to "execute" and state our predicate
+ .fork(Brigadier.getRootNode().getChild("execute"), Brigadier.fromPredicate((sender, args) -> {
+ // Parse arguments like normal
+ int num = (int) args[0];
+ int denom = (int) args[1];
+
+ // Return boolean with a num/denom chance
+ return Math.ceil(Math.random() * denom) <= num;
+ }, arguments));
+
+val denominator = Brigadier.fromArgument(denominatorArgument)
+ // Fork redirecting to "execute" and state our predicate
+ .fork(Brigadier.getRootNode().getChild("execute"), Brigadier.fromPredicate( { _: CommandSender, args ->
+ // Parse arguments like normal
+ val num = (args[0] as Int).toDouble()
+ val denom = (args[1] as Int).toDouble()
+
+ // Return boolean with a num/denom chance
+ Math.ceil(Math.random() * denom) <= num
+ }, arguments))
+
+Finally, we can now link everything up. We know that numerator
comes first, then denominator
, so we have to have numerator.then(denominator)
. We also know that these arguments are the children of the randomChance
literal, so we use the following code to state all of this:
// Add <numerator> <denominator> as a child of randomchance
+randomChance.addChild(numerator.then(denominator).build());
+
+// Add <numerator> <denominator> as a child of randomchance
+randomChance.addChild(numerator.then(denominator).build())
+
+Finally, we "register" the command. In this case, we're actually just adding the randomChance
node under \(\texttt{execute}\rightarrow\texttt{if}\), which we can add using the following code:
// Add (randomchance <numerator> <denominator>) as a child of (execute -> if)
+Brigadier.getRootNode().getChild("execute").getChild("if").addChild(randomChance);
+
+// Add (randomchance <numerator> <denominator>) as a child of (execute -> if)
+Brigadier.getRootNode().getChild("execute").getChild("if").addChild(randomChance)
+
+So, hopefully that wasn't too confusing! If you're still lost, here's the whole code that we wrote:
+// Register literal "randomchance"
+LiteralCommandNode randomChance = Brigadier.fromLiteralArgument(new LiteralArgument("randomchance")).build();
+
+// Declare arguments like normal
+Argument<Integer> numeratorArgument = new IntegerArgument("numerator", 0);
+Argument<Integer> denominatorArgument = new IntegerArgument("denominator", 1);
+
+List<Argument> arguments = new ArrayList<>();
+arguments.add(numeratorArgument);
+arguments.add(denominatorArgument);
+
+// Get brigadier argument objects
+ArgumentBuilder numerator = Brigadier.fromArgument(numeratorArgument);
+ArgumentBuilder denominator = Brigadier.fromArgument(denominatorArgument)
+ // Fork redirecting to "execute" and state our predicate
+ .fork(Brigadier.getRootNode().getChild("execute"), Brigadier.fromPredicate((sender, args) -> {
+ // Parse arguments like normal
+ int num = (int) args[0];
+ int denom = (int) args[1];
+
+ // Return boolean with a num/denom chance
+ return Math.ceil(Math.random() * denom) <= num;
+ }, arguments));
+
+// Add <numerator> <denominator> as a child of randomchance
+randomChance.addChild(numerator.then(denominator).build());
+
+// Add (randomchance <numerator> <denominator>) as a child of (execute -> if)
+Brigadier.getRootNode().getChild("execute").getChild("if").addChild(randomChance);
+
+// Register literal "randomchance"
+val randomChance: LiteralCommandNode<Any> = Brigadier.fromLiteralArgument(LiteralArgument("randomchance")).build()
+
+// Declare arguments like normal
+val numeratorArgument = IntegerArgument("numerator", 0)
+val denominatorArgument = IntegerArgument("denominator", 1)
+
+val arguments = listOf<Argument<*>>(numeratorArgument, denominatorArgument)
+
+// Get brigadier argument objects
+val numerator = Brigadier.fromArgument(numeratorArgument)
+val denominator = Brigadier.fromArgument(denominatorArgument)
+ // Fork redirecting to "execute" and state our predicate
+ .fork(Brigadier.getRootNode().getChild("execute"), Brigadier.fromPredicate( { _: CommandSender, args ->
+ // Parse arguments like normal
+ val num = (args[0] as Int).toDouble()
+ val denom = (args[1] as Int).toDouble()
+
+ // Return boolean with a num/denom chance
+ Math.ceil(Math.random() * denom) <= num
+ }, arguments))
+
+// Add <numerator> <denominator> as a child of randomchance
+randomChance.addChild(numerator.then(denominator).build())
+
+// Add (randomchance <numerator> <denominator>) as a child of (execute -> if)
+Brigadier.getRootNode().getChild("execute").getChild("if").addChild(randomChance)
+
+As described in The ArgumentSuggestions interface, the ArgumentSuggestions
interface has the following default method:
@FunctionalInterface
+public interface ArgumentSuggestions<CommandSender> {
+
+ /**
+ * Create a {@link CompletableFuture} resolving onto a brigadier {@link Suggestions} object.
+ * @param info The suggestions info
+ * @param builder The Brigadier {@link SuggestionsBuilder} object
+ * @return a {@link CompletableFuture} resolving onto a brigadier {@link Suggestions} object.
+ *
+ * @throws CommandSyntaxException if there is an error making suggestions
+ */
+ CompletableFuture<Suggestions> suggest(SuggestionInfo<CommandSender> info, SuggestionsBuilder builder)
+ throws CommandSyntaxException;
+
+}
+
+This allows you to use Brigadier's SuggestionsBuilder
and Suggestions
classes to create more powerful suggestions beyond the basic capabilities of the CommandAPI.
In order to use this, you will need the Brigadier dependency, which you can find under the Brigadier installation instructions.
+Say we want to let users broadcast a message, but also allow them to enter emojis into the message they're typing:
+ +For this command, we'll use a GreedyStringArgument
as if we were making a generic broadcasted message. We create a map of emojis to their descriptions to use as tooltips and then we use Brigadier to display the suggestions at the end of the message where the cursor is.
Map<String, String> emojis = new HashMap<>();
+emojis.put("☻", "smile");
+emojis.put("❤", "heart");
+emojis.put("🔥", "fire");
+emojis.put("★", "star");
+emojis.put("☠", "death");
+emojis.put("⚠", "warning");
+emojis.put("☀", "sun");
+emojis.put("☺", "smile");
+emojis.put("☹", "frown");
+emojis.put("✉", "mail");
+emojis.put("☂", "umbrella");
+emojis.put("✘", "cross");
+emojis.put("♪", "music note (eighth)");
+emojis.put("♬", "music note (beamed sixteenth)");
+emojis.put("♩", "music note (quarter)");
+emojis.put("♫", "music note (beamed eighth)");
+emojis.put("☄", "comet");
+emojis.put("✦", "star");
+emojis.put("🗡", "sword");
+emojis.put("🪓", "axe");
+emojis.put("🔱", "trident");
+emojis.put("🎣", "fishing rod");
+emojis.put("🏹", "bow");
+emojis.put("⛏", "pickaxe");
+emojis.put("🍖", "food");
+
+Argument<String> messageArgument = new GreedyStringArgument("message")
+ .replaceSuggestions((info, builder) -> {
+ // Only display suggestions at the very end character
+ builder = builder.createOffset(builder.getStart() + info.currentArg().length());
+
+ // Suggest all the emojis!
+ for (Entry<String, String> str : emojis.entrySet()) {
+ builder.suggest(str.getKey(), new LiteralMessage(str.getValue()));
+ }
+
+ return builder.buildFuture();
+ });
+
+new CommandAPICommand("emoji")
+ .withArguments(messageArgument)
+ .executes((sender, args) -> {
+ Bukkit.broadcastMessage((String) args.get("message"));
+ })
+ .register();
+
+val emojis = mapOf(
+ "☻" to "smile",
+ "❤" to "heart",
+ "🔥" to "fire",
+ "★" to "star",
+ "☠" to "death",
+ "⚠" to "warning",
+ "☀" to "sun",
+ "☺" to "smile",
+ "☹" to "frown",
+ "✉" to "mail",
+ "☂" to "umbrella",
+ "✘" to "cross",
+ "♪" to "music note (eighth)",
+ "♬" to "music note (beamed sixteenth)",
+ "♩" to "music note (quarter)",
+ "♫" to "music note (beamed eighth)",
+ "☄" to "comet",
+ "✦" to "star",
+ "🗡" to "sword",
+ "🪓" to "axe",
+ "🔱" to "trident",
+ "🎣" to "fishing rod",
+ "🏹" to "bow",
+ "⛏" to "pickaxe",
+ "🍖" to "food"
+)
+
+val messageArgument = GreedyStringArgument("message")
+ .replaceSuggestions { info, builder ->
+ // Only display suggestions at the very end character
+ val newBuilder = builder.createOffset(builder.start + info.currentArg().length)
+
+ // Suggest all the emojis!
+ emojis.forEach { (emoji, description) ->
+ newBuilder.suggest(emoji, LiteralMessage(description))
+ }
+
+ newBuilder.buildFuture()
+ }
+
+CommandAPICommand("emoji")
+ .withArguments(messageArgument)
+ .executes(CommandExecutor { _, args ->
+ Bukkit.broadcastMessage(args["message"] as String)
+ })
+ .register()
+
+In this example, we simply create the GreedyStringArgument
and use replaceSuggestions()
to specify our suggestion rules. We create an offset using the current builder to make suggestions start at the last character (the current builder start builder.getStart()
and the current length of what the user has already typed info.currentArg().length()
). Finally, we build the suggestions with builder.buildFuture()
and then register our command as normal.
++Developer's Note:
+This example has been superseded by the Command argument. This example is still present as it gives an example of much more complicated brigadier suggestions which may be useful for readers!
+
Courtesy of 469512345, the following example shows how using Brigadier's suggestions and parser can be combined with the CommandAPI to create an argument which suggests valid Minecraft commands. This could be used for example as a sudo
command, to run a command as another player.
For this command, we'll use a GreedyStringArgument
because that allows users to enter any combination of characters (which therefore, allows users to enter any command). First, we start by defining the suggestions that we'll use for the GreedyStringArgument
. We'll use the ArgumentSuggestions
functional interface described above:
ArgumentSuggestions<CommandSender> commandSuggestions = (info, builder) -> {
+ // The current argument, which is a full command
+ String arg = info.currentArg();
+
+ // Identify the position of the current argument
+ int start;
+ if (arg.contains(" ")) {
+ // Current argument contains spaces - it starts after the last space and after the start of this argument.
+ start = builder.getStart() + arg.lastIndexOf(' ') + 1;
+ } else {
+ // Input starts at the start of this argument
+ start = builder.getStart();
+ }
+
+ // Parse command using brigadier
+ ParseResults<?> parseResults = Brigadier.getCommandDispatcher()
+ .parse(info.currentArg(), Brigadier.getBrigadierSourceFromCommandSender(info.sender()));
+
+ // Intercept any parsing errors indicating an invalid command
+ if(!parseResults.getExceptions().isEmpty()) {
+ CommandSyntaxException exception = parseResults.getExceptions().values().iterator().next();
+ // Raise the error, with the cursor offset to line up with the argument
+ throw new CommandSyntaxException(exception.getType(), exception.getRawMessage(), exception.getInput(), exception.getCursor() + start);
+ }
+
+ return Brigadier
+ .getCommandDispatcher()
+ .getCompletionSuggestions(parseResults)
+ .thenApply(suggestionsObject -> {
+ // Brigadier's suggestions
+ Suggestions suggestions = (Suggestions) suggestionsObject;
+
+ return new Suggestions(
+ // Offset the index range of the suggestions by the start of the current argument
+ new StringRange(start, start + suggestions.getRange().getLength()),
+ // Copy the suggestions
+ suggestions.getList()
+ );
+ });
+};
+
+val commandSuggestions: ArgumentSuggestions<CommandSender> = ArgumentSuggestions { info, builder ->
+ // The current argument, which is a full command
+ val arg: String = info.currentArg()
+
+ // Identify the position of the current argument
+ var start = if (arg.contains(" ")) {
+ // Current argument contains spaces - it starts after the last space and after the start of this argument.
+ builder.start + arg.lastIndexOf(' ') + 1
+ } else {
+ // Input starts at the start of this argument
+ builder.start
+ }
+
+ // Parse command using brigadier
+ val parseResults: ParseResults<*> = Brigadier.getCommandDispatcher()
+ .parse(info.currentArg(), Brigadier.getBrigadierSourceFromCommandSender(info.sender))
+
+ // Intercept any parsing errors indicating an invalid command
+ for ((_, exception) in parseResults.exceptions) {
+ // Raise the error, with the cursor offset to line up with the argument
+ throw CommandSyntaxException(exception.type, exception.rawMessage, exception.input, exception.cursor + start)
+ }
+
+ val completableFutureSuggestions: CompletableFuture<Suggestions> =
+ Brigadier.getCommandDispatcher().getCompletionSuggestions(parseResults) as CompletableFuture<Suggestions>
+
+ completableFutureSuggestions.thenApply { suggestions: Suggestions ->
+ Suggestions(
+ // Offset the index range of the suggestions by the start of the current argument
+ StringRange(start, start + suggestions.range.length),
+ // Copy the suggestions
+ suggestions.list
+ )
+ }
+}
+
+There's a lot to unpack there, but it's generally split up into 4 key sections:
+Finding the start of the argument. We find the start of the argument so we know where the beginning of our command suggestion is. This is done easily using builder.getStart()
, but we also have to take into account any spaces if our command argument contains spaces.
Parsing the command argument. We make use of Brigadier's parse()
method to parse the argument and generate some ParseResults
.
Reporting parsing errors. This is actually an optional step, but in general it's good practice to handle exceptions stored in ParseResults
. While Brigadier doesn't actually handle suggestion exceptions, this has been included in this example to showcase exception handling.
Generating suggestions from parse results. We use our parse results with Brigadier's getCompletionSuggestions()
method to generate some suggestions based on the parse results and the suggestion string range.
Now that we've declared our arguments suggestions, we can then create our simple command with the following syntax:
+/commandargument <command>
+
+We use the command suggestions declared above by using the replaceSuggestions
method in our GreedyStringArgument
, and write a simple executor which runs the command that the user provided:
new CommandAPICommand("commandargument")
+ .withArguments(new GreedyStringArgument("command").replaceSuggestions(commandSuggestions))
+ .executes((sender, args) -> {
+ // Run the command using Bukkit.dispatchCommand()
+ Bukkit.dispatchCommand(sender, (String) args.get("command"));
+ }).register();
+
+CommandAPICommand("commandargument")
+ .withArguments(GreedyStringArgument("command").replaceSuggestions(commandSuggestions))
+ .executes(CommandExecutor { sender, args ->
+ // Run the command using Bukkit.dispatchCommand()
+ Bukkit.dispatchCommand(sender, args["command"] as String)
+ })
+ .register()
+
+In our example for creating a party system, we ended up having lots of code repetition. In our party creation command, we had the following code:
+List<Argument<?>> arguments = new ArrayList<>();
+
+// The "create" literal, with a requirement that a player must have a party
+arguments.add(new LiteralArgument("create")
+ .withRequirement(sender -> !partyMembers.containsKey(((Player) sender).getUniqueId()))
+);
+
+arguments.add(new StringArgument("partyName"));
+
+var arguments = mutableListOf<Argument<*>>()
+
+// The "create" literal, with a requirement that a player must have a party
+arguments.add(LiteralArgument("create")
+ .withRequirement { !partyMembers.containsKey((it as Player).uniqueId) }
+)
+
+arguments.add(StringArgument("partyName"))
+
+And for our party teleportation command, we had the following code:
+arguments = new ArrayList<>();
+arguments.add(new LiteralArgument("tp")
+ .withRequirement(sender -> partyMembers.containsKey(((Player) sender).getUniqueId()))
+);
+
+arguments = mutableListOf<Argument<*>>()
+arguments.add(LiteralArgument("tp")
+ .withRequirement { partyMembers.containsKey((it as Player).uniqueId) })
+
+We can simplify this code by declaring the predicate:
+Predicate<CommandSender> testIfPlayerHasParty = sender -> {
+ return partyMembers.containsKey(((Player) sender).getUniqueId());
+};
+
+val testIfPlayerHasParty = Predicate { sender: CommandSender ->
+ partyMembers.containsKey((sender as Player).uniqueId)
+}
+
+Now, we can use the predicate testIfPlayerHasParty
in our code for creating a party. Since we want to apply the "not" (!
) operator to this predicate, we can use .negate()
to invert the result of our predicate:
List<Argument<?>> args = new ArrayList<>();
+args.add(new LiteralArgument("create").withRequirement(testIfPlayerHasParty.negate()));
+args.add(new StringArgument("partyName"));
+
+var args = listOf<Argument<*>>(
+ LiteralArgument("create").withRequirement(testIfPlayerHasParty.negate()),
+ StringArgument("partyName")
+)
+
+And we can use it again for our code for teleporting to party members:
+args = new ArrayList<>();
+args.add(new LiteralArgument("tp").withRequirement(testIfPlayerHasParty));
+
+args = listOf<Argument<*>>(LiteralArgument("tp").withRequirement(testIfPlayerHasParty))
+
+++Dev note:
+Coming soon! In this section, I'll outline how to get started with building and contributing to the CommandAPI.
+
The CommandAPI is a relatively large project (especially from the standpoint of one guy, because the CommandAPI was written by just one guy in their spare time!) and trying to figure out what everything does is a nightmare without some guidance. I've always felt that other community project structures aren't well documented and contributing to them can be daunting. Here's the CommandAPI's project structure for you!
+This is where all of the code is for the CommandAPI. The CommandAPI is a Maven project with multiple modules which each serve a different purpose:
+commandapi-preprocessor
- The CommandAPI uses a bit of reflection to perform things which could not normally be done (for example, allowing custom commands in datapacks). Reflection is inherently unsafe and can lead to runtime errors if specific fields or methods are not present. The CommandAPI preprocessor project is a source annotation processor that checks all declared reflection calls and looks up at compile-time whether those calls are possible - if not, it prevents the CommandAPI from building. In short, it's a compile-time reflection checker.
commandapi-x.x.x
- The CommandAPI needs to access various NMS methods in order to operate. These are implemented for the specific version given by x.x.x
. For example, to support Minecraft 1.16.5
, the project is commandapi-1.16.5
. The NMS
class implementation is done in these version-specific files.
commandapi-core
- The main brains of the CommandAPI. This includes both the code that makes the CommandAPI run, as well as the API which developers can use.
commandapi-vh
- The CommandAPI version handler. This is a super tiny project which simply links up all of the NMS version-specific files into the CommandAPI. This is only used for the actual running of the CommandAPI (e.g. the CommandAPI plugin or shading the CommandAPI). This ensures proper compile-time safety of NMS implementations.
commandapi-plugin
- It's the CommandAPI plugin! This is the project which is used for releases to both GitHub and Spigot. It's the CommandAPI all in one neat package, with a few extra features such as config-based command conversion for server owners (or other non-developers)
commandapi-shade
- It's the CommandAPI, but in shade-able format. It has none of the features of the CommandAPI plugin variant and can be shaded into your own plugins. Effectively, it's commandapi-core
+ commandapi-vh
with all of the commandapi-x.x.x
NMS implementations included.
commandapi-annotations
- The CommandAPI annotations project is a small compile-time annotation processer that writes CommandAPI code for you. Using a compile-time annotation processor makes the server run so much faster than using a runtime-annotation processor, because annotation processing requires reflection to inspect class metadata.
docs
folderThis is where all of the lovely documentation, JavaDocs and the CommandAPI homepage is stored. Everything in this folder is automatically hosted on GitHub using GitHub Pages - this is the live stuff. Key things in this folder:
+javadocs
- It's the JavaDocs! This is built using Doxygen with the configuration in the root of this project (See the Doxyfile
). I also serve a custom version of Mojang's Brigadier library which was written by I-Al-Istannen because this person spent a lot of time and effort producing JavaDocs for Brigadier when Mojang didn't bother. This person deserves a lot of respect - their work is amazing!
index.html
- It's the CommandAPI's homepage! It's not anything especially amazing - it's a very very simple static webpage with links - that's all that it needs to be. Every update, this file needs to be edited manually to update all of the links.
docssrc
folderThis is the brains behind the documentation. It's a collection of Markdown files which form the CommandAPI documentation and is built using mdBook. For the CommandAPI in particular, it is actually built using my own custom fork of mdBook (here, in the fa5 branch) which incorporates this pull request which adds support for FontAwesome 5.
+The BukkitTooltip.generateAdvenureComponents
methods have now been deprecated in favour of the correctly named BukkitTooltip.generateAdventureComponents
methods:
BukkitTooltip.generateAdvenureComponents()
+
+$$\downarrow$$
+BukkitTooltip.generateAdventureComponents()
+
+All MultiLiteralArgument
constructors have been deprecated in 9.1.0! Instead the new MultiLiteralArgument
constructor should be used:
withArguments(new MultiLiteralArgument("gamemodes", List.of("survival", "creative", "adventure", "spectator")))
+
+$$\downarrow$$
+withArguments(new MultiLiteralArgument("gamemodes", "survival", "creative", "adventure", "spectator"))
+
+For 9.1.0 all deprecated methods are no longer deprecated. To learn about all the methods now available, refer to the CommandArguments page.
+In previous versions, the ability has been introduced to access arguments by their node names. However, while this was possible for every other argument, it wasn't possible for MultiLiteralArgument
s. This was now changed because the values from the MultiLiteralArgument
are included in the CommandArguments
of a command.
Therefore, the current constructor has been deprecated and the new one should be used:
+withArguments(new MultiLiteralArgument("survival", "creative", "adventure", "spectator"))
+
+$$\downarrow$$
+withArguments(new MultiLiteralArgument("gamemodes", List.of("survival", "creative", "adventure", "spectator")))
+
+Because it is possible to list LiteralArgument
s in the CommandArguments
of a command, there was also an additional constructor add to the LiteralArgument
class. The other one is not deprecated.
Now, the LiteralArgument
class contains two possible constructors:
public LiteralArgument(String literal) // Recommended if the literal is not listed
+public LiteralArgument(String nodeName, String literal)
+
+For 9.0.1, the CustomArgumentException
constructors have been deprecated and should no longer be used. Instead, use the CustomArgumentException
static factory methods:
throw new CustomArgumentException(new MessageBuilder(...));
+throw new CustomArgumentException("Error message");
+
+$$\downarrow$$
+throw CustomArgumentException.fromMessageBuilder(new MessageBuilder(...));
+throw CustomArgumentException.fromString("Error message");
+
+For 9.0.1 the various CommandArguments#getOrDefault()
and CommandArguments#getOrDefaultUnchecked()
have been deprecated and should no longer be used. Instead, use the CommandArguments#getOptional()
and CommandArguments#getOptionalUnchecked()
methods:
new CommandAPICommand("mycommand")
+ .withOptionalArguments(new StringArgument("string"))
+ .executes((sender, args) -> {
+ String string = (String) args.getOrDefault("string", "Default Value");
+ })
+ .register();
+
+new CommandAPICommand("mycommand")
+ .withOptionalArguments(new StringArgument("string"))
+ .executes((sender, args) -> {
+ String string = args.getOrDefaultUnchecked("string", "Default Value");
+ })
+ .register();
+
+$$\downarrow$$
+new CommandAPICommand("mycommand")
+ .withOptionalArguments(new StringArgument("string"))
+ .executes((sender, args) -> {
+ String string = (String) args.getOptional("string").orElse("Default Value");
+ })
+ .register();
+
+new CommandAPICommand("mycommand")
+ .withOptionalArguments(new StringArgument("string"))
+ .executes((sender, args) -> {
+ String string = args.getOptionalUnchecked("string").orElse("Default Value");
+ })
+ .register();
+
+CommandAPI 9.0.0 is arguably the biggest change in the CommandAPI's project structure and usage. This update was designed to allow the CommandAPI to be generalized for other platforms (e.g. Velocity, Fabric, Sponge), and as a result this update is incompatible with previous versions of the CommandAPI.
+All deprecated methods from 8.8.x have been removed in this update. Please ensure that you use the relevant replacement methods (these are described in the JavaDocs for the various deprecated methods) before upgrading to 9.0.0.
+For Bukkit/Spigot/Paper plugins, the commandapi-core
and commandapi-shade
modules should no longer be used. Instead, use the new commandapi-bukkit-core
and commandapi-bukkit-shade
modules:
<dependencies>
+ <dependency>
+ <groupId>dev.jorel</groupId>
+ <artifactId>commandapi-core</artifactId>
+ <version>9.0.0</version>
+ <scope>provided</scope>
+ </dependency>
+</dependencies>
+
+<dependencies>
+ <dependency>
+ <groupId>dev.jorel</groupId>
+ <artifactId>commandapi-shade</artifactId>
+ <version>9.0.0</version>
+ </dependency>
+</dependencies>
+
+dependencies {
+ compileOnly "dev.jorel:commandapi-core:9.0.0"
+}
+
+dependencies {
+ compileOnly("dev.jorel:commandapi-core:9.0.0")
+}
+
+dependencies {
+ implementation "dev.jorel:commandapi-shade:9.0.0"
+}
+
+dependencies {
+ implementation("dev.jorel:commandapi-shade:9.0.0")
+}
+
+$$\downarrow$$
+<dependencies>
+ <dependency>
+ <groupId>dev.jorel</groupId>
+ <artifactId>commandapi-bukkit-core</artifactId>
+ <version>9.0.0</version>
+ <scope>provided</scope>
+ </dependency>
+</dependencies>
+
+<dependencies>
+ <dependency>
+ <groupId>dev.jorel</groupId>
+ <artifactId>commandapi-bukkit-shade</artifactId>
+ <version>9.0.0</version>
+ </dependency>
+</dependencies>
+
+dependencies {
+ compileOnly "dev.jorel:commandapi-bukkit-core:9.0.0"
+}
+
+dependencies {
+ compileOnly("dev.jorel:commandapi-bukkit-core:9.0.0")
+}
+
+dependencies {
+ implementation "dev.jorel:commandapi-bukkit-shade:9.0.0"
+}
+
+dependencies {
+ implementation("dev.jorel:commandapi-bukkit-shade:9.0.0")
+}
+
+Additionally, when using the Kotlin DSL for Bukkit, instead of using commandapi-kotlin
, use commandapi-bukkit-kotlin
:
<dependencies>
+ <dependency>
+ <groupId>dev.jorel</groupId>
+ <artifactId>commandapi-kotlin</artifactId>
+ <version>9.0.0</version>
+ </dependency>
+</dependencies>
+
+dependencies {
+ implementation "dev.jorel:commandapi-kotlin:9.0.0"
+}
+
+dependencies {
+ implementation("dev.jorel:commandapi-kotlin:9.0.0")
+}
+
+$$\downarrow$$
+<dependencies>
+ <dependency>
+ <groupId>dev.jorel</groupId>
+ <artifactId>commandapi-bukkit-kotlin</artifactId>
+ <version>9.0.0</version>
+ </dependency>
+</dependencies>
+
+dependencies {
+ implementation "dev.jorel:commandapi-bukkit-kotlin:9.0.0"
+}
+
+dependencies {
+ implementation("dev.jorel:commandapi-bukkit-kotlin:9.0.0")
+}
+
+The CommandAPI.onLoad()
method has changed in this update. Instead of using the CommandAPIConfig
object, use the CommandAPIBukkitConfig
and pass in the current plugin reference (this
).
and CommandAPI.onEnable()
method has also changed, and now no longer requires the plugin reference (this
), as it is now included in CommandAPI.onLoad()
instead.:
public void onLoad() {
+ CommandAPI.onLoad(new CommandAPIConfig());
+}
+
+public void onEnable() {
+ CommandAPI.onEnable(this);
+}
+
+$$\downarrow$$
+public void onLoad() {
+ CommandAPI.onLoad(new CommandAPIBukkitConfig(this));
+}
+
+public void onEnable() {
+ CommandAPI.onEnable();
+}
+
+Arguments for commands are no longer an Object[]
and have now been replaced with a more powerful CommandArguments
object. This object now lets you access arguments in a number of ways:
args.get(int)
methodIf you're in a rush and just want to upgrade quickly, call the .get(int)
method instead of accessing the arguments using the array access notation:
new CommandAPICommand("cmd")
+ .withArguments(new StringArgument("mystring"))
+ .withArguments(new PotionEffectArgument("mypotion"))
+ .withArguments(new LocationArgument("mylocation"))
+ .executes((sender, args) -> {
+ String stringArg = (String) args[0];
+ PotionEffectType potionArg = (PotionEffectType) args[1];
+ Location locationArg = (Location) args[2];
+ })
+ .register();
+
+$$\downarrow$$
+new CommandAPICommand("cmd")
+ .withArguments(new StringArgument("mystring"))
+ .withArguments(new PotionEffectArgument("mypotion"))
+ .withArguments(new LocationArgument("mylocation"))
+ .executes((sender, args) -> {
+ String stringArg = (String) args.get(0);
+ PotionEffectType potionArg = (PotionEffectType) args.get(1);
+ Location locationArg = (Location) args.get(2);
+ })
+ .register();
+
+args.get(String)
method (recommended)The CommandAPI introduces a new args.get(String)
method to access arguments using the argument node name. This method also makes your code much more compatible with optional arguments:
new CommandAPICommand("cmd")
+ .withArguments(new StringArgument("mystring"))
+ .withArguments(new PotionEffectArgument("mypotion"))
+ .withArguments(new LocationArgument("mylocation"))
+ .executes((sender, args) -> {
+ String stringArg = (String) args[0];
+ PotionEffectType potionArg = (PotionEffectType) args[1];
+ Location locationArg = (Location) args[2];
+ })
+ .register();
+
+$$\downarrow$$
+new CommandAPICommand("cmd")
+ .withArguments(new StringArgument("mystring"))
+ .withArguments(new PotionEffectArgument("mypotion"))
+ .withArguments(new LocationArgument("mylocation"))
+ .executes((sender, args) -> {
+ String stringArg = (String) args.get("mystring");
+ PotionEffectType potionArg = (PotionEffectType) args.get("mypotion");
+ Location locationArg = (Location) args.get("mylocation");
+ })
+ .register();
+
+CommandAPI
helper methodsThe CommandAPI.failWithBaseComponents(message)
and CommandAPI.failWithAdventureComponent(message)
methods have now been moved from CommandAPI
to CommandAPIBukkit
, because these methods are Bukkit/Spigot/Paper specific and don't exist for other platforms (e.g. Velocity, Fabric, Sponge):
CommandAPI.failWithBaseComponents(...);
+CommandAPI.failWithAdventureComponent(...);
+
+$$\downarrow$$
+CommandAPIBukkit.failWithBaseComponents(...);
+CommandAPIBukkit.failWithAdventureComponent(...);
+
+EnvironmentArgument
The EnvironmentArgument
has been removed in this update, as it was implemented incorrectly and is not fit for purpose. Instead, the CommandAPI has the more accurate WorldArgument
.
TeamArgument
The TeamArgument
has been updated to no longer use a String
as its return type. Instead, you can now just use a Team
object directly:
new CommandAPICommand("team")
+ .withArguments(new TeamArgument("team"))
+ .executes((sender, args) -> {
+ String teamName = (String) args.get("team");
+ Team team = Bukkit.getScoreboardManager().getMainScoreboard().getTeam(teamName);
+ })
+ .register();
+
+$$\downarrow$$
+new CommandAPICommand("team")
+ .withArguments(new TeamArgument("team"))
+ .executes((sender, args) -> {
+ Team team = (Team) args.get("team");
+ })
+ .register();
+
+ObjectiveArgument
The ObjectiveArgument
has been updated to no longer use a String
as its return type. Instead, you can now just use an Objective
object directly:
new CommandAPICommand("objective")
+ .withArguments(new ObjectiveArgument("objective"))
+ .executes((sender, args) -> {
+ String objectiveName = (String) args.get("objective");
+ Objective objective = Bukkit.getScoreboardManager().getMainScoreboard().getObjective(objectiveName);
+ })
+ .register();
+
+$$\downarrow$$
+new CommandAPICommand("objective")
+ .withArguments(new ObjectiveArgument("objective"))
+ .executes((sender, args) -> {
+ Objective objective = (Objective) args.get("objective");
+ })
+ .register();
+
+ListArgumentBuilder
The ListArgumentBuilder
no longer has withList(Function<CommandSender, Collection<T>> list)
and instead uses SuggestionInfo
to have withList(Function<SuggestionInfo<CommandSender>, Collection<T>> list)
.
This now allows you to access more information when generating a list dynamically instead of just the command sender. To access the original command sender, you can use the sender()
method from SuggestionInfo
:
ListArgument<?> arg = new ListArgumentBuilder<>("values", ", ")
+ .withList(sender -> List.of("cat", "wolf", "axolotl", sender.getName()))
+ .withStringMapper()
+ .buildGreedy();
+
+$$\downarrow$$
+ListArgument<?> arg = new ListArgumentBuilder<>("values", ", ")
+ .withList(info -> List.of("cat", "wolf", "axolotl", info.sender().getName()))
+ .withStringMapper()
+ .buildGreedy();
+
+Rotation
wrapperThe Rotation
class now uses a constructor which has the yaw first, and the pitch second, instead of the pitch first and the yaw second.
new Rotation(20, 80); // Yaw = 80, Pitch = 20
+
+$$\downarrow$$
+new Rotation(20, 80); // Yaw = 20, Pitch = 80
+
+ScoreboardSlot
wrapperThe ScoreboardSlot
wrapper is now an enum that has direct support for sidebar team colors, via the SIDEBAR_TEAM_###
enum values, for example SIDEBAR_TEAM_RED
;
ScoreboardSlot slot = // Some ScoreboardSlot
+DisplaySlot displaySlot = slot.getDisplaySlot(); // Returns PLAYER_LIST, SIDEBAR or BELOW_NAME
+
+// Extract the color if necessary
+if (slot.hasTeamColor()) {
+ ChatColor color = slot.getTeamColor();
+}
+
+$$\downarrow$$
+ScoreboardSlot slot = // Some ScoreboardSlot
+DisplaySlot displaySlot = slot.getDisplaySlot(); // Returns PLAYER_LIST, BELOW_NAME or SIDEBAR_TEAM_###
+
+This update introduces a backwards-incompatible change to the SoundArgument
which was introduced in 8.6.0. SoundArguments no longer need a generic type parameter:
new SoundArgument<Sound>("sound")
+
+$$\downarrow$$
+new SoundArgument("sound")
+
+NamespacedKey
SoundArguments no longer need a SoundType
parameter, instead the NamespacedKey
constructor should be used:
new SoundArgument<NamespacedKey>("sound", SoundType.NAMESPACED_KEY)
+
+$$\downarrow$$
+new SoundArgument.NamespacedKey("sound")
+
+Entity selector arguments no longer need a generic type parameter or a EntitySelector
parameter. Instead, the corresponding constructor should be used instead:
new EntitySelectorArgument<Player>("target", EntitySelector.ONE_PLAYER)
+new EntitySelectorArgument<Collection<Player>>("target", EntitySelector.MANY_PLAYERS)
+
+new EntitySelectorArgument<Entity>("target", EntitySelector.ONE_ENTITY)
+new EntitySelectorArgument<Collection<Entity>>("target", EntitySelector.MANY_ENTITIES)
+
+$$\downarrow$$
+new EntitySelectorArgument.OnePlayer("target")
+new EntitySelectorArgument.ManyPlayers("target")
+
+new EntitySelectorArgument.OneEntity("target")
+new EntitySelectorArgument.ManyEntities("target")
+
+The ScoreHolderArgument
no longer needs a generic type parameter or a ScoreHolderType
parameter. Instead, the corresponding constructor should be used instead:
new ScoreHolderArgument<String>(nodeName, ScoreHolderType.SINGLE);
+new ScoreHolderArgument<Collection<String>>(nodeName, ScoreHolderType.MULTIPLE);
+
+$$\downarrow$$
+new ScoreHolderArgument.Single(nodeName);
+new ScoreHolderArgument.Multiple(nodeName);
+
+In 8.6.0, the SoundArgument
now supports returning a Sound
or NamespacedKey
object. More information on how to use this can be found in the Sound arguments page. SoundArgument
objects now require a generic type parameter to specify what the return type will be (either Sound
or NamespacedKey
). For the default behaviour, use Sound
as the generic type parameter:
new SoundArgument("sound")
+
+$$\downarrow$$
+new SoundArgument<Sound>("sound")
+
+In 8.6.0, tooltips have been reworked to provide support for Spigot's BaseComponent[]
s, and Adventure's Component
s. As a result, the default method StringTooltip.of()
and Tooltip.of()
have been deprecated in favour of the better named StringTooltip.ofString()
and Tooltip.ofString()
methods:
StringTooltip.of("wave", "Waves at a player")
+
+Tooltip.of(player.getWorld().getSpawnLocation(), "World spawn")
+
+$$\downarrow$$
+StringTooltip.ofString("wave", "Waves at a player")
+
+Tooltip.ofString(player.getWorld().getSpawnLocation(), "World spawn")
+
+Additionally, the IStringTooltip
interface's getTooltip
method was changed to return a Brigadier Message
object instead of a String
. To use the IStringTooltip
directly, you now have to add Brigadier to your project's dependencies (info on how to do that can be found here).
You can use the Tooltip.messageFromString(String)
to easily upgrade to the new Message
return type:
@Override
+public String getTooltip() {
+ return this.itemstack.getItemMeta().getLore().get(0);
+}
+
+$$\downarrow$$
+@Override
+public Message getTooltip() {
+ return Tooltip.messageFromString(this.itemstack.getItemMeta().getLore().get(0));
+}
+
+To support Spigot's BaseComponent[]
s and Adventure's Component
s, the CommandAPI.fail()
method has now been deprecated in favour of the better named CommandAPI.failWithString()
method:
throw CommandAPI.fail("Error message");
+
+$$\downarrow$$
+throw CommandAPI.failWithString("Error message");
+
+List arguments can now be implemented using an underlying text argument, instead of requiring it to be a greedy string. This allows you to use multiple lists in a command, in any position. As such, the ListArgumentBuilder.build()
method has been deprecated and replaced with ListArgumentBuilder.buildGreedy()
instead:
new ListArgumentBuilder<Material>("materials")
+ .withList(List.of(Material.values()))
+ .withMapper(material -> material.name().toLowerCase())
+ .build();
+
+$$\downarrow$$
+new ListArgumentBuilder<Material>("materials")
+ .withList(List.of(Material.values()))
+ .withMapper(material -> material.name().toLowerCase())
+ .buildGreedy();
+
+In 8.5.1, the methods Brigadier.fromArgument
and Brigadier.toSuggestions
were changed to use Argument
based parameters instead of String
based parameters. Instead of providing the node name, you now have to provide the whole argument:
Argument<?> myArgument = new StringArgument("myargument");
+List<Argument<?>> argumentList = List.of(myArgument);
+
+Brigadier.fromArgument(argumentList, "myargument");
+Brigadier.toSuggestions("myargument", argumentList);
+
+$$\downarrow$$
+Argument<?> myArgument = new StringArgument("myargument");
+List<Argument<?>> argumentList = List.of(myArgument);
+
+Brigadier.fromArgument(argumentList, myArgument);
+Brigadier.toSuggestions(myArgument, argumentList);
+
+In 8.2.1, the CommandAPI exposed CommandAPIHandler.getInstance().registeredCommands
to get a list of registered commands. This has now been changed and properly implemented as a getter method which can be accessed from CommandAPI
:
CommandAPIHandler.getInstance().registeredCommands
+
+$$\downarrow$$
+CommandAPI.getRegisteredCommands()
+
+The import for EntitySelector
for the EntitySelectorArgument
has moved to improve CommandAPI shading support with jar minimization:
dev.jorel.commandapi.arguments.EntitySelectorArgument.EntitySelector
+
+$$\downarrow$$
+dev.jorel.commandapi.arguments.EntitySelector
+
+Custom arguments are no longer restricted to a string-based argument or a keyed-based argument and can now be implemented over any existing argument "base". This argument is now parameterized over two types: the first type being the return type of this custom argument and the second type being the return type of the "base" argument. Custom arguments should now use the new constructor that accepts an argument - more information on how to do that can be found on the Custom arguments page. It's recommended to review your implementation of custom arguments and upgrade them if you feel that you need a more powerful argument parser (for example, you might want to use a greedy string argument as the base argument instead of a string argument).
+Custom arguments that are not keyed can be drop-in replaced with a StringArgument
:
new CustomArgument<T>("nodename", inputInfo -> {
+ // Code here
+ return T;
+});
+
+$$\downarrow$$
+new CustomArgument<T, String>(new StringArgument("nodename"), inputInfo -> {
+ // Code here
+ return T;
+});
+
+Keyed custom arguments can be drop-in replaced with a NamespacedKeyArgument
:
new CustomArgument<T>("nodename", inputInfo -> {
+ // Code here
+ return T;
+}, true);
+
+$$\downarrow$$
+new CustomArgument<T, NamespacedKey>(new NamespacedKeyArgument("nodename"), inputInfo -> {
+ // Code here
+ return T;
+});
+
+NBT arguments now have a different implementation if you're using the plugin version of the CommandAPI or shading the CommandAPI.
+NBTCompoundArguments are now parameterized over their implemented NBTCompound implementation. For the NBT API, this means:
+new NBTCompoundArgument("nbt");
+
+$$\downarrow$$
+new NBTCompoundArgument<NBTContainer>("nbt");
+
+You no longer have to include the NBT API separately, the CommandAPI comes with the NBT API built-in:
+de.tr7zw.nbtapi.NBTContainer
+
+$$\downarrow$$
+dev.jorel.commandapi.nbtapi.NBTContainer
+
+You now need to shade the NBT API into your plugin (as well as the CommandAPI). So the CommandAPI knows how to use the underlying implementation of the NBT API, you have to configure it using the CommandAPIConfig.initializeNBTAPI()
method in CommandAPI.onLoad()
. More information on how to do that can be found on the NBT arguments page, under Shading usage setup.
Arguments are now parameterized over a generic type. This does very little in terms of the running of the CommandAPI, but does ensure type safety with its internals. Instances of the Argument
type now have to be parameterized. In general, this basically means:
Argument myArgument = new GreedyStringArgument("arg");
+
+$$\downarrow$$
+Argument<?> myArgument = new GreedyStringArgument("arg");
+
+Arguments that have multiple return types also need to be parameterized over their return type. This includes:
+CustomArgument
EntitySelectorArgument
ScoreholderArgument
For example:
+new EntitySelectorArgument("target", EntitySelector.ONE_PLAYER);
+
+$$\downarrow$$
+new EntitySelectorArgument<Player>("target", EntitySelector.ONE_PLAYER);
+
+Particle arguments no longer return Bukkit's org.bukkit.Particle
enum, but now return a wrapper dev.jorel.commandapi.wrappers.ParticleData
instead. More information about this wrapper class and how to use it can be found on the particle arguments page. To update, change any Particle
casts into a ParticleData
cast instead:
new CommandAPICommand("mycommand")
+ .withArgument(new ParticleArgument("particle"))
+ .executes((sender, args) -> {
+ Particle particle = (Particle) args[0];
+ // Do stuff with particle
+ })
+ .register();
+
+$$\downarrow$$
+new CommandAPICommand("mycommand")
+ .withArgument(new ParticleArgument("particle"))
+ .executes((sender, args) -> {
+ ParticleData particleData = (ParticleData) args[0];
+
+ Particle particle = particleData.particle();
+ Object data = particleData.data();
+
+ // Do stuff with particle and data
+ })
+ .register();
+
+The Maven repository used to serve the CommandAPI has changed from JitPack.io to Maven Central. For Maven projects, you no longer require wan explicit <repository>
entry for the CommandAPI. for Gradle projects, you need to ensure mavenCentral()
in present in your repositories
section.
The group ID has changed from dev.jorel.CommandAPI
to dev.jorel
More information about setting up your development environment can be found in Setting up your development environment.
+The CommandAPI.fail()
no longer automatically throws the exception that it creates, and instead now requires you to manually throw the exception yourself. This improves upon invalid states in command executors and allows invalid states to be identified more easily at compile time. To update, simply add the throw
keyword before you call CommandAPI.fail()
:
new CommandAPICommand("mycommand")
+ .executes((sender, args) -> {
+ if(!sender.hasPermission("some.permission")) {
+ CommandAPI.fail("You don't have permission to run /mycommand!");
+ return;
+ }
+ sender.sendMessage("Hello!");
+ })
+
+$$\downarrow$$
+new CommandAPICommand("mycommand")
+ .executes((sender, args) -> {
+ if(!sender.hasPermission("some.permission")) {
+ throw CommandAPI.fail("You don't have permission to run /mycommand!");
+ }
+ sender.sendMessage("Hello!");
+ })
+
+Suggestions have been overhauled and no longer take in a Function<SuggestionsInfo, String[]>
anymore. Instead, they now take in a ArgumentSuggestions
object which represents argument suggestions (and whether they are executed asynchronously or have tooltips).
These normal suggestions methods have been replaced with an ArgumentSuggestions
parameter instead of a function:
Argument replaceSuggestions(Function<SuggestionInfo, String[]> suggestions);
+Argument includeSuggestions(Function<SuggestionInfo, String[]> suggestions);
+
+$$\downarrow$$
+Argument replaceSuggestions(ArgumentSuggestions suggestions);
+Argument includeSuggestions(ArgumentSuggestions suggestions);
+
+The same functionality can be reproduced by wrapping your existing functions in ArgumentSuggestions.strings
:
List<Argument> arguments = new ArrayList<>();
+arguments.add(new StringArgument("world").replaceSuggestions(info ->
+ new String[] {"northland", "eastland", "southland", "westland" }
+));
+
+$$\downarrow$$
+List<Argument> arguments = new ArrayList<>();
+arguments.add(new StringArgument("world").replaceSuggestions(ArgumentSuggestions.strings(info ->
+ new String[] {"northland", "eastland", "southland", "westland" }
+)));
+
+The ...T()
methods have been replaced with the normal methods above, and can use the ArgumentSuggestions.stringsWithTooltips
method:
Argument replaceSuggestionsT(Function<SuggestionInfo, IStringTooltip[]> suggestions);
+Argument includeSuggestionsT(Function<SuggestionInfo, IStringTooltip[]> suggestions);
+
+$$\downarrow$$
+Argument replaceSuggestions(ArgumentSuggestions suggestions);
+Argument includeSuggestions(ArgumentSuggestions suggestions);
+
+For example:
+List<Argument> arguments = new ArrayList<>();
+arguments.add(new StringArgument("emote")
+ .replaceSuggestionsT( info -> new IStringTooltip[] {
+ StringTooltip.of("wave", "Waves at a player"),
+ StringTooltip.of("hug", "Gives a player a hug"),
+ StringTooltip.of("glare", "Gives a player the death glare")
+ }
+ )
+);
+
+$$\downarrow$$
+List<Argument> arguments = new ArrayList<>();
+arguments.add(new StringArgument("emote")
+ .replaceSuggestions(ArgumentSuggestions.stringsWithTooltips(info -> new IStringTooltip[] {
+ StringTooltip.of("wave", "Waves at a player"),
+ StringTooltip.of("hug", "Gives a player a hug"),
+ StringTooltip.of("glare", "Gives a player the death glare")
+ }
+ ))
+);
+
+Similar to above with normal suggestions, safe suggestions have been replaced with replaceSafeSuggestions
and includeSafeSuggestions
respectively:
Argument replaceWithSafeSuggestions(Function<SuggestionInfo, S[]> suggestions);
+Argument includeWithSafeSuggestions(Function<SuggestionInfo, S[]> suggestions);
+
+$$\downarrow$$
+Argument replaceSafeSuggestions(SafeSuggestions<T> suggestions);
+Argument includeSafeSuggestions(SafeSuggestions<T> suggestions);
+
+These can be used with the SafeSuggestions.suggest
and SafeSuggestions.tooltips
methods to wrap existing functions. For example:
List<Argument> arguments = new ArrayList<>();
+arguments.add(new RecipeArgument("recipe").replaceWithSafeSuggestions(info ->
+ new Recipe[] { emeraldSwordRecipe, /* Other recipes here */ }
+));
+
+$$\downarrow$$
+List<Argument> arguments = new ArrayList<>();
+arguments.add(new RecipeArgument("recipe").replaceSafeSuggestions(SafeSuggestions.suggest(info ->
+ new Recipe[] { emeraldSwordRecipe, /* Other recipes here */ }
+)));
+
+Please refer to an older version of the documentation. This has been omitted to save space and reduce confusion in this upgrading section.
+Here's a list of questions that have come up time and time again which all have the same answer.
+The CommandAPI's documentation is the place to search for anything! In the top left corner of this documentation, you can find this icon. You can pretty much search for anything - it'll find it!
+/reload
?Formally, no. If you are encountering issues with /reload
, consider not using /reload
. More information about reloading can be found in Plugin reloading.
As of 9.0.0, yes! Please view information on optional arguments in Optional arguments.
+Not yet. The CommandAPI's annotation system was actually originally a little test on writing a compile-time annotation system which actually worked out much better than I had intended. I plan to rewrite the CommandAPI's annotation system to make it much more powerful (and support suggestions!). This is stated in the project roadmap
+No. This is a Brigadier limitation.
+++Technical reason that this is a limitation of Brigadier
+Brigadier's code has two classes for arguments,
+LiteralCommandNode
andArgumentCommandNode
. TheArgumentCommandNode
class contains a fieldcustomSuggestions
of typeSuggestionProvider<S>
which is used to handle suggestions - this field is not present insideLiteralCommandNode
, meaning that LiteralArguments cannot provide suggestions to users.We need suggestions to provide tooltips. This is because
+SuggestionProvider
provides us with an instance ofSuggestionsBuilder
,SuggestionsBuilder
contains aList<Suggestion>
and theSuggestion
class containsMessage
. ThisMessage
class is what is needed to display a tooltip to users.
No. That message is handled client-side and isn't controlled by the CommandAPI. An alternative solution is to perform permission checking inside the command and return a custom message if it's not satisfied:
+new CommandAPICommand("mycommand")
+ .withPermission("some.permission")
+ .executes((sender, args) -> {
+ sender.sendMessage("Hello!");
+ })
+ .register();
+
+$$\downarrow$$
+new CommandAPICommand("mycommand")
+ .executes((sender, args) -> {
+ if(!sender.hasPermission("some.permission")) {
+ throw CommandAPI.failWithString("You don't have permission to run /mycommand!");
+ }
+ sender.sendMessage("Hello!");
+ })
+ .register();
+
+No. That message is handled client-side and isn't controlled by the CommandAPI.
+Arguments with suggestions provided using ArgumentSuggestions.strings(String...)
are calculated when the command is registered. In order to have argument suggestions calculated when the command is being typed, you need to use the lambda-variant of the ArgumentSuggestions.strings(Function<SuggestionInfo, String[]> suggestions)
method instead. More information about the different methods can be found here.
The easiest way to do this is to add info ->
at the start of your array:
ArgumentSuggestions.strings(SomeClass.someArray);
+
+$$\downarrow$$
+ArgumentSuggestions.strings(info -> SomeClass.someArray);
+
+There are a few arguments that are incompatible with various versions of Minecraft. This page outlines the full list of incompatibilities that the CommandAPI has with what versions of Minecraft.
+Incompatible with Minecraft versions less than 1.19 and greater than 1.19.2 (Works on 1.19, 1.19.1 and 1.19.2)
+Running functions generated via the FunctionArgument
on Minecraft version 1.20.3 and 1.20.4 will always return a value of 1, regardless of whether the command succeeds, fails, or returns a result. (Works normally on 1.20.2 and below). Trying to retrieve the list of commands in a function on Minecraft version 1.20.3 and 1.20.4 will always return an empty array.
In Minecraft version 1.16, the way datapacks were loaded changed in such a way that the CommandAPI had to put in additional countermeasures to provide full support to it. To illustrate this, this was the previous loading sequence for Bukkit servers in Minecraft 1.15:
+\(\texttt{Server loads}\rightarrow\texttt{Plugins load}\rightarrow\texttt{Datapacks load}\rightarrow\texttt{Server finishes loading}\)
+Instead however, Minecraft 1.16 changed the loading sequence to the following:
+\(\texttt{Server loads}\rightarrow\texttt{Datapacks load}\rightarrow\texttt{Plugins load}\rightarrow\texttt{Server finishes loading}\)
+Because the CommandAPI used to register vanilla Minecraft commands before datapacks (and thus, custom Minecraft functions), it was possible to register custom commands that can be used in functions. With this new loading sequence change in Minecraft 1.16, this meant that datapacks load first before the CommandAPI does, so custom commands are not registered and functions with custom commands would fail to load.
+To resolve this, the CommandAPI reloads datapacks and recipes at the end:
+\begin{align} +&\quad\texttt{Server loads} \\ +\rightarrow&\quad\texttt{Datapacks load} \\ +\rightarrow&\quad\texttt{Plugins load} \\ +\rightarrow&\quad\texttt{Server finishes loading} \\ +\rightarrow&\quad\texttt{Datapacks are reloaded} && \texttt{(by the CommandAPI)} \\ +\rightarrow&\quad\texttt{Recipes are reloaded} && \texttt{(by the CommandAPI)} +\end{align}
+By doing this, this means:
+Although this sounds pretty bad (since reloading these things twice can be time consuming, thus contributing to the server start-up time), it is the only way to make custom functions work in Minecraft 1.16 and beyond.
+This section summarizes the list of things that could go wrong with the CommandAPI and how to mitigate these circumstances.
+If you've registered a command with an argument and replaced/included additional suggestions, but the suggestions:
+This is most likely caused by using a constant array instead of using a lambda to dynamically update argument suggestions when the player requests them:
+new StringArgument("players")
+ .replaceSuggestions(ArgumentSuggestions.strings(
+ Bukkit.getOnlinePlayers().stream().map(Player::getName).toArray(String[]::new)
+ ));
+
+$$\downarrow$$
+new StringArgument("players")
+ .replaceSuggestions(ArgumentSuggestions.strings(
+ info -> Bukkit.getOnlinePlayers().stream().map(Player::getName).toArray(String[]::new)
+ ));
+
+If you're shading the CommandAPI and any of the following occur:
+.withPermission(CommandPermission.NONE)
doesn't work, even if a player has no permissionsYou've probably not initialized the CommandAPI correctly. To assign permissions to a command, you have to add CommandAPI.onEnable()
to your plugin's onEnable()
method.
If you've registered a command, the command should be present in the console if verbose logging is enabled. If this does not appear in the console, check that you've:
+.register()
at the end of your command declarationCommandAPI.onLoad()
and CommandAPI.onEnable()
to your onLoad()
and onEnable()
methods if you're shading the CommandAPIplugin.yml
file if you're not shading the CommandAPIUnsupportedClassVersionError
and "class file version 60.0"If you're getting an error which looks something like this (key thing to look for is class file version 60.0
), then you're running an old and unsupported version of Java:
[06.09 16:48:26] [Server] [Server thread/ERROR]: Could not load 'plugins/CommandAPI.jar' in folder 'plugins'
+org.bukkit.plugin.InvalidPluginExceptionjava.lang.UnsupportedClassVersionError: dev/jorel/commandapi/CommandAPIMain has been compiled by a more recent version of the Java Runtime (class file version 60.0), this version of the Java Runtime only recognizes class file versions up to 52.0
+ at org.bukkit.plugin.java.JavaPluginLoader.loadPlugin(JavaPluginLoader.java:149) ~[spigot-1.16.5.jar:3096-Spigot-9fb885e-296df56]
+ at org.bukkit.plugin.SimplePluginManager.loadPlugin(SimplePluginManager.java:394) ~[spigot-1.16.5.jar:3096-Spigot-9fb885e-296df56]
+ at org.bukkit.plugin.SimplePluginManager.loadPlugins(SimplePluginManager.java:301) ~[spigot-1.16.5.jar:3096-Spigot-9fb885e-296df56]
+ at org.bukkit.craftbukkit.v1_16_R3.CraftServer.loadPlugins(CraftServer.java:381) ~[spigot-1.16.5.jar:3096-Spigot-9fb885e-296df56]
+ at net.minecraft.server.v1_16_R3.DedicatedServer.init(DedicatedServer.java:224) ~[spigot-1.16.5.jar:3096-Spigot-9fb885e-296df56]
+ at net.minecraft.server.v1_16_R3.MinecraftServer.w(MinecraftServer.java:928) ~[spigot-1.16.5.jar:3096-Spigot-9fb885e-296df56]
+ at net.minecraft.server.v1_16_R3.MinecraftServer.lambda$0(MinecraftServer.java:273) ~[spigot-1.16.5.jar:3096-Spigot-9fb885e-296df56]
+ at java.lang.Thread.run(Thread.java:748) [?:1.8.0_271]
+Caused byjava.lang.UnsupportedClassVersionError: dev/jorel/commandapi/CommandAPIMain has been compiled by a more recent version of the Java Runtime (class file version 60.0), this version of the Java Runtime only recognizes class file versions up to 52.0
+ at java.lang.ClassLoader.defineClass1(Native Method) ~[?:1.8.0_271]
+ at java.lang.ClassLoader.defineClass(ClassLoader.java:756) ~[?:1.8.0_271]
+ at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) ~[?:1.8.0_271]
+ at org.bukkit.plugin.java.PluginClassLoader.findClass(PluginClassLoader.java:186) ~[spigot-1.16.5.jar:3096-Spigot-9fb885e-296df56]
+ at java.lang.ClassLoader.loadClass(ClassLoader.java:418) ~[?:1.8.0_271]
+ at org.bukkit.plugin.java.PluginClassLoader.loadClass0(PluginClassLoader.java:104) ~[spigot-1.16.5.jar:3096-Spigot-9fb885e-296df56]
+ at org.bukkit.plugin.java.PluginClassLoader.loadClass(PluginClassLoader.java:99) ~[spigot-1.16.5.jar:3096-Spigot-9fb885e-296df56]
+ at java.lang.ClassLoader.loadClass(ClassLoader.java:351) ~[?:1.8.0_271]
+ at java.lang.Class.forName0(Native Method) ~[?:1.8.0_271]
+ at java.lang.Class.forName(Class.java:348) ~[?:1.8.0_271]
+ at org.bukkit.plugin.java.PluginClassLoader.<init>(PluginClassLoader.java:67) ~[spigot-1.16.5.jar:3096-Spigot-9fb885e-296df56]
+ at org.bukkit.plugin.java.JavaPluginLoader.loadPlugin(JavaPluginLoader.java:145) ~[spigot-1.16.5.jar:3096-Spigot-9fb885e-296df56]
+ ... more
+
+As of CommandAPI 6.0.0 onwards, the CommandAPI is written in Java 16, so old versions such as Java 8 will not run the CommandAPI. Consider upgrading to Java 16.
+For context, a list of Java versions and their class file version:
+Java version | Class file version | Java version | Class file version | |
---|---|---|---|---|
Java 17 | 61.0 | Java 12 | 56.0 | |
Java 16 | 60.0 | Java 11 | 55.0 | |
Java 15 | 59.0 | Java 10 | 54.0 | |
Java 14 | 58.0 | Java 9 | 53.0 | |
Java 13 | 57.0 | Java 8 | 52.0 |
If you get an error at the very start of the server's startup sequence along the lines of:
+[15:57:29] [Worker-Main-5/ERROR]: Failed to load function mycustomnamespace:test
+java.util.concurrent.CompletionException: java.lang.IllegalArgumentException: Whilst parsing command on line 2: Unknown or incomplete command, see below for error at position 0: <--[HERE]
+ at java.util.concurrent.CompletableFuture.encodeThrowable(Unknown Source) ~[?:1.8.0_261]
+ at java.util.concurrent.CompletableFuture.completeThrowable(Unknown Source) [?:1.8.0_261]
+ at java.util.concurrent.CompletableFuture$AsyncSupply.run(Unknown Source) [?:1.8.0_261]
+ at java.util.concurrent.CompletableFuture$AsyncSupply.exec(Unknown Source) [?:1.8.0_261]
+ at java.util.concurrent.ForkJoinTask.doExec(Unknown Source) [?:1.8.0_261]
+ at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(Unknown Source) [?:1.8.0_261]
+ at java.util.concurrent.ForkJoinPool.runWorker(Unknown Source) [?:1.8.0_261]
+ at java.util.concurrent.ForkJoinWorkerThread.run(Unknown Source) [?:1.8.0_261]
+Caused by: java.lang.IllegalArgumentException: Whilst parsing command on line 2: Unknown or incomplete command, see below for error at position 0: <--[HERE]
+ at net.minecraft.server.v1_16_R1.CustomFunction.a(SourceFile:62) ~[spigot-1.16.1.jar:git-Spigot-758abbe-8dc1da1]
+ at net.minecraft.server.v1_16_R1.CustomFunctionManager.a(SourceFile:84) ~[spigot-1.16.1.jar:git-Spigot-758abbe-8dc1da1]
+ ... 6 more
+
+You can safely ignore it - the CommandAPI fixes this later. This is described in more detail here.
+/reload
doesn't workSee Plugin reloading
+If players cannot connect, this could be due to the size of the command data packet. To see the resultant packet being sent to players when they log in, enable the create-dispatcher-json: true
setting and view the file size of the resultant file. If the file size is abnormally large (Over 2MB is considered very large), consider reducing the number of LiteralArguments
which your plugin uses.
If you've found a bug that isn't solved here, submit a bug report on the CommandAPI's issues page and I'll try my best to resolve the issue!
+Congratulations on making it to the end of the documentation! It's really long, but I did my best to make it the best (Bukkit/Spigot plugin) documentation in existence.
+My name is Jorel, commonly known by my Minecraft username Skepter. I started the CommandAPI in the summer holidays between my first and second year at university. On the 19th August, 2018 I made my first commit to the CommandAPI project - just a month and a day after Minecraft 1.13 was released.
+At the time, I just decided to call it "The 1.13 Command API" - it wasn't the catchiest name out there, but it sort of said what I wanted it to - it's a Command API for Minecraft 1.13, which was the update when the big overhaul to the command system was introduced.
+It all started as a simple idea that can be summarized in 3 bullet points:
+After the release of version 1.2, two days after the initial release, I received my first GitHub issue. This was quite a shock to me - version 1.2 only had 11 total downloads so it seemed odd that someone managed to stumble upon it despite the fact that I did nothing to promote the CommandAPI. Little did I know that that one issue was the main motivation to keep this API alive after its initial release.
+I would never have possible imagined in my wildest dreams that 2 years later, I would still be in contact with them and know that if I had not chosen to create this API, their server would not have survived beyond Minecraft 1.13, let alone Minecraft 1.15, two major Minecraft versions later.
+This project has been insane. Absolutely, utterly insane. At over 1000 commits and over 1,000,000 additions (that includes things such as lines of code, lines of generated HTML documentation etc.), I can say without a doubt that this is indeed my biggest project ever.
+Not only have I managed to deal with hundreds of GitHub issues, I've also had the opportunity to try all sorts of technologies that I could have only dreamt of using, such as:
+Anyway, I digress. I'd like to give credit to all of the people that have opened issues on the CommandAPI GitHub, for without these people, the CommandAPI would have only remained a shadow of what it is now. I'd like to give credit to the people that have starred the CommandAPI on its GitHub page, and all of the members of the CommandAPI Discord server.
+I would like to personally give thanks to the following people - these are people that have made a significant contribution to the project in terms of ideas or support early in the CommandAPI's development:
+I'd also like to give a special mention to the following people that have helped find bugs or have supported the project in some way early in the CommandAPI's development: aianlinb, Baka-Mitai, Checkium, Comeza, DerpNerb, DogeBogey, endrdragon, EnragedRabisu, i509VCB, KieranGateley, lifespan, Loapu, Marvmallow, MatrixTunnel, Ricky12Awesome, SHsuperCM, SpaceCheetah, The_Gavster, Tinkot, vladfrangu, zedwick.
+I'm well aware there are lots of users that have made significant contributions to the CommandAPI that aren't listed above! Over the past four years, hundreds of users have created GitHub issues, joined the CommandAPI Discord server and submitted pull requests to contribute to the CommandAPI.
+I'd like to personally give thanks to my ✨Special Contributors in the CommandAPI Discord server - these are users that have made an exceptionally significant contribution to the CommandAPI project in general, from helping new users get started with the CommandAPI, to contributing to the CommandAPI repository directly, to bringing a liveliness to the CommandAPI Discord server as a whole:
+The CommandAPI would not be where it is currently without the community that has been established over the years in the CommandAPI Discord server. As such, I'd like to give a special thanks to some of the most active CommandAPI Discord server members that make the community feel lively:
+I didn't really want to add a way for CommandAPI users to be able to contribute financially via donations because the CommandAPI is free for all and the cost of maintaining the CommandAPI is effectively negligible. That said, I'd like to give special thanks to those that have donated to the CommandAPI.
+I never really expected more than 5 or so people to use this API, so it was truly a pleasure to see everyone's responses, issues and suggestions that has made the CommandAPI what it is today.
+Thank you so much for using the CommandAPI!
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +