Paper Plugin Guide
File mode0

Paper essentials

Commands, part 3: the classic plugin.yml way

Read and write the CommandExecutor style used in most older tutorials.

Intermediate27 min read

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:

  1. 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.

  2. Write a class that implements CommandExecutor

    Its onCommand method runs whenever someone uses the command, just like execute in a BasicCommand.

  3. Connect them in onEnable

    Ask Paper for the command with getCommand("name") and give it your class with setExecutor.

When the server starts plugin.yml commands: entry Paper creates a PluginCommand in onEnable getCommand(...) setExecutor setTabCompleter your class is what runs When someone runs it Player types /spawnzombie 5 Permission check onCommand(...) your code runs return true done return false Paper sends usage no permission: your code never runs
The classic flow. plugin.yml creates the command, onEnable connects your class to it, and the value onCommand returns decides whether the usage line is shown.

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

plugin.ymlCompiles on Paper 26.3Compile Lab
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
    • 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

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
  1. 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).
  2. The command's name, in lower case. This exact word is what getCommand("spawnzombie") will look for.
  3. The command's description, shown in /help. (The description near the top describes the whole plugin.)
  4. The line Paper sends when your onCommand returns false. <command> is replaced with whatever the player typed, so it shows /zombie if they used the alias. It is in quotes because of the colon inside it; an unquoted colon confuses YAML.
  5. Other names for the same command, as a YAML list. /zombie 3 and /sz 3 work too.
  6. The permission needed to use the command. Without it, your onCommand never runs for that sender.
  7. The permission itself is declared in the permissions: section, as usual. default: op means 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:

SpawnZombieCommand.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. The method Paper calls when the command runs. It has four parameters (explained in the table below) and returns true or false.
  2. The same check as in part 1. Classic commands only get a sender, not a source, so you check the sender itself.
  3. Returning true means "I handled it", even when "handling it" was an error message. Paper then does nothing more.
  4. The default, used when no amount is typed. Optional arguments work exactly like in part 1.
  5. Returning false means "the player typed it wrong". Paper then sends the usage line from plugin.yml. Here, that happens when the amount is not a number.
  6. A constant: a named value that never changes, declared at the top of the class. Better than a bare 10 that nobody understands later.
  7. 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.
  8. 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:

ParameterWhat it isIn /zombie 3
CommandSender senderWho ran the command: a player, the console, a command block.Steve
Command commandThe command object Paper created from plugin.yml. Useful when one executor handles several commands: command.getName() is always the main name.spawnzombie
String labelThe word the player actually typed, which can be an alias.zombie
String[] argsThe 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 the usage line 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:

Spawned 3 zombie(s). Good luck!
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

SpawnZombiePlugin.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Asks Paper for the command it created from plugin.yml. The name must match the key under commands:.
  2. If the command is not in plugin.yml, getCommand returns null. This check turns a confusing crash into a clear message in the console.
  3. Prints an error line in the console with your plugin's name in front. severe is the logger's word for "error".
  4. One object that does both jobs, because the class implements TabExecutor.
  5. From now on, /spawnzombie runs this object's onCommand.
  6. And its onTabComplete fills the suggestions. Paper would also find it without this line, because the executor is a TabCompleter, but saying it makes the code clearer.

The classic NullPointerException

Almost every tutorial writes the connection in one line:

SpawnZombiePlugin.java (tutorial style)Old way
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:

Server console
[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:

SpawnZombieCommand.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. The same four parameters as onCommand. It returns the list of suggestions.
  2. The player is typing the first argument. In the classic style, args always contains the word being typed, even if it is still empty, so this is 1 and not 0.
  3. No suggestions for any other position. Never return null here 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

BasicCommandBrigadierCommandExecutor
Declared inCode onlyCode onlyplugin.yml and code
Registered withregisterCommand or LifecycleEvents.COMMANDSLifecycleEvents.COMMANDSgetCommand(...).setExecutor(...)
Arguments arrive asString[] argsTyped values (int, Player, ItemStack...)String[] args
Who checks the inputYouThe game, while the player typesYou
Selectors like @aNoYesNo
Wrong inputYour own messageAutomatic red errorreturn false shows usage
SuggestionssuggestBuilt into types, plus suggestsonTabComplete
Permissionpermission()requires on any nodepermission: in plugin.yml
Works in paper-plugin.yml pluginsYesYesNo
Paper's advice in 26.3Recommended for simple commandsRecommendedObsolete

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.

HealCommand.java (old tutorial)Old way
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}
plugin.yml (old tutorial)Old way
1commands:2  heal:3    description: Heals you4    permission: healer.heal

And the same command, translated to a modern BasicCommand. It compiles on Paper 26.3 with no deprecated calls:

HealCommand.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Was implements CommandExecutor.
  2. Was public boolean onCommand(...) with four parameters. It returns nothing now; there is no usage line to trigger.
  3. Was a separate instanceof check and an extra cast line. The pattern does both. It also checks the executor, so /execute as works.
  4. Was ChatColor.RED + "...". ChatColor is deprecated; MiniMessage tags replace it. See MiniMessage.
  5. Was player.getMaxHealth(), which is deprecated. The attribute gives the real maximum health.
  6. Was permission: healer.heal under commands: in plugin.yml.
HealerPlugin.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Was getCommand("heal").setExecutor(new HealCommand()); plus the commands: entry. One line, no plugin.yml entry, no chance of the null crash.

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 tutorialModern Paper
implements CommandExecutorimplements 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.ymlregisterCommand(name, description, aliases, command)
permission: under the commandpermission() 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. getCommand returns null and the plugin crashes on startup. Check for null, as the example does.
  • return false after success. Players see the usage line every time. Return true whenever you handled the command, even with an error message.
  • Importing the wrong Command. onCommand needs org.bukkit.command.Command. Brigadier also has a class called Command (com.mojang.brigadier.Command). If IntelliJ offers both, pick the org.bukkit one here.
  • Casting without checking. (Player) sender crashes when the console runs the command. Use the instanceof pattern.
  • 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 plus registerCommand for the same name gives you two commands fighting over one word. Pick one way per command.
Try it

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.

BroadcastCommand.java (old tutorial)Old way
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:

BroadcastCommand.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Was return false. Now the command explains itself.
  2. Turns ["Server", "restart", "in", "5", "minutes"] back into one sentence.
  3. Sends the message to every player and to the console.
  4. The message is inserted as plain text, so a typed <rainbow> stays visible instead of changing colors.
BroadcasterPlugin.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
plugin.ymlCompiles on Paper 26.3Compile Lab
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
    • 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

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: op
[Broadcast] Server restart in 5 minutes

Recap

  • Classic commands need three pieces that agree on the name: a commands: entry in plugin.yml, a CommandExecutor class, and getCommand(...).setExecutor(...) in onEnable.
  • onCommand(sender, command, label, args): label is the word typed (maybe an alias); return false sends the usage line, return true means handled.
  • getCommand returns null when the command is missing from plugin.yml, the classic NullPointerException. Check for null.
  • TabCompleter (or TabExecutor, both in one) suggests values with onTabComplete. Returning null suggests player names.
  • Paper 26.3 marks this style obsolete. Write new commands with BasicCommand or Brigadier, and translate old code with the table above.

Quick quiz

  1. Your onCommand sends "Done!" and then returns false. What does the player see?

  2. The plugin crashes on startup with "because the return value of ... getCommand(String) is null". What is the fix?

  3. A player types /sz 4, where sz is an alias of spawnzombie. What is label?

  4. Which way should you choose for a brand-new /warp <name> command in your own plugin?

Next steps