Commands, part 3: the classic plugin.yml way
Read and write the CommandExecutor style used in most older tutorials.
Most plugin tutorials, videos and forum answers make commands the classic way: a commands: section in plugin.yml and a class with an onCommand method. On this page you will learn to read and write that style, fix its most famous crash, and translate old tutorial code into modern Paper code.
Why learn the old way?
You already know two modern ways to make commands: BasicCommand from part 1 and Brigadier trees from part 2. So why a third?
Because Bukkit plugins have been written this way for more than ten years. When you search "how to make a command in a Minecraft plugin", most results show the classic CommandExecutor style. Many open-source plugins you might read or fix use it too. You need to understand it, even if you write new commands the modern way.
The classic way still works on Paper 26.3 for plugins that use plugin.yml. But Paper marked its interfaces as obsolete in 26.3, and the documentation of CommandExecutor now says to prefer Brigadier or BasicCommand. Obsolete means "still supported, but there is a better way"; it is not an error, and your code still compiles without warnings.
The three pieces
A classic command is spread over three places, and all three must agree on the command's name:
Declare the command in plugin.yml
A
commands:section lists every command with its description, usage line, aliases and permission. When the server starts, Paper reads this list and creates the commands.Write a class that implements CommandExecutor
Its
onCommandmethod runs whenever someone uses the command, just likeexecutein aBasicCommand.Connect them in onEnable
Ask Paper for the command with
getCommand("name")and give it your class withsetExecutor.
The example on this page is /spawnzombie [amount], which spawns one to ten zombies next to you, each named after you. Let's build it piece by piece.
Step 1: the commands section
Where this file livescommands-legacysrcmainresourcesplugin.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-legacy/
- src/main/
- java/com/example/commandslegacy/Package com.example.commandslegacy
- SpawnZombieCommand.javaCommand (classic CommandExecutor)
- SpawnZombiePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- 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/commandslegacy/Package com.example.commandslegacy
- 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: SpawnZombie2version: '1.0.0'3main: com.example.commandslegacy.SpawnZombiePlugin4api-version: '26.3'5description: A classic CommandExecutor and TabCompleter command, /spawnzombie [amount].6commands:7 spawnzombie:8 description: Spawn zombies next to you.9 usage: "Usage: /<command> [amount]"10 aliases: [zombie, sz]11 permission: spawnzombie.use12permissions:13 spawnzombie.use:14 description: Lets a player use /spawnzombie.15 default: op- The list of commands this plugin adds. Each command is a key indented under it, with its settings indented once more. Use spaces, never tabs (see plugin.yml explained).
- The command's name, in lower case. This exact word is what
getCommand("spawnzombie")will look for. - The command's description, shown in
/help. (Thedescriptionnear the top describes the whole plugin.) - The line Paper sends when your
onCommandreturnsfalse.<command>is replaced with whatever the player typed, so it shows/zombieif they used the alias. It is in quotes because of the colon inside it; an unquoted colon confuses YAML. - Other names for the same command, as a YAML list.
/zombie 3and/sz 3work too. - The permission needed to use the command. Without it, your
onCommandnever runs for that sender. - The permission itself is declared in the
permissions:section, as usual.default: opmeans only operators have it unless it is granted.
Older tutorials also use a permission-message: field to change the "no permission" text. Paper has deprecated it: players without the permission do not see the command in tab completion, and typing it gives "Unknown or incomplete command", just like a requires in Brigadier. Leave that field out.
Step 2: CommandExecutor and onCommand
The command class implements TabExecutor, which is simply CommandExecutor and TabCompleter combined into one interface, so one class can run the command and suggest values. Here is the part that runs the command:
Where this file livescommands-legacysrcmainjavacomexamplecommandslegacySpawnZombieCommand.java
The package com.example.commandslegacy is the folder path com/example/commandslegacy 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-legacy/
- src/main/
- java/com/example/commandslegacy/Package com.example.commandslegacy
- SpawnZombieCommand.javayou are hereCommand (classic CommandExecutor)
- SpawnZombiePlugin.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/commandslegacy/Package com.example.commandslegacy
- 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.
18@Override19public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {20 if (!(sender instanceof Player player)) {21 sender.sendRichMessage("<red>Only players can spawn zombies next to themselves.");22 return true;23 }24 25 int amount = 1;26 if (args.length >= 1) {27 try {28 amount = Integer.parseInt(args[0]);29 } catch (NumberFormatException exception) {30 return false;31 }32 }33 if (amount < 1 || amount > MAX_ZOMBIES) {34 player.sendRichMessage("<red>You can spawn 1 to 10 zombies at a time.");35 return true;36 }37 38 Location location = player.getLocation();39 Component zombieName = Component.text(player.getName() + "'s zombie", NamedTextColor.GREEN);40 for (int i = 0; i < amount; i++) {41 player.getWorld().spawn(location, Zombie.class, zombie -> zombie.customName(zombieName));42 }43 player.sendRichMessage("<green>Spawned <white><amount></white> zombie(s). Good luck!",44 Placeholder.unparsed("amount", String.valueOf(amount)));45 return true;46}- The method Paper calls when the command runs. It has four
parameters (explained in the table below) and returnstrueorfalse. - The same check as in part 1. Classic commands only get a sender, not a source, so you check the sender itself.
- Returning
truemeans "I handled it", even when "handling it" was an error message. Paper then does nothing more. - The default, used when no amount is typed. Optional arguments work exactly like in part 1.
- Returning
falsemeans "the player typed it wrong". Paper then sends theusageline fromplugin.yml. Here, that happens when the amount is not a number. - A
constant : a named value that never changes, declared at the top of the class. Better than a bare 10 that nobody understands later. - Spawns a zombie at the player's location. The lambda at the end runs on the new zombie before it appears, so it shows up already named. See Entities and mobs.
- Gives the zombie a name tag, as a component.
The four parameters of onCommand carry the same information you know from BasicCommand, in a slightly different form:
| Parameter | What it is | In /zombie 3 |
|---|---|---|
CommandSender sender | Who ran the command: a player, the console, a command block. | Steve |
Command command | The command object Paper created from plugin.yml. Useful when one executor handles several commands: command.getName() is always the main name. | spawnzombie |
String label | The word the player actually typed, which can be an alias. | zombie |
String[] args | The words after the label, exactly like in part 1. | ["3"] |
And the return value is the classic way's own idea. It trips up a lot of beginners, so remember it as a rule:
return true: the command was used correctly. You already sent any messages yourself.return false: the player typed it wrong. Paper sends theusageline for you.
A very common mistake from tutorials is return false at the end of every onCommand. Then every successful use also prints the usage line, and players think something went wrong. Here is what Steve sees for /zombie 3, then /zombie lots, then /zombie 50:
Usage: /zombie [amount]
You can spawn 1 to 10 zombies at a time.
The second line is the usage from plugin.yml, with <command> replaced by the alias he typed. It is plain white text, because Paper sends it as it is.
Step 3: connecting it in onEnable
Where this file livescommands-legacysrcmainjavacomexamplecommandslegacySpawnZombiePlugin.java
The package com.example.commandslegacy is the folder path com/example/commandslegacy 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-legacy/
- src/main/
- java/com/example/commandslegacy/Package com.example.commandslegacy
- SpawnZombieCommand.javaCommand (classic CommandExecutor)
- SpawnZombiePlugin.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/commandslegacy/Package com.example.commandslegacy
- 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.commandslegacy;2 3import org.bukkit.command.PluginCommand;4import org.bukkit.plugin.java.JavaPlugin;5 6public final class SpawnZombiePlugin extends JavaPlugin {7 8 @Override9 public void onEnable() {10 PluginCommand command = getCommand("spawnzombie");11 if (command == null) {12 getLogger().severe("The spawnzombie command is missing from plugin.yml.");13 return;14 }15 SpawnZombieCommand spawnZombie = new SpawnZombieCommand();16 command.setExecutor(spawnZombie);17 command.setTabCompleter(spawnZombie);18 }19}- Asks Paper for the command it created from
plugin.yml. The name must match the key undercommands:. - If the command is not in
plugin.yml,getCommandreturnsnull. This check turns a confusing crash into a clear message in the console. - Prints an error line in the console with your plugin's name in front.
severeis the logger's word for "error". - One object that does both jobs, because the class implements
TabExecutor. - From now on,
/spawnzombieruns this object'sonCommand. - And its
onTabCompletefills the suggestions. Paper would also find it without this line, because the executor is aTabCompleter, but saying it makes the code clearer.
The classic NullPointerException
Almost every tutorial writes the connection in one line:
1getCommand("spawnzombie").setExecutor(new SpawnZombieCommand());That line is fine as long as plugin.yml has the command. But forget the commands: entry, or spell it differently (spawnZombie, spawn-zombie), and getCommand returns null. Calling setExecutor on null throws a NullPointerException, and your whole plugin fails to start:
[14:10:02 INFO]: [SpawnZombie] Enabling SpawnZombie v1.0.0[14:10:02 ERROR]: Error occurred while enabling SpawnZombie v1.0.0 (Is it up to date?)java.lang.NullPointerException: Cannot invoke "org.bukkit.command.PluginCommand.setExecutor(org.bukkit.command.CommandExecutor)" because the return value of "com.example.commandslegacy.SpawnZombiePlugin.getCommand(String)" is null at com.example.commandslegacy.SpawnZombiePlugin.onEnable(SpawnZombiePlugin.java:10)Java's message tells you exactly what happened if you read it slowly: it could not call setExecutor, because getCommand(String) returned null, in onEnable at line 10. Whenever you see this, open plugin.yml and compare the command name letter by letter. The Error Doctor can explain errors like this one for you, and Null, exceptions and stack traces teaches you to read them.
Tab completion: TabCompleter
The second method of TabExecutor fills the suggestion list while the player types:
Where this file livescommands-legacysrcmainjavacomexamplecommandslegacySpawnZombieCommand.java
The package com.example.commandslegacy is the folder path com/example/commandslegacy 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-legacy/
- src/main/
- java/com/example/commandslegacy/Package com.example.commandslegacy
- SpawnZombieCommand.javayou are hereCommand (classic CommandExecutor)
- SpawnZombiePlugin.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/commandslegacy/Package com.example.commandslegacy
- 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.
48@Override49public List<String> onTabComplete(CommandSender sender, Command command, String label, String[] args) {50 if (args.length == 1) {51 return List.of("1", "5", "10");52 }53 return List.of();54}- The same four parameters as
onCommand. It returns the list of suggestions. - The player is typing the first argument. In the classic style,
argsalways contains the word being typed, even if it is still empty, so this is1and not0. - No suggestions for any other position. Never return
nullhere unless you want the default (see below).
One difference from BasicCommand's suggest: while typing the first argument of a classic command, args is [""] (one empty word), never empty. And if you return null instead of a list, Paper fills in the names of online players. That is handy for commands that take a player name, and confusing everywhere else, which is why the example returns an empty list.
Comparing the three ways
BasicCommand | Brigadier | CommandExecutor | |
|---|---|---|---|
| Declared in | Code only | Code only | plugin.yml and code |
| Registered with | registerCommand or LifecycleEvents.COMMANDS | LifecycleEvents.COMMANDS | getCommand(...).setExecutor(...) |
| Arguments arrive as | String[] args | Typed values (int, Player, ItemStack...) | String[] args |
| Who checks the input | You | The game, while the player types | You |
Selectors like @a | No | Yes | No |
| Wrong input | Your own message | Automatic red error | return false shows usage |
| Suggestions | suggest | Built into types, plus suggests | onTabComplete |
| Permission | permission() | requires on any node | permission: in plugin.yml |
Works in paper-plugin.yml plugins | Yes | Yes | No |
| Paper's advice in 26.3 | Recommended for simple commands | Recommended | Obsolete |
For your own new code: use BasicCommand for small commands with a word or two after them, and Brigadier for anything with typed arguments, selectors or subcommands. Use CommandExecutor only when you are working inside an existing plugin that already uses it, so the code stays consistent.
Translating an old tutorial
Here is a typical /heal from an older tutorial. It has the three pieces, plus two more old habits: ChatColor for colors and getMaxHealth(), which is deprecated.
1public class HealCommand implements CommandExecutor {2 3 @Override4 public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {5 if (!(sender instanceof Player)) {6 sender.sendMessage(ChatColor.RED + "Only players can use this!");7 return true;8 }9 Player player = (Player) sender;10 player.setHealth(player.getMaxHealth());11 player.setFoodLevel(20);12 player.sendMessage(ChatColor.GREEN + "You have been healed!");13 return true;14 }15}1commands:2 heal:3 description: Heals you4 permission: healer.healAnd the same command, translated to a modern BasicCommand. It compiles on Paper 26.3 with no deprecated calls:
Where this file livescommands-legacy-translatedsrcmainjavacomexamplecommandslegacytranslatedHealCommand.java
The package com.example.commandslegacytranslated is the folder path com/example/commandslegacytranslated 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-legacy-translated/
- src/main/
- java/com/example/commandslegacytranslated/Package com.example.commandslegacytranslated
- HealCommand.javayou are hereCommand (BasicCommand)
- HealerPlugin.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/commandslegacytranslated/Package com.example.commandslegacytranslated
- 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.commandslegacytranslated;2 3import io.papermc.paper.command.brigadier.BasicCommand;4import io.papermc.paper.command.brigadier.CommandSourceStack;5import org.bukkit.attribute.Attribute;6import org.bukkit.attribute.AttributeInstance;7import org.bukkit.entity.Player;8 9public final class HealCommand implements BasicCommand {10 11 @Override12 public void execute(CommandSourceStack source, String[] args) {13 if (!(source.getExecutor() instanceof Player player)) {14 source.getSender().sendRichMessage("<red>Only players can use this!");15 return;16 }17 AttributeInstance maxHealth = player.getAttribute(Attribute.MAX_HEALTH);18 player.setHealth(maxHealth == null ? 20.0 : maxHealth.getValue());19 player.setFoodLevel(20);20 player.sendRichMessage("<green>You have been healed!");21 }22 23 @Override24 public String permission() {25 return "healer.heal";26 }27}- Was
implements CommandExecutor. - Was
public boolean onCommand(...)with four parameters. It returns nothing now; there is no usage line to trigger. - Was a separate
instanceofcheck and an extra cast line. The pattern does both. It also checks the executor, so/execute asworks. - Was
ChatColor.RED + "...".ChatColoris deprecated; MiniMessage tags replace it. See MiniMessage. - Was
player.getMaxHealth(), which is deprecated. The attribute gives the real maximum health. - Was
permission: healer.healundercommands:inplugin.yml.
Where this file livescommands-legacy-translatedsrcmainjavacomexamplecommandslegacytranslatedHealerPlugin.java
The package com.example.commandslegacytranslated is the folder path com/example/commandslegacytranslated 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-legacy-translated/
- src/main/
- java/com/example/commandslegacytranslated/Package com.example.commandslegacytranslated
- HealCommand.javaCommand (BasicCommand)
- HealerPlugin.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/commandslegacytranslated/Package com.example.commandslegacytranslated
- 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.commandslegacytranslated;2 3import org.bukkit.plugin.java.JavaPlugin;4 5public final class HealerPlugin extends JavaPlugin {6 7 @Override8 public void onEnable() {9 registerCommand("heal", "Heal yourself", new HealCommand());10 }11}- Was
getCommand("heal").setExecutor(new HealCommand());plus thecommands:entry. One line, noplugin.ymlentry, no chance of thenullcrash.
The permission stays in plugin.yml's permissions: section, because that is where every permission's default lives. Only the commands: section goes away. Use this table when you translate any old command:
| Old tutorial | Modern Paper |
|---|---|
implements CommandExecutor | implements BasicCommand |
onCommand(sender, command, label, args) | execute(source, args), with source.getSender() |
Player player = (Player) sender; | if (!(source.getExecutor() instanceof Player player)) guard |
return true; | return; |
return false; (shows usage) | Send your own usage message, then return; |
onTabComplete(...) | suggest(source, args) (remember: args can be empty) |
commands: entry in plugin.yml | registerCommand(name, description, aliases, command) |
permission: under the command | permission() method |
ChatColor.GREEN + "text" | sendRichMessage("<green>text") |
Bukkit.broadcastMessage(...) | Bukkit.broadcast(component) |
Mistakes to avoid
- The command is missing from
plugin.yml, or spelled differently.getCommandreturnsnulland the plugin crashes on startup. Check fornull, as the example does. return falseafter success. Players see the usage line every time. Returntruewhenever you handled the command, even with an error message.- Importing the wrong
Command.onCommandneedsorg.bukkit.command.Command. Brigadier also has a class calledCommand(com.mojang.brigadier.Command). If IntelliJ offers both, pick theorg.bukkitone here. - Casting without checking.
(Player) sendercrashes when the console runs the command. Use theinstanceofpattern. - Copying deprecated calls from old tutorials.
ChatColor,getMaxHealth(),setDisplayName(String)and friends still appear in tutorials. When IntelliJ strikes a name through, look up the modern replacement in the Paper cheat sheet. - Registering the same name twice. A
commands:entry plusregisterCommandfor the same name gives you two commands fighting over one word. Pick one way per command.
Modernize a broadcast command
This /broadcast command comes from an old tutorial. Rewrite it as a BasicCommand in a new plugin: no commands: section, no ChatColor, no deprecated broadcastMessage. Keep the alias /bc, and require the permission broadcaster.broadcast.
1public class BroadcastCommand implements CommandExecutor {2 3 @Override4 public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {5 if (args.length == 0) {6 return false;7 }8 String message = String.join(" ", args);9 Bukkit.broadcastMessage(ChatColor.GOLD + "[Broadcast] " + ChatColor.WHITE + message);10 return true;11 }12}Hint 1
Walk down the translation table line by line. return false becomes your own usage message.
Hint 2
To send a component to everyone, use Bukkit.broadcast(component). Build the component with MiniMessage and put the message in with Placeholder.unparsed, so tags inside the message are shown as text.
Show the solution
String.join(" ", args) glues the words back together with spaces between them. The rest is the translation table:
Where this file livescommands-legacy-exercisesrcmainjavacomexamplecommandslegacyexerciseBroadcastCommand.java
The package com.example.commandslegacyexercise is the folder path com/example/commandslegacyexercise 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-legacy-exercise/
- src/main/
- java/com/example/commandslegacyexercise/Package com.example.commandslegacyexercise
- BroadcastCommand.javayou are hereCommand (BasicCommand)
- BroadcasterPlugin.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/commandslegacyexercise/Package com.example.commandslegacyexercise
- 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.commandslegacyexercise;2 3import io.papermc.paper.command.brigadier.BasicCommand;4import io.papermc.paper.command.brigadier.CommandSourceStack;5import net.kyori.adventure.text.Component;6import net.kyori.adventure.text.format.NamedTextColor;7import net.kyori.adventure.text.minimessage.MiniMessage;8import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;9import org.bukkit.Bukkit;10 11public final class BroadcastCommand implements BasicCommand {12 13 private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage();14 15 @Override16 public void execute(CommandSourceStack source, String[] args) {17 if (args.length == 0) {18 source.getSender().sendMessage(Component.text("Usage: /broadcast <message>", NamedTextColor.RED));19 return;20 }21 String message = String.join(" ", args);22 Bukkit.broadcast(MINI_MESSAGE.deserialize("<gold>[Broadcast]</gold> <message>",23 Placeholder.unparsed("message", message)));24 }25 26 @Override27 public String permission() {28 return "broadcaster.broadcast";29 }30}- Was
return false. Now the command explains itself. - Turns
["Server", "restart", "in", "5", "minutes"]back into one sentence. - Sends the message to every player and to the console.
- The message is inserted as plain text, so a typed
<rainbow>stays visible instead of changing colors.
Where this file livescommands-legacy-exercisesrcmainjavacomexamplecommandslegacyexerciseBroadcasterPlugin.java
The package com.example.commandslegacyexercise is the folder path com/example/commandslegacyexercise 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-legacy-exercise/
- src/main/
- java/com/example/commandslegacyexercise/Package com.example.commandslegacyexercise
- BroadcastCommand.javaCommand (BasicCommand)
- BroadcasterPlugin.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/commandslegacyexercise/Package com.example.commandslegacyexercise
- 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.commandslegacyexercise;2 3import java.util.List;4import org.bukkit.plugin.java.JavaPlugin;5 6public final class BroadcasterPlugin extends JavaPlugin {7 8 @Override9 public void onEnable() {10 registerCommand("broadcast", "Send a message to everyone", List.of("bc"), new BroadcastCommand());11 }12}Where this file livescommands-legacy-exercisesrcmainresourcesplugin.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-legacy-exercise/
- src/main/
- java/com/example/commandslegacyexercise/Package com.example.commandslegacyexercise
- BroadcastCommand.javaCommand (BasicCommand)
- BroadcasterPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- 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/commandslegacyexercise/Package com.example.commandslegacyexercise
- 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: Broadcaster2version: '1.0.0'3main: com.example.commandslegacyexercise.BroadcasterPlugin4api-version: '26.3'5description: An old tutorial /broadcast command, rewritten as a BasicCommand.6permissions:7 broadcaster.broadcast:8 description: Lets a player send a broadcast to everyone.9 default: opRecap
- Classic commands need three pieces that agree on the name: a
commands:entry inplugin.yml, aCommandExecutorclass, andgetCommand(...).setExecutor(...)inonEnable. onCommand(sender, command, label, args):labelis the word typed (maybe an alias);return falsesends theusageline,return truemeans handled.getCommandreturnsnullwhen the command is missing fromplugin.yml, the classicNullPointerException. Check fornull.TabCompleter(orTabExecutor, both in one) suggests values withonTabComplete. Returningnullsuggests player names.- Paper 26.3 marks this style obsolete. Write new commands with
BasicCommandor Brigadier, and translate old code with the table above.
Quick quiz
Your
onCommandsends "Done!" and then returnsfalse. What does the player see?falsetells Paper the command was used wrong, so it sends the usage line after your message. Returntruewhenever you handled the command.The plugin crashes on startup with "because the return value of ... getCommand(String) is null". What is the fix?
getCommandonly finds commands declared inplugin.yml. A missing or misspelled entry givesnull, and callingsetExecutoronnullthrows theNullPointerException.A player types
/sz 4, whereszis an alias ofspawnzombie. What islabel?labelis the word the player actually typed.command.getName()would give the main name,spawnzombie, andargs[0]is"4".Which way should you choose for a brand-new
/warp <name>command in your own plugin?All three work in aplugin.ymlplugin, but Paper 26.3 marksCommandExecutoras obsolete and recommends the modern ways for new code. Learn the classic way to read old code, not to write new code.
Next steps
- plugin.yml explained: every field, including
commands:andpermissions:. - Permissions: defaults, children and permission plugins.
- MiniMessage: the modern replacement for
ChatColorin all those old tutorials. - plugin.yml Builder: generate a
plugin.ymlwith commands and permissions.