Commands, part 2: Brigadier command trees
Build commands with typed arguments, tab completion and subcommands that the client understands.
Brigadier is the command system Minecraft itself uses. On this page you will describe commands as trees, let the game check numbers, player names and items for you, add subcommands and custom suggestions, and build /heal, /gm and /tools.
Why Brigadier?
In part 1 you wrote /roll [sides] with a BasicCommand. It worked, but you did all the checking yourself: is there an argument, is it a number, is it between 2 and 100? And while the player typed, the game had no idea what /roll expected.
Brigadier turns that around. Instead of receiving loose words, you describe the shape of the command: "first the word roll, then optionally a whole number from 2 to 100". Paper sends that shape to the player's game, and three good things happen:
- The game checks the input while the player types. A wrong argument turns red before they press Enter, with an error that says what is wrong.
- You get real values, not text. Your code receives an
int, aPlayer, anItemStackor aGameMode, already checked. - Vanilla features come for free. Selectors such as
@aand@p, item syntax likediamond_sword[enchantments={sharpness:5}], and argument hints in the chat box all work in your commands, because they are the same argument types vanilla commands use.
The price is a new way of writing commands. It looks strange at first, so this page goes slowly.
Commands are trees
Think of vanilla's /gamemode, and of the /gm shortcut you will build later on this page. After /gm you must type a mode, and after the mode you may type a player name. Brigadier stores this as a command tree. Every node in the tree is one word of the command, and there are two kinds:
- A literal is a fixed word that the player types exactly, such as
gamemode, orgivein/tools give. - An argument is a value the player chooses, such as a number, a player or a game mode. It has a name (so your code can ask for it) and a type (so the game can check it).
Each node can also have an executes part: the code that runs when the command ends at that node. If the player stops at a node without executes, the command is incomplete and the game says so. This page calls the nodes with executes stations, because those are the places where a command may stop.
Your first tree: /roll again
The smallest possible Brigadier command is one literal with an executes. Read it as "a command called hello that, when it ends here, sends a message". You will see where code like this goes in a moment:
1Commands.literal("hello")2 .executes(context -> {3 context.getSource().getSender().sendRichMessage("<green>Hello!");4 return Command.SINGLE_SUCCESS;5 })6 .build();Now the real thing: /roll from part 1, rebuilt as a tree. The tree has two stations. /roll alone rolls a six-sided die, and /roll <sides> rolls a die with that many sides:
Where this file livescommands-brigadier-firstsrcmainjavacomexamplecommandsbrigadierfirstRollCommand.java
The package com.example.commandsbrigadierfirst is the folder path com/example/commandsbrigadierfirst inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- commands-brigadier-first/
- src/main/
- java/com/example/commandsbrigadierfirst/Package com.example.commandsbrigadierfirst
- BrigadierFirstPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- RollCommand.javayou are hereCommand (Brigadier tree)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/commandsbrigadierfirst/Package com.example.commandsbrigadierfirst
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.commandsbrigadierfirst;2 3import com.mojang.brigadier.Command;4import com.mojang.brigadier.arguments.IntegerArgumentType;5import com.mojang.brigadier.context.CommandContext;6import com.mojang.brigadier.tree.LiteralCommandNode;7import io.papermc.paper.command.brigadier.CommandSourceStack;8import io.papermc.paper.command.brigadier.Commands;9import java.util.concurrent.ThreadLocalRandom;10import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;11 12final class RollCommand {13 14 private RollCommand() {15 }16 17 static LiteralCommandNode<CommandSourceStack> create() {18 return Commands.literal("roll")19 .executes(context -> roll(context, 6))20 .then(Commands.argument("sides", IntegerArgumentType.integer(2, 100))21 .executes(context -> roll(context, IntegerArgumentType.getInteger(context, "sides"))))22 .build();23 }24 25 private static int roll(CommandContext<CommandSourceStack> context, int sides) {26 int result = ThreadLocalRandom.current().nextInt(1, sides + 1);27 context.getSource().getSender().sendRichMessage(28 "<gray>You rolled a <gold><result></gold> on a <sides>-sided die.",29 Placeholder.unparsed("result", String.valueOf(result)),30 Placeholder.unparsed("sides", String.valueOf(sides)));31 return Command.SINGLE_SUCCESS;32 }33}- Brigadier was made by Mojang, so its own classes live in
com.mojang.brigadier. Paper's additions live inio.papermc.paper.command.brigadier. You use both. - This class only builds the tree; it is never turned into an object. That is why it has a private
constructor and astaticmethod. - Builds and returns the finished tree.
staticmeans the method belongs to the class itself, so other code callsRollCommand.create()without making an object first. The return type reads as "a literal node for commands whose source is aCommandSourceStack". The part in angle brackets is ageneric type parameter; you can copy it as it is. - The root of the tree: the literal word
roll. This is the command's name. - The first station. If the player types just
/roll, thislambda runs and rolls a six-sided die.contextholds everything about this run of the command. - Adds a child node. Read
thenas "and after that, the player may type...". - The argument's type: a whole number from 2 to 100. The game rejects anything else before your code ever runs.
- Reads the argument called
sidesas anint. The name must match the name inCommands.argument("sides", ...)exactly. - Finishes the tree. Until you call
build()you only have a builder, a plan for the tree. - Both stations share this method, so the rolling code exists only once. It returns an
int, because everyexecutesmust return a number. - The same source and sender as in part 1:
getSource()gives theCommandSourceStack, and that gives the sender. - Means "it worked, once".
Command.SINGLE_SUCCESSis simply the number 1.
Compare this with part 1's /roll: no args.length, no Integer.parseInt, no try/catch, no range check. The argument type does all of it. Registering works like part 1's lifecycle event, except that you pass the built tree instead of a BasicCommand:
Where this file livescommands-brigadier-firstsrcmainjavacomexamplecommandsbrigadierfirstBrigadierFirstPlugin.java
The package com.example.commandsbrigadierfirst is the folder path com/example/commandsbrigadierfirst inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- commands-brigadier-first/
- src/main/
- java/com/example/commandsbrigadierfirst/Package com.example.commandsbrigadierfirst
- BrigadierFirstPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- RollCommand.javaCommand (Brigadier tree)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/commandsbrigadierfirst/Package com.example.commandsbrigadierfirst
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.commandsbrigadierfirst;2 3import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;4import java.util.List;5import org.bukkit.plugin.java.JavaPlugin;6 7public final class BrigadierFirstPlugin extends JavaPlugin {8 9 @Override10 public void onEnable() {11 getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event ->12 event.registrar().register(RollCommand.create(), "Roll a die with 2 to 100 sides", List.of("dice")));13 }14}- The same lifecycle event as in part 1. Brigadier commands are always registered here (there is no
registerCommandshortcut for trees). - Builds the tree and hands it to the registrar, with a description for
/helpand the alias/dice.
Here is what players see. A valid roll, and then what happens when someone types /roll 500: the game shows the error and your code never runs.
You rolled a 73 on a 100-sided die.
Integer must not be more than 100, found 500
roll 500<--[HERE]
While typing, the player also sees a gray hint <sides> above the chat box, so they know what comes next. You got all of that by choosing a name and a type.
Reading the chain of dots
Brigadier code is one long expression: Commands.literal(...) followed by .then(...), .executes(...), .requires(...) and finally .build(). This style is called a builder: each method changes the plan and hands the same builder back, so you can keep adding dots.
The tricky part is the brackets. A .then(...) or .executes(...) belongs to whatever node it is written on. Indentation shows this for humans, but Java only looks at the brackets. Compare:
1Commands.literal("roll")2 .then(Commands.argument("sides", IntegerArgumentType.integer(2, 100))3 .executes(context -> roll(context, IntegerArgumentType.getInteger(context, "sides"))))4 .build();1Commands.literal("roll")2 .then(Commands.argument("sides", IntegerArgumentType.integer(2, 100)))3 .executes(context -> roll(context, IntegerArgumentType.getInteger(context, "sides")))4 .build();In the wrong version, one bracket closed the argument too early. Now the executes belongs to roll, so plain /roll tries to read an argument that was never typed and fails, while /roll 20 has no executes at all and is "incomplete". If a command behaves strangely, count the brackets. IntelliJ highlights the matching bracket when you put the cursor next to one.
What executes returns
Every executes returns an int. Players never see it, but vanilla uses it: /execute store result can save it into a scoreboard, and a command block passes it to a comparator. The convention is:
Command.SINGLE_SUCCESS(which is 1) when the command did its job.- A count when it did something several times, like "healed 3 players": return
3. 0when it did nothing.
If in doubt, return Command.SINGLE_SUCCESS.
Argument types
The argument type is where Brigadier saves you work. Pick the type that matches what you need, and the game does the checking. These are the ones you will use most:
| Type | Player types | Read it with |
|---|---|---|
IntegerArgumentType.integer(1, 64) | 16 | IntegerArgumentType.getInteger(context, "amount") gives an int |
DoubleArgumentType.doubleArg(0.1, 5.0) | 2.5 | DoubleArgumentType.getDouble(context, "power") gives a double |
BoolArgumentType.bool() | true or false | BoolArgumentType.getBool(context, "silent") |
StringArgumentType.word() | one word: letters, digits and _ - . + | StringArgumentType.getString(context, "name") |
StringArgumentType.string() | one word, or "several words in quotes" | StringArgumentType.getString(context, "text") |
StringArgumentType.greedyString() | everything up to the end of the line | StringArgumentType.getString(context, "message") |
ArgumentTypes.player() | Steve or @p (exactly one player) | context.getArgument("player", PlayerSelectorArgumentResolver.class), then .resolve(...) |
ArgumentTypes.players() | Steve, @a, @a[distance=..10] | same as above; gives a List<Player> |
ArgumentTypes.entities() | @e[type=zombie], a player name | EntitySelectorArgumentResolver, then .resolve(...) |
ArgumentTypes.gameMode() | creative, survival... | context.getArgument("mode", GameMode.class) |
ArgumentTypes.world() | minecraft:the_nether | context.getArgument("world", World.class) |
ArgumentTypes.itemStack() | diamond, diamond_sword[enchantments={sharpness:5}] | context.getArgument("item", ItemStack.class) |
ArgumentTypes.blockPosition() | 10 64 -20 or ~ ~1 ~ | BlockPositionResolver, then .resolve(...) |
ArgumentTypes.namedColor() | red, dark_aqua | context.getArgument("color", NamedTextColor.class) |
There are three ways to read an argument, and the table shows which one each type uses:
- Brigadier's own types (numbers, booleans, strings) have a static helper on their class:
IntegerArgumentType.getInteger(context, "amount"). - Paper's simple types come out with
context.getArgument(name, Type.class), for exampleGameMode.classorItemStack.class. - Paper's selector and position types give you a resolver first. A selector like
@pmeans "the nearest player", but nearest to whom? The resolver works that out when you call.resolve(context.getSource()), using the source's location.
Example: /heal [targets]
Time for the demo plugin. /heal heals you; /heal <targets> heals other players, and because the argument is players(), admins can write /heal @a to heal everyone:
Where this file livescommands-brigadiersrcmainjavacomexamplecommandsbrigadierHealCommand.java
The package com.example.commandsbrigadier is the folder path com/example/commandsbrigadier inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- commands-brigadier/
- src/main/
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- CommandsBrigadierPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- GameModeCommand.javaCommand (Brigadier tree)
- HealCommand.javayou are hereCommand (Brigadier tree)
- ToolsCommand.javaCommand (Brigadier tree)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
22static LiteralCommandNode<CommandSourceStack> create() {23 return Commands.literal("heal")24 .requires(source -> source.getSender().hasPermission("commandsbrigadier.heal"))25 .executes(HealCommand::healSelf)26 .then(Commands.argument("targets", ArgumentTypes.players())27 .requires(source -> source.getSender().hasPermission("commandsbrigadier.heal.others"))28 .executes(HealCommand::healTargets))29 .build();30}- Only senders with this permission get the command at all. Others do not see it in tab completion and cannot run it. More on
requiresbelow. - A
method reference : a short way to say "run the methodhealSelfof this class". It is the same ascontext -> healSelf(context). - One or more players: a name, or a selector such as
@aor@a[distance=..10]. - A second, stronger permission for healing other people. A node can have its own
requires, so different branches can need different permissions.
And the methods the two stations run:
Where this file livescommands-brigadiersrcmainjavacomexamplecommandsbrigadierHealCommand.java
The package com.example.commandsbrigadier is the folder path com/example/commandsbrigadier inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- commands-brigadier/
- src/main/
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- CommandsBrigadierPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- GameModeCommand.javaCommand (Brigadier tree)
- HealCommand.javayou are hereCommand (Brigadier tree)
- ToolsCommand.javaCommand (Brigadier tree)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
32private static int healSelf(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {33 Player player = context.getSource().getPlayerOrThrow();34 heal(player);35 player.sendRichMessage("<green>You have been healed.");36 return Command.SINGLE_SUCCESS;37}- This method may throw a Brigadier error. Brigadier catches it and shows its message to the player in red, so you never need a
tryhere. - The Brigadier way to say "this needs a player". If the executor is a player, you get it. If not (the console, for example), it throws an error with the message "A player is required to run this command here".
- The actual healing lives in one shared method, used by both stations.
Where this file livescommands-brigadiersrcmainjavacomexamplecommandsbrigadierHealCommand.java
The package com.example.commandsbrigadier is the folder path com/example/commandsbrigadier inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- commands-brigadier/
- src/main/
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- CommandsBrigadierPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- GameModeCommand.javaCommand (Brigadier tree)
- HealCommand.javayou are hereCommand (Brigadier tree)
- ToolsCommand.javaCommand (Brigadier tree)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
39private static int healTargets(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {40 List<Player> targets = context.getArgument("targets", PlayerSelectorArgumentResolver.class)41 .resolve(context.getSource());42 for (Player target : targets) {43 heal(target);44 target.sendRichMessage("<green>You have been healed.");45 }46 context.getSource().getSender().sendRichMessage("<green>Healed <white><count></white> player(s).",47 Placeholder.unparsed("count", String.valueOf(targets.size())));48 return targets.size();49}- Gets the resolver for the
targetsargument. - Turns the name or selector into real players. If nobody matches, it throws an error ("No player was found"), so the list is never empty.
- Heals each player in the list and tells them.
- Returns how many players were healed, following the convention from above.
Where this file livescommands-brigadiersrcmainjavacomexamplecommandsbrigadierHealCommand.java
The package com.example.commandsbrigadier is the folder path com/example/commandsbrigadier inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- commands-brigadier/
- src/main/
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- CommandsBrigadierPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- GameModeCommand.javaCommand (Brigadier tree)
- HealCommand.javayou are hereCommand (Brigadier tree)
- ToolsCommand.javaCommand (Brigadier tree)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
51private static void heal(Player player) {52 AttributeInstance maxHealth = player.getAttribute(Attribute.MAX_HEALTH);53 player.setHealth(maxHealth == null ? 20.0 : maxHealth.getValue());54 player.setFoodLevel(20);55 player.setSaturation(20.0f);56 player.setFireTicks(0);57}- Players can have more or less than 20 health (10 hearts) if a plugin or effect changed it. Reading the attribute heals them to their real maximum. See Working with players.
- Fills the hunger bar (20 is full).
setSaturationon the next line adds a hidden food buffer, so the bar does not start draining right away. - Puts out fire, so they do not burn right after healing.
What an admin sees after /heal @a with three players online, and what each healed player sees:
You have been healed.
Example: /gm <mode> [player]
A shortcut for /gamemode. The mode is required, the player is optional, and changing someone else's mode needs an extra permission. This time the stations use lambdas with a few lines inside, instead of method references:
Where this file livescommands-brigadiersrcmainjavacomexamplecommandsbrigadierGameModeCommand.java
The package com.example.commandsbrigadier is the folder path com/example/commandsbrigadier inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- commands-brigadier/
- src/main/
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- CommandsBrigadierPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- GameModeCommand.javayou are hereCommand (Brigadier tree)
- HealCommand.javaCommand (Brigadier tree)
- ToolsCommand.javaCommand (Brigadier tree)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
21static LiteralCommandNode<CommandSourceStack> create() {22 return Commands.literal("gm")23 .requires(source -> source.getSender().hasPermission("commandsbrigadier.gamemode"))24 .then(Commands.argument("mode", ArgumentTypes.gameMode())25 .executes(context -> {26 Player player = context.getSource().getPlayerOrThrow();27 return setMode(context, player);28 })29 .then(Commands.argument("player", ArgumentTypes.player())30 .requires(source -> source.getSender().hasPermission("commandsbrigadier.gamemode.others"))31 .executes(context -> {32 Player target = context.getArgument("player", PlayerSelectorArgumentResolver.class)33 .resolve(context.getSource())34 .getFirst();35 return setMode(context, target);36 })))37 .build();38}- The root has no
executes, so plain/gmis incomplete. A mode is required. - The same argument vanilla
/gamemodeuses. The game suggestssurvival,creative,adventureandspectator. - Without a player argument, the target is whoever ran the command.
- Exactly one player.
@ais rejected here by the game, because it could mean many. - The resolver always gives a list, even for
player(). It holds exactly one player, so take the first. - Both stations call the same method and return what it returns.
Where this file livescommands-brigadiersrcmainjavacomexamplecommandsbrigadierGameModeCommand.java
The package com.example.commandsbrigadier is the folder path com/example/commandsbrigadier inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- commands-brigadier/
- src/main/
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- CommandsBrigadierPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- GameModeCommand.javayou are hereCommand (Brigadier tree)
- HealCommand.javaCommand (Brigadier tree)
- ToolsCommand.javaCommand (Brigadier tree)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
40private static int setMode(CommandContext<CommandSourceStack> context, Player target) {41 GameMode mode = context.getArgument("mode", GameMode.class);42 target.setGameMode(mode);43 Component modeName = Component.translatable(mode);44 target.sendRichMessage("<gray>Your game mode is now <gold><mode></gold>.",45 Placeholder.component("mode", modeName));46 CommandSender sender = context.getSource().getSender();47 if (sender != target) {48 sender.sendRichMessage("<gray>Set <white><player></white> to <gold><mode></gold>.",49 Placeholder.unparsed("player", target.getName()),50 Placeholder.component("mode", modeName));51 }52 return Command.SINGLE_SUCCESS;53}- Reads the
modeargument. It is a realGameModevalue, so you can pass it straight tosetGameMode. - The game mode's name in each player's own language: "Creative Mode" in English. See Text and colors.
- Only send a second message when someone changed another player's mode. Otherwise the player would get two messages about the same thing.
Set Alex to Survival Mode.
Subcommands: /tools
A subcommand is a literal under the root: the give in /tools give. In Brigadier, subcommands are just more then(Commands.literal(...)) branches, each with its own children, permissions and suggestions. No if (args[0].equals("give")) chains needed. Here is the tree you are about to read:
Where this file livescommands-brigadiersrcmainjavacomexamplecommandsbrigadierToolsCommand.java
The package com.example.commandsbrigadier is the folder path com/example/commandsbrigadier inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- commands-brigadier/
- src/main/
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- CommandsBrigadierPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- GameModeCommand.javaCommand (Brigadier tree)
- HealCommand.javaCommand (Brigadier tree)
- ToolsCommand.javayou are hereCommand (Brigadier tree)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
33static LiteralCommandNode<CommandSourceStack> create() {34 return Commands.literal("tools")35 .executes(ToolsCommand::help)36 .then(Commands.literal("color")37 .then(Commands.argument("name", StringArgumentType.word())38 .suggests(ToolsCommand::suggestColors)39 .executes(ToolsCommand::color)))40 .then(Commands.literal("give")41 .requires(source -> source.getSender().hasPermission("commandsbrigadier.tools.give"))42 .then(Commands.argument("item", ArgumentTypes.itemStack())43 .executes(context -> give(context, 1))44 .then(Commands.argument("amount", IntegerArgumentType.integer(1, 64))45 .executes(context -> give(context, IntegerArgumentType.getInteger(context, "amount"))))))46 .then(Commands.literal("clear")47 .requires(Commands.restricted(source -> source.getSender().hasPermission("commandsbrigadier.tools.clear")))48 .executes(ToolsCommand::clear))49 .build();50}- Plain
/toolsshows a help list. Giving the root anexecutesis a friendly habit: players who type only the command name learn what it can do. - The first subcommand. It is a literal, so the player types
colorexactly. - Any single word. The game cannot know which words are colors, so this argument gets custom suggestions and its own check.
- Uses your method to fill the suggestion list for this argument.
- The second subcommand, with its own permission on the literal. Players without it do not even see
giveas an option. - The same item syntax as vanilla
/give, with suggestions for every item. - An optional amount after the item. Both nodes have
executes, so/tools give diamondand/tools give diamond 16both work. - Wraps the permission check. A restricted command asks the player to confirm when it is triggered from a clickable chat message, so nobody can trick them into emptying their inventory with a link.
Plain /tools shows this:
/tools color [name] preview a chat color
/tools give [item] [amount] give yourself an item
/tools clear empty your inventory
The give station shows how normal Paper code continues once Brigadier hands you the values:
Where this file livescommands-brigadiersrcmainjavacomexamplecommandsbrigadierToolsCommand.java
The package com.example.commandsbrigadier is the folder path com/example/commandsbrigadier inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- commands-brigadier/
- src/main/
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- CommandsBrigadierPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- GameModeCommand.javaCommand (Brigadier tree)
- HealCommand.javaCommand (Brigadier tree)
- ToolsCommand.javayou are hereCommand (Brigadier tree)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
83private static int give(CommandContext<CommandSourceStack> context, int amount) throws CommandSyntaxException {84 Player player = context.getSource().getPlayerOrThrow();85 ItemStack item = context.getArgument("item", ItemStack.class).clone();86 item.setAmount(Math.min(amount, item.getMaxStackSize()));87 for (ItemStack leftover : player.getInventory().addItem(item).values()) {88 player.getWorld().dropItemNaturally(player.getLocation(), leftover);89 }90 player.sendRichMessage("<green>Gave you <white><amount>x</white> <item>.",91 Placeholder.unparsed("amount", String.valueOf(item.getAmount())),92 Placeholder.component("item", item.effectiveName()));93 return Command.SINGLE_SUCCESS;94}- The item the player described.
clone()makes your own copy, so changing its amount cannot affect anything else. - Ender pearls stack to 16 and swords to 1. This keeps the amount within one stack.
- Puts the item in the inventory. Whatever did not fit comes back, and the loop drops it at the player's feet. See Inventories.
- The item's name as a component, translated for each player.
Custom suggestions
Built-in types bring their own suggestions. For a plain word() argument you can add yours with .suggests(...). The suggestion method gets the context and a SuggestionsBuilder, adds words to the builder, and returns it:
Where this file livescommands-brigadiersrcmainjavacomexamplecommandsbrigadierToolsCommand.java
The package com.example.commandsbrigadier is the folder path com/example/commandsbrigadier inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- commands-brigadier/
- src/main/
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- CommandsBrigadierPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- GameModeCommand.javaCommand (Brigadier tree)
- HealCommand.javaCommand (Brigadier tree)
- ToolsCommand.javayou are hereCommand (Brigadier tree)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
61private static CompletableFuture<Suggestions> suggestColors(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {62 String typed = builder.getRemainingLowerCase();63 for (String name : NamedTextColor.NAMES.keys()) {64 if (name.startsWith(typed)) {65 NamedTextColor color = NamedTextColor.NAMES.value(name);66 builder.suggest(name, MessageComponentSerializer.message().serialize(67 Component.text("This is " + name, color)));68 }69 }70 return builder.buildFuture();71}- The return type Brigadier expects. It looks scary, but you always end with
return builder.buildFuture();, so you can copy it. - What the player has typed of this argument so far, in lower case. An empty string if they have typed nothing yet.
- The names of Minecraft's 16 chat colors, such as
redanddark_aqua. - Only suggest names that match what was typed. The suggestion list stays short and useful.
- Adds one suggestion. The second value is an optional
tooltip , shown when the player hovers the suggestion. Here it is a sample of the color itself. - Brigadier does not understand Adventure components, so this turns your component into Brigadier's message type.
The suggestion method runs every time the player types a character, so you can compute the list from anything that is true right now: online players, loaded worlds, a player's saved homes, entries in your config. This one suggests the names of the loaded worlds:
1.suggests((context, builder) -> {2 for (World world : Bukkit.getWorlds()) {3 builder.suggest(world.getName());4 }5 return builder.buildFuture();6})Permissions with requires
.requires(...) takes a small test, a lambda that gets the source and answers true or false. If it answers false, that node and everything below it does not exist for that sender: it is missing from tab completion, and typing it gives "Unknown or incomplete command". You saw three uses:
- On the root (
/heal,/gm): the whole command needs a permission. - On an argument (
/heal <targets>,/gm <mode> <player>): the basic command is allowed, but the stronger form needs a second permission. - On a subcommand (
/tools give): one branch needs a permission, the rest of/toolsdoes not.
The permissions themselves are declared in plugin.yml, so Paper knows who has them by default:
Where this file livescommands-brigadiersrcmainresourcesplugin.yml
Files in src/main/resources are copied into the jar exactly as they are. Paper looks for plugin.yml at the top of the jar.
- commands-brigadier/
- src/main/
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- CommandsBrigadierPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- GameModeCommand.javaCommand (Brigadier tree)
- HealCommand.javaCommand (Brigadier tree)
- ToolsCommand.javaCommand (Brigadier tree)
- resources/Files copied into the jar as they are
- plugin.ymlyou are hereTells Paper the plugin's name, version and main class
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1name: CommandsBrigadier2version: '1.0.0'3main: com.example.commandsbrigadier.CommandsBrigadierPlugin4api-version: '26.3'5description: Brigadier command trees, /heal, /gm and /tools.6permissions:7 commandsbrigadier.heal:8 description: Lets a player heal themselves with /heal.9 default: op10 commandsbrigadier.heal.others:11 description: Lets a player heal other players with /heal targets.12 default: op13 children:14 commandsbrigadier.heal: true15 commandsbrigadier.gamemode:16 description: Lets a player change their own game mode with /gm.17 default: op18 commandsbrigadier.gamemode.others:19 description: Lets a player change another player's game mode.20 default: op21 children:22 commandsbrigadier.gamemode: true23 commandsbrigadier.tools.give:24 description: Lets a player give themselves items with /tools give.25 default: op26 commandsbrigadier.tools.clear:27 description: Lets a player empty their own inventory with /tools clear.28 default: true- One entry per permission. The
descriptionhelps server owners understand what it allows. - Anyone with
commandsbrigadier.heal.othersautomatically getscommandsbrigadier.healtoo. It would be odd to heal others but not yourself. - Everyone has this one unless a server owner takes it away. Clearing your own inventory hurts nobody else.
Keep requires tests fast and simple, such as permission checks, because the game asks them often: whenever it sends the command list to a player, and whenever someone runs or tab-completes the command. The command list is sent when a player joins. If permissions change while a player is online, call player.updateCommands() so their game gets the new list; permission plugins such as LuckPerms do that for you. More in Permissions.
Error messages
Brigadier errors are a CommandSyntaxException. Most of them you never write yourself: the argument types create them. Players see these before they press Enter, in red under the chat box:
No player was found
Unknown item 'minecraft:diamnd'
A player is required to run this command here
For problems the game cannot know about, such as a word that is not a color, you throw your own. First describe the error once, as a field:
Where this file livescommands-brigadiersrcmainjavacomexamplecommandsbrigadierToolsCommand.java
The package com.example.commandsbrigadier is the folder path com/example/commandsbrigadier inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- commands-brigadier/
- src/main/
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- CommandsBrigadierPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- GameModeCommand.javaCommand (Brigadier tree)
- HealCommand.javaCommand (Brigadier tree)
- ToolsCommand.javayou are hereCommand (Brigadier tree)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
27private static final DynamicCommandExceptionType UNKNOWN_COLOR = new DynamicCommandExceptionType(name ->28 MessageComponentSerializer.message().serialize(Component.text("Unknown color: " + name)));- An error type with a blank to fill in, here the unknown name. For an error that never changes, use
SimpleCommandExceptionTypeinstead. - A lambda that builds the message from the value you pass in later.
Then throw it where the check fails:
Where this file livescommands-brigadiersrcmainjavacomexamplecommandsbrigadierToolsCommand.java
The package com.example.commandsbrigadier is the folder path com/example/commandsbrigadier inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- commands-brigadier/
- src/main/
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- CommandsBrigadierPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- GameModeCommand.javaCommand (Brigadier tree)
- HealCommand.javaCommand (Brigadier tree)
- ToolsCommand.javayou are hereCommand (Brigadier tree)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/commandsbrigadier/Package com.example.commandsbrigadier
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
73private static int color(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {74 String name = StringArgumentType.getString(context, "name").toLowerCase(Locale.ROOT);75 NamedTextColor color = NamedTextColor.NAMES.value(name);76 if (color == null) {77 throw UNKNOWN_COLOR.create(name);78 }79 context.getSource().getSender().sendMessage(Component.text("This text is " + name + ".", color));80 return Command.SINGLE_SUCCESS;81}- The word the player typed, as text.
- Looks up the color by name. If the word is not a color, the answer is
null. - Creates the error with the typed name filled in and throws it. The method stops here, and Brigadier shows the message in red.
- The happy path: a line of text in the chosen color.
Unknown color: purplee
Why throw an error instead of sending a red message and returning? To the player both look the same, but a thrown error also tells the game that the command failed, which command blocks and /execute can react to. A good habit: throw errors for "this input is wrong", and send normal messages for everything else.
- commands-brigadier/
- src/
- main/
- java/
- com/example/commandsbrigadier/
- CommandsBrigadierPlugin.javaRegisters the three trees in the COMMANDS lifecycle event
- HealCommand.java/heal and /heal targets, with two permissions
- GameModeCommand.java/gm mode and /gm mode player
- ToolsCommand.java/tools with color, give and clear subcommands
- com/example/commandsbrigadier/
- resources/
- plugin.ymlName, main class and six permissions
- java/
- main/
- src/
Download the demo plugin or open it in the Compile Lab.
Mistakes to avoid
- Brackets in the wrong place. An
executesorthenattached to the wrong node is the most common Brigadier bug. Count brackets, and keep one node per line. - Mismatched argument names.
Commands.argument("amount", ...)andgetInteger(context, "ammount")compile fine, then fail when the command runs. Copy names, do not retype them. - Forgetting
.build(). The registrar wants a finished node. Withoutbuild()you get a compile error aboutLiteralArgumentBuilder. - Asking for an argument the station does not have. At the
/rollstation there is nosidesyet; reading it there throws an error. Each station can only read the arguments on its own path. - A greedy string that is not last. Nothing after it can ever be typed.
- Trusting suggestions. Players can type anything. Check custom values again in
executes.
Launch pad
Write a /launch [targets] [power] command that throws players straight up into the air. Plain /launch launches you with power 1.0, /launch @a launches everyone, and /launch Steve 3 launches Steve with power 3. The power must be between 0.1 and 5. Everything needs the permission launchpad.launch.
Hint 1
You need three stations: the root, the targets argument (ArgumentTypes.players()), and the power argument (DoubleArgumentType.doubleArg(0.1, 5.0)) under targets.
Hint 2
To throw a player up: target.setVelocity(new Vector(0, power, 0));. Vector is org.bukkit.util.Vector.
Hint 3
Write one method launch(context, List<Player> targets, double power) that all three stations call. For the root station, the list is List.of(context.getSource().getPlayerOrThrow()).
Show the solution
One shared launch method does the work, and a small helper resolves the targets so the two lower stations do not repeat that code:
Where this file livescommands-brigadier-exercisesrcmainjavacomexamplecommandsbrigadierexerciseLaunchCommand.java
The package com.example.commandsbrigadierexercise is the folder path com/example/commandsbrigadierexercise inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- commands-brigadier-exercise/
- src/main/
- java/com/example/commandsbrigadierexercise/Package com.example.commandsbrigadierexercise
- LaunchCommand.javayou are hereCommand (Brigadier tree)
- LaunchPadPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/commandsbrigadierexercise/Package com.example.commandsbrigadierexercise
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.commandsbrigadierexercise;2 3import com.mojang.brigadier.arguments.DoubleArgumentType;4import com.mojang.brigadier.context.CommandContext;5import com.mojang.brigadier.exceptions.CommandSyntaxException;6import com.mojang.brigadier.tree.LiteralCommandNode;7import io.papermc.paper.command.brigadier.CommandSourceStack;8import io.papermc.paper.command.brigadier.Commands;9import io.papermc.paper.command.brigadier.argument.ArgumentTypes;10import io.papermc.paper.command.brigadier.argument.resolvers.selector.PlayerSelectorArgumentResolver;11import java.util.List;12import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;13import org.bukkit.entity.Player;14import org.bukkit.util.Vector;15 16final class LaunchCommand {17 18 private LaunchCommand() {19 }20 21 static LiteralCommandNode<CommandSourceStack> create() {22 return Commands.literal("launch")23 .requires(source -> source.getSender().hasPermission("launchpad.launch"))24 .executes(context -> launch(context, List.of(context.getSource().getPlayerOrThrow()), 1.0))25 .then(Commands.argument("targets", ArgumentTypes.players())26 .executes(context -> launch(context, targets(context), 1.0))27 .then(Commands.argument("power", DoubleArgumentType.doubleArg(0.1, 5.0))28 .executes(context -> launch(context, targets(context),29 DoubleArgumentType.getDouble(context, "power")))))30 .build();31 }32 33 private static List<Player> targets(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {34 return context.getArgument("targets", PlayerSelectorArgumentResolver.class).resolve(context.getSource());35 }36 37 private static int launch(CommandContext<CommandSourceStack> context, List<Player> targets, double power) {38 for (Player target : targets) {39 target.setVelocity(new Vector(0, power, 0));40 target.sendRichMessage("<aqua>Whoosh!");41 }42 context.getSource().getSender().sendRichMessage("<gray>Launched <white><count></white> player(s).",43 Placeholder.unparsed("count", String.valueOf(targets.size())));44 return targets.size();45 }46}- One permission on the root covers the whole tree.
- A list with just the player who ran the command, so the shared method always gets a list.
- A decimal number. The game rejects 0 and 50 before your code runs.
- A helper that both lower stations use. It may throw, so it says
throws CommandSyntaxException. - Straight up: nothing sideways (x and z),
powerupward (y).
Where this file livescommands-brigadier-exercisesrcmainjavacomexamplecommandsbrigadierexerciseLaunchPadPlugin.java
The package com.example.commandsbrigadierexercise is the folder path com/example/commandsbrigadierexercise inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- commands-brigadier-exercise/
- src/main/
- java/com/example/commandsbrigadierexercise/Package com.example.commandsbrigadierexercise
- LaunchCommand.javaCommand (Brigadier tree)
- LaunchPadPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/commandsbrigadierexercise/Package com.example.commandsbrigadierexercise
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.commandsbrigadierexercise;2 3import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;4import org.bukkit.plugin.java.JavaPlugin;5 6public final class LaunchPadPlugin extends JavaPlugin {7 8 @Override9 public void onEnable() {10 getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event ->11 event.registrar().register(LaunchCommand.create(), "Launch players into the air"));12 }13}Add a fourth tool
Add /tools heal to the /tools tree, reusing the healing from HealCommand without copying it.
Hint
The heal(Player) method in HealCommand is private. Make it package-private (remove private) so other classes in the same package can call HealCommand.heal(player).
Show the solution
Change private static void heal(Player player) in HealCommand to static void heal(Player player). Then add one more branch to the /tools tree, next to the others:
1.then(Commands.literal("heal")2 .executes(context -> {3 HealCommand.heal(context.getSource().getPlayerOrThrow());4 return Command.SINGLE_SUCCESS;5 }))Add a line for it to the help list too, and think about whether it needs a requires.
Recap
- A Brigadier command is a tree of literal nodes (fixed words) and argument nodes (typed values). Build it with
Commands.literal,Commands.argument,.then,.executesand.build(). - The command may end at any node with
executes. Eachexecutesreturns a number, usuallyCommand.SINGLE_SUCCESS. - Argument types check the input for you and give you real values. Read them with the type's helper, with
context.getArgument, or through a resolver. - Subcommands are literals under the root. Share logic by calling the same method from several stations.
.suggests(...)adds custom suggestions;.requires(...)hides nodes from senders who may not use them;Commands.restrictedprotects sensitive commands from click tricks.- Throw a
CommandSyntaxException(from aSimpleCommandExceptionTypeorDynamicCommandExceptionType) for invalid input. - Register trees in the
LifecycleEvents.COMMANDSevent.
Quick quiz
In
/tools give diamond 16, which parts are literal nodes?Literals are fixed words the player must type exactly:toolsandgive.diamondand16are values the player chooses, so they are argument nodes.A player types
/roll 500, and the argument isIntegerArgumentType.integer(2, 100). What happens?The argument type checks the range before anything runs. That is the big win overBasicCommand, where you had to write that check yourself.How do you get the players from an
ArgumentTypes.players()argument calledtargets?Selector arguments give you a resolver. Callingresolvewith the source turns the name or selector, such as@p, into real players, using the source's location.Where can a
greedyString()argument go?A greedy string swallows the rest of the input, spaces included, so no node after it could ever be reached.The
/heal <targets>node has.requires(source -> source.getSender().hasPermission("commandsbrigadier.heal.others")). A player has onlycommandsbrigadier.heal. What can they do?Each node has its ownrequires. They pass the root's test but not the argument's, so the argument branch does not exist for them. Thechildrenin plugin.yml go the other way:heal.othersincludesheal.
Next steps
- Commands, part 3: the classic plugin.yml way: understand the older style you will find in tutorials.
- Permissions: defaults, children and permission plugins in depth.
- Project: /heal and /feed with cooldowns: a complete Brigadier plugin, step by step.
- Paper plugins: registering commands even earlier, in the bootstrapper.