Paper Plugin Guide
File mode0

Paper essentials

Commands, part 1: simple commands

Make your first /command with BasicCommand and register it the modern Paper way.

Beginner46 min read

Commands are how players talk to your plugin. On this page you will build /hello, a /fly toggle and a /paylevels command that reads a player name and a number, with friendly error messages and tab completion, all with Paper's simple BasicCommand.

What is a command?

You already know commands from vanilla Minecraft: /give, /tp, /gamemode. A command is a word that starts with a slash, plus some optional words after it. When someone runs it, the server finds the code that belongs to that word and runs it.

Plugins can add their own commands. Once your plugin registers /hello, typing /hello in chat runs a method you wrote, exactly like events run your event handlers. The difference is who decides when it happens: an event fires when something happens in the game, a command runs when someone asks for it.

Players are not the only ones who can run commands. Whoever runs a command is called the sender, and there are a few kinds:

Who runs itHowJava type
A playerTypes /hello in chat.Player
The server consoleTypes hello in the server window (no slash needed there).ConsoleCommandSender
A command blockRuns the command when it gets a redstone signal.BlockCommandSender

All of them are a CommandSender: something that can run commands and receive messages. That matters, because your command must work (or fail politely) no matter who runs it. The console cannot fly, and a command block has no levels to pay with.

Paper gives you three ways to make commands. This page teaches the simplest one; the next two pages cover the others:

  • BasicCommand (this page): you get the typed words as a list of text and decide what to do. Easy to learn.
  • Brigadier (part 2): you describe the command's shape, and Minecraft checks the arguments for you, colors them while you type and suggests values.
  • The classic plugin.yml way (part 3): what most older tutorials use. It still works, but Paper recommends the first two for new code.

Your first command: /hello

A command needs exactly three pieces, the same pattern as a listener:

  1. Write a class that implements BasicCommand

    BasicCommand is an interface from Paper. By writing implements BasicCommand, your class promises to have an execute method, and Paper knows how to run it.

  2. Fill in the execute method

    Paper calls execute every time someone runs the command. Whatever you put inside it is what the command does.

  3. Register the command in onEnable

    Tell Paper "the word hello belongs to this class". Until you register it, typing /hello gives an "Unknown command" error.

This is the whole command class. Read the notes, then hover any word in the code to see what it is.

HelloCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livescommands-basic-hellosrcmainjavacomexamplecommandsbasichelloHelloCommand.java

The package com.example.commandsbasichello is the folder path com/example/commandsbasichello 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-basic-hello/
    • src/main/
      • java/com/example/commandsbasichello/Package com.example.commandsbasichello
        • HelloCommand.javayou are hereCommand (BasicCommand)
        • HelloPlugin.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.commandsbasichello;2 3import io.papermc.paper.command.brigadier.BasicCommand;4import io.papermc.paper.command.brigadier.CommandSourceStack;5import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;6import org.bukkit.command.CommandSender;7 8public final class HelloCommand implements BasicCommand {9 10    @Override11    public void execute(CommandSourceStack source, String[] args) {12        CommandSender sender = source.getSender();13        sender.sendRichMessage("<green>Hello, <name>!",14            Placeholder.unparsed("name", sender.getName()));15    }16}
  1. Paper keeps BasicCommand in its brigadier package, because under the hood Paper turns every basic command into a Brigadier command. You do not need to know Brigadier to use it.
  2. This class promises to be a command. Paper will only accept objects of classes that implement BasicCommand.
  3. An annotation that says "this method fills in a method from the interface". If you misspell execute, the compiler sees @Override and stops with a clear error instead of quietly treating it as a different method.
  4. Paper calls this method each time the command runs. It gets two things: source (who ran it and where) and args (the words typed after the command name). This first command ignores args.
  5. Asks the source who ran the command, and saves that in a variable called sender. It could be a player, the console or a command block.
  6. Sends a message back to whoever ran the command. The text is MiniMessage, so <green> makes it green and <name> is a gap that the placeholder fills.
  7. Fills the <name> gap with the sender's name as plain text. "Unparsed" means any tags inside the name are shown as text instead of being treated as formatting.

And this is the main class, which registers the command when the plugin starts:

HelloPlugin.javaCompiles on Paper 26.3Compile Lab
Where this file livescommands-basic-hellosrcmainjavacomexamplecommandsbasichelloHelloPlugin.java

The package com.example.commandsbasichello is the folder path com/example/commandsbasichello 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-basic-hello/
    • src/main/
      • java/com/example/commandsbasichello/Package com.example.commandsbasichello
        • HelloCommand.javaCommand (BasicCommand)
        • HelloPlugin.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.commandsbasichello;2 3import java.util.List;4import org.bukkit.plugin.java.JavaPlugin;5 6public final class HelloPlugin extends JavaPlugin {7 8    @Override9    public void onEnable() {10        registerCommand("hello", "Says hello to you", List.of("hi"), new HelloCommand());11    }12}
  1. Paper calls onEnable once when your plugin starts. Commands are registered here, just like listeners. See the plugin lifecycle for the full story.
  2. A shortcut built into JavaPlugin. It takes four things: the command's name, a description for /help, a list of other names (aliases), and an object of your command class.
  3. The aliases. Now /hi does the same as /hello. If you want no aliases, use List.of(), or call the shorter registerCommand("hello", "Says hello to you", new HelloCommand()).

Notice what is missing: plugin.yml has no commands: section. Old tutorials always add one, but commands registered in code do not need it. This is the whole file:

plugin.ymlCompiles on Paper 26.3Compile Lab
Where this file livescommands-basic-hellosrcmainresourcesplugin.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-basic-hello/
    • src/main/
      • java/com/example/commandsbasichello/Package com.example.commandsbasichello
        • HelloCommand.javaCommand (BasicCommand)
        • HelloPlugin.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: HelloCommands2version: '1.0.0'3main: com.example.commandsbasichello.HelloPlugin4api-version: '26.3'5description: Adds a /hello command that greets whoever runs it.
  1. The plugin's name. Paper also registers /hellocommands:hello, the command's name with your plugin's name in front, so players can still reach it if another plugin also has a /hello.

Build the plugin and start your test server with Run Paper Server (see Build, install and test). Join with localhost and type /hello:

Hello, Steve!

Now type hello in the server console window. The console is a sender too, and its name is CONSOLE:

Server console
 hello[14:05:03 INFO]: Hello, CONSOLE!

Who ran the command? The source

The first parameter of execute is a CommandSourceStack. The name is long, but the idea is simple: it is a little bundle of facts about where the command came from. You will use three of its methods:

MethodWhat it gives you
source.getSender()Who started the command: a player, the console or a command block. Send your replies here.
source.getExecutor()The entity the command should act on. Usually the same player as the sender. It is null when no entity is involved, for example when the console runs the command.
source.getLocation()Where the command runs: the player's position, or the command block's position.

Why are there two, sender and executor? Because of vanilla's /execute command. An admin in the console can type execute as Alex run fly. The console started it (the sender), but the command should act on Alex (the executor). This small command prints both, so you can see the difference:

WhoAmICommand.javaCompiles on Paper 26.3Compile Lab
Where this file livescommands-basicsrcmainjavacomexamplecommandsbasicWhoAmICommand.java

The package com.example.commandsbasic is the folder path com/example/commandsbasic 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-basic/
    • src/main/
      • java/com/example/commandsbasic/Package com.example.commandsbasic
        • CommandsBasicPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • FlyCommand.javaCommand (BasicCommand)
        • HelloCommand.javaCommand (BasicCommand)
        • PayLevelsCommand.javaCommand (BasicCommand)
        • PlayerNames.javaHelper class
        • WhoAmICommand.javayou are hereCommand (BasicCommand)
      • 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.commandsbasic;2 3import io.papermc.paper.command.brigadier.BasicCommand;4import io.papermc.paper.command.brigadier.CommandSourceStack;5import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;6import org.bukkit.command.BlockCommandSender;7import org.bukkit.command.CommandSender;8import org.bukkit.command.ConsoleCommandSender;9import org.bukkit.entity.Entity;10import org.bukkit.entity.Player;11 12public final class WhoAmICommand implements BasicCommand {13 14    @Override15    public void execute(CommandSourceStack source, String[] args) {16        CommandSender sender = source.getSender();17        Entity executor = source.getExecutor();18        sender.sendRichMessage("<gray>Sender: <white><name></white> (<kind>)",19            Placeholder.unparsed("name", sender.getName()),20            Placeholder.unparsed("kind", describe(sender)));21        if (executor == null) {22            sender.sendRichMessage("<gray>Executor: <white>none</white> (no entity is running this command)");23        } else {24            sender.sendRichMessage("<gray>Executor: <white><name></white>",25                Placeholder.unparsed("name", executor.getName()));26        }27    }28 29    private static String describe(CommandSender sender) {30        if (sender instanceof Player) {31            return "a player";32        }33        if (sender instanceof ConsoleCommandSender) {34            return "the server console";35        }36        if (sender instanceof BlockCommandSender) {37            return "a command block";38        }39        return "something else";40    }41}
  1. May be null. Entity is the type for everything that moves in the world: players, mobs, arrows, minecarts.
  2. Two placeholders in one message: the name and the kind of sender.
  3. Always check for null before using the executor, or the command crashes when the console runs it.
  4. A small helper method that turns a sender into words. Keeping it separate keeps execute short and readable.
  5. The instanceof operator asks "is this sender a player?" and answers true or false. The next section uses it a lot.

Here is what /whoami shows when Steve types it, and then when an admin types execute as Steve run whoami in the console (the console sees the reply in its window):

Sender: Steve (a player)
Executor: Steve
Server console
 execute as Steve run whoami[14:06:41 INFO]: Sender: CONSOLE (the server console)[14:06:41 INFO]: Executor: Steve

A simple rule covers almost every command: reply to the sender, act on the executor. In everyday use both are the same player, so you rarely notice the difference, but following the rule makes your commands work with /execute and command blocks too.

Only players can fly: checking with instanceof

Many commands only make sense for a player. The console cannot fly or hold items. So before doing anything, check that the executor really is a player:

FlyCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livescommands-basicsrcmainjavacomexamplecommandsbasicFlyCommand.java

The package com.example.commandsbasic is the folder path com/example/commandsbasic 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-basic/
    • src/main/
      • java/com/example/commandsbasic/Package com.example.commandsbasic
        • CommandsBasicPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • FlyCommand.javayou are hereCommand (BasicCommand)
        • HelloCommand.javaCommand (BasicCommand)
        • PayLevelsCommand.javaCommand (BasicCommand)
        • PlayerNames.javaHelper class
        • WhoAmICommand.javaCommand (BasicCommand)
      • 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.commandsbasic;2 3import io.papermc.paper.command.brigadier.BasicCommand;4import io.papermc.paper.command.brigadier.CommandSourceStack;5import org.bukkit.entity.Player;6 7public final class FlyCommand implements BasicCommand {8 9    @Override10    public void execute(CommandSourceStack source, String[] args) {11        if (!(source.getExecutor() instanceof Player player)) {12            source.getSender().sendRichMessage("<red>Only players can fly.");13            return;14        }15        boolean flightOn = !player.getAllowFlight();16        player.setAllowFlight(flightOn);17        if (flightOn) {18            player.sendRichMessage("<green>Flight on. Double-tap jump to take off.");19        } else {20            player.sendRichMessage("<yellow>Flight off.");21        }22    }23 24    @Override25    public String permission() {26        return "commandsbasic.fly";27    }28}
  1. This does two things at once. It checks "is the executor a player?", and if so, it creates a variable player of type Player that you can use below. This is called pattern matching.
  2. The ! flips the answer: "if the executor is not a player". Mind the brackets: the ! must apply to the whole instanceof check.
  3. The console gets a clear reason instead of a crash. Then return ends the method right away, a guard clause.
  4. Reads whether the player may fly now, and flips it. If flying was allowed, flightOn becomes false, and the other way round. That is the whole toggle.
  5. Lets the player fly by double-tapping jump, like in creative mode. Turning it off also brings a flying player back down.
  6. An optional method from BasicCommand. Return a permission name, and only senders with that permission can use the command.

Why does the check use the variable player afterwards, not source.getExecutor()? Because getExecutor() returns an Entity, and an Entity has no setAllowFlight method. Only Player has it. The pattern match gives you the same object, already treated as a Player.

The permission is declared in plugin.yml, so Paper knows that server operators have it by default:

plugin.ymlCompiles on Paper 26.3Compile Lab
Where this file livescommands-basicsrcmainresourcesplugin.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-basic/
    • src/main/
      • java/com/example/commandsbasic/Package com.example.commandsbasic
        • CommandsBasicPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • FlyCommand.javaCommand (BasicCommand)
        • HelloCommand.javaCommand (BasicCommand)
        • PayLevelsCommand.javaCommand (BasicCommand)
        • PlayerNames.javaHelper class
        • WhoAmICommand.javaCommand (BasicCommand)
      • 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: CommandsBasic2version: '1.0.0'3main: com.example.commandsbasic.CommandsBasicPlugin4api-version: '26.3'5description: Simple BasicCommand examples, /hello, /fly, /paylevels and /whoami.6permissions:7  commandsbasic.fly:8    description: Lets a player turn flying on and off with /fly.9    default: op
  1. The permission's name, starting with the plugin's name so it never clashes with another plugin.
  2. Operators get it automatically. Other players need it granted, for example with a permissions plugin like LuckPerms. See Permissions.

When a player with the permission runs /fly twice:

Flight on. Double-tap jump to take off.
Flight off.

A player without the permission does not even see /fly in tab completion. If they type it anyway, Minecraft treats it like a command that does not exist:

Unknown or incomplete command, see below for error
fly<--[HERE]

And when the console types fly, the guard clause answers:

Server console
 fly[14:07:15 INFO]: Only players can fly.

Arguments: the words after the command

The words a player types after the command name are its arguments. Paper hands them to execute as String[] args: an array of text. Paper removes the slash, takes the first word as the command's name (the label), and splits the rest at every space:

What the player types in chat /paylevels Alex 5 Paper drops the slash and splits the rest at every space paylevels label picks which command runs "Alex" args[0] "5" args[1] String[] args, args.length = 2 every argument is text, even "5"
How /paylevels Alex 5 reaches your code: the label picks the command, and the rest becomes the args array.

This small Java program does the same splitting, so you can watch it happen. Press Run, then try your own commands with Edit:

SplitArgs.javaRuns on Java 25
1void main() {2    show("/hello");3    show("/hello Alex");4    show("/paylevels Alex 5");5}6 7void show(String typed) {8    String withoutSlash = typed.substring(1);9    String[] words = withoutSlash.split(" ");10    String label = words[0];11    String[] args = Arrays.copyOfRange(words, 1, words.length);12 13    IO.println("You typed: " + typed);14    IO.println("  label = " + label);15    IO.println("  args.length = " + args.length);16    for (int i = 0; i < args.length; i++) {17        IO.println("  args[" + i + "] = " + args[i]);18    }19    IO.println("");20}
  1. Removes the first character, the slash.
  2. Cuts the text at every space, giving an array of words.
  3. Copies every word except the first one. Those are the arguments.
  4. How many arguments there are. It is 0 when the player typed only the command name.
  5. Arrays count from 0, so the first argument is args[0] and the second is args[1]. See Loops for how this loop works.

Three facts about args that every plugin developer learns, often the hard way:

  • Counting starts at 0. The first argument is args[0]. Paper also ignores extra spaces, so /hello   Alex still gives args[0] = Alex.
  • Everything is text. Even when a player types 5, you get the String "5", not the number 5. You have to turn it into a number yourself (see below).
  • The array can be shorter than you expect. Players forget arguments all the time. Reading args[0] when args.length is 0 crashes your command.
Crashes when the player types only /helloHas a mistake
1Player target = Bukkit.getPlayerExact(args[0]);

If a player types just /hello, the array is empty, and asking for args[0] throws an ArrayIndexOutOfBoundsException. The player sees a generic error and the console fills with a stack trace. Always check args.length first.

Reading a player name

Here is the upgraded /hello from the demo plugin. With no arguments it greets you; with a name, it waves at that player:

HelloCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livescommands-basicsrcmainjavacomexamplecommandsbasicHelloCommand.java

The package com.example.commandsbasic is the folder path com/example/commandsbasic 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-basic/
    • src/main/
      • java/com/example/commandsbasic/Package com.example.commandsbasic
        • CommandsBasicPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • FlyCommand.javaCommand (BasicCommand)
        • HelloCommand.javayou are hereCommand (BasicCommand)
        • PayLevelsCommand.javaCommand (BasicCommand)
        • PlayerNames.javaHelper class
        • WhoAmICommand.javaCommand (BasicCommand)
      • 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.

14@Override15public void execute(CommandSourceStack source, String[] args) {16    CommandSender sender = source.getSender();17    if (args.length == 0) {18        sender.sendRichMessage("<green>Hello, <name>!",19            Placeholder.unparsed("name", sender.getName()));20        return;21    }22    Player target = Bukkit.getPlayerExact(args[0]);23    if (target == null) {24        sender.sendRichMessage("<red>No online player is called <name>.",25            Placeholder.unparsed("name", args[0]));26        return;27    }28    target.sendRichMessage("<yellow><name> waves at you!",29        Placeholder.unparsed("name", sender.getName()));30    sender.sendRichMessage("<gray>You waved at <name>.",31        Placeholder.unparsed("name", target.getName()));32}
  1. Checks the length before reading anything. No arguments means "just greet me".
  2. Stop here. Without this return, the code would carry on and read args[0], which does not exist.
  3. Looks up an online player by name, ignoring upper and lower case. If nobody with that exact name is online, you get null.
  4. Players make typos. Tell them which name was not found, so they can fix it.
  5. One message to the other player, then one confirmation to the sender. Always tell the sender the command worked.
You waved at Alex.
No online player is called Alxe.

And Alex, on the other side, sees:

Steve waves at you!

Reading numbers, with friendly errors

To turn the text "5" into the number 5, Java has Integer.parseInt. If the text is not a whole number, such as "abc" or "2.5", it does not return anything. Instead it throws a NumberFormatException: an exception is Java's way of saying "I cannot do this". You catch it with try and catch and turn it into a helpful message. Run this to see what happens with different inputs:

ParseAmount.javaRuns on Java 25
1void main() {2    String[] typedAmounts = {"5", "abc", "-3", "2.5", "99999999999"};3    for (String typed : typedAmounts) {4        IO.println("/paylevels Alex " + typed + "  ->  " + check(typed));5    }6}7 8String check(String typed) {9    int amount;10    try {11        amount = Integer.parseInt(typed);12    } catch (NumberFormatException exception) {13        return typed + " is not a whole number.";14    }15    if (amount < 1) {16        return "The amount must be at least 1.";17    }18    return "OK, paying " + amount + " levels.";19}
  1. Five things a player might type. Watch the last one: 99999999999 is a whole number, but it is too big for an int (the limit is about 2.1 billion), so it fails too.
  2. Run the code inside, but be ready for it to fail.
  3. Turns text into an int. Works for "5" and "-3"; throws for anything else.
  4. If parseInt failed, Java jumps here instead of crashing. This is where you explain the problem.
  5. A number can be valid Java and still make no sense for your command. Paying -3 levels would take levels from the other player.

See Null, exceptions and stack traces if you want to know more about exceptions. Now put it all together in /paylevels <player> <levels>, which gives some of your experience levels to another player. It is a good model for any command with arguments, because it checks every way the input can be wrong, one step at a time:

PayLevelsCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livescommands-basicsrcmainjavacomexamplecommandsbasicPayLevelsCommand.java

The package com.example.commandsbasic is the folder path com/example/commandsbasic 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-basic/
    • src/main/
      • java/com/example/commandsbasic/Package com.example.commandsbasic
        • CommandsBasicPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • FlyCommand.javaCommand (BasicCommand)
        • HelloCommand.javaCommand (BasicCommand)
        • PayLevelsCommand.javayou are hereCommand (BasicCommand)
        • PlayerNames.javaHelper class
        • WhoAmICommand.javaCommand (BasicCommand)
      • 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.

16@Override17public void execute(CommandSourceStack source, String[] args) {18    CommandSender sender = source.getSender();19    if (!(source.getExecutor() instanceof Player payer)) {20        sender.sendRichMessage("<red>Only players have levels to pay with.");21        return;22    }23    if (args.length != 2) {24        sender.sendMessage(Component.text("Usage: /paylevels <player> <levels>", NamedTextColor.RED));25        return;26    }27 28    Player target = Bukkit.getPlayerExact(args[0]);29    if (target == null) {30        sender.sendRichMessage("<red>No online player is called <name>.",31            Placeholder.unparsed("name", args[0]));32        return;33    }34    if (target.equals(payer)) {35        sender.sendRichMessage("<red>You cannot pay yourself.");36        return;37    }38 39    int levels;40    try {41        levels = Integer.parseInt(args[1]);42    } catch (NumberFormatException exception) {43        sender.sendRichMessage("<red><amount> is not a whole number.",44            Placeholder.unparsed("amount", args[1]));45        return;46    }47    if (levels < 1) {48        sender.sendRichMessage("<red>You have to pay at least 1 level.");49        return;50    }51    if (payer.getLevel() < levels) {52        sender.sendRichMessage("<red>You only have <have> levels.",53            Placeholder.unparsed("have", String.valueOf(payer.getLevel())));54        return;55    }56 57    payer.setLevel(payer.getLevel() - levels);58    target.setLevel(target.getLevel() + levels);59    payer.sendRichMessage("<green>You paid <white><levels></white> levels to <white><name></white>.",60        Placeholder.unparsed("levels", String.valueOf(levels)),61        Placeholder.unparsed("name", target.getName()));62    target.sendRichMessage("<green><name> paid you <white><levels></white> levels!",63        Placeholder.unparsed("name", payer.getName()),64        Placeholder.unparsed("levels", String.valueOf(levels)));65}
  1. Only players have levels. The pattern variable is called payer here, a name that says what it is.
  2. This command needs exactly two arguments. Anything else gets the usage line.
  3. A usage line shows how to type the command. <player> in angle brackets means "required"; that is the common style. It uses Component.text instead of MiniMessage so the angle brackets are not mistaken for tags. See Text and colors.
  4. Players will try to pay themselves to see what happens. Catch it.
  5. Declared before the try, so it still exists after the try block ends.
  6. The second argument is the amount, as text. This turns it into a number or jumps to catch.
  7. The last check: does the payer have enough? Only after every check passes does anything change.
  8. The actual work, two lines: take from one player, give to the other.
  9. Tell both players what happened.

Notice the shape: a ladder of checks, each with its own message and a return, and the real work at the very bottom. When you write your own commands, copy this shape. Here is what players see for a few attempts:

Usage: /paylevels
abc is not a whole number.
You only have 3 levels.
You paid 2 levels to Alex.

Tab completion with suggest()

When a player presses Tab or simply types, Minecraft shows a little list of suggestions above the chat box. A BasicCommand fills that list with its suggest method. It gets the same source and args as execute, but it runs while the player is still typing, and it returns the words to suggest.

The trick is knowing which argument the player is typing right now. While typing, the last item in args is the word in progress:

The player has typedargs in suggestSo suggest
/paylevels empty, length 0player names
/paylevels Al["Al"], length 1player names starting with "Al"
/paylevels Alex ["Alex", ""], length 2amounts
/paylevels Alex 1["Alex", "1"], length 2amounts
PayLevelsCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livescommands-basicsrcmainjavacomexamplecommandsbasicPayLevelsCommand.java

The package com.example.commandsbasic is the folder path com/example/commandsbasic 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-basic/
    • src/main/
      • java/com/example/commandsbasic/Package com.example.commandsbasic
        • CommandsBasicPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • FlyCommand.javaCommand (BasicCommand)
        • HelloCommand.javaCommand (BasicCommand)
        • PayLevelsCommand.javayou are hereCommand (BasicCommand)
        • PlayerNames.javaHelper class
        • WhoAmICommand.javaCommand (BasicCommand)
      • 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.

67@Override68public Collection<String> suggest(CommandSourceStack source, String[] args) {69    if (args.length <= 1) {70        String typed = args.length == 0 ? "" : args[0];71        return PlayerNames.startingWith(typed);72    }73    if (args.length == 2) {74        return List.of("1", "5", "10");75    }76    return List.of();77}
  1. The method returns a collection of words. Any list works, for example List.of("1", "5", "10").
  2. Length 0 or 1 means the player is on the first argument, the name.
  3. What they have typed of the name so far, or nothing yet.
  4. A helper method, shared with /hello, that lists online players whose names start with what was typed.
  5. Second argument: a few handy amounts. Players can still type any number.
  6. Third argument and beyond: nothing to suggest. Return an empty list, never null.

Paper shows the suggestions you return, so filter them to match what the player has typed. That is the job of the helper class:

PlayerNames.javaCompiles on Paper 26.3Compile Lab
Where this file livescommands-basicsrcmainjavacomexamplecommandsbasicPlayerNames.java

The package com.example.commandsbasic is the folder path com/example/commandsbasic 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-basic/
    • src/main/
      • java/com/example/commandsbasic/Package com.example.commandsbasic
        • CommandsBasicPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • FlyCommand.javaCommand (BasicCommand)
        • HelloCommand.javaCommand (BasicCommand)
        • PayLevelsCommand.javaCommand (BasicCommand)
        • PlayerNames.javayou are hereHelper class
        • WhoAmICommand.javaCommand (BasicCommand)
      • 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.commandsbasic;2 3import java.util.ArrayList;4import java.util.List;5import java.util.Locale;6import org.bukkit.Bukkit;7import org.bukkit.entity.Player;8 9final class PlayerNames {10 11    private PlayerNames() {12    }13 14    static List<String> startingWith(String typed) {15        String typedLower = typed.toLowerCase(Locale.ROOT);16        List<String> names = new ArrayList<>();17        for (Player player : Bukkit.getOnlinePlayers()) {18            if (player.getName().toLowerCase(Locale.ROOT).startsWith(typedLower)) {19                names.add(player.getName());20            }21        }22        return names;23    }24}
  1. A helper class with no public: only classes in the same package can use it, which is all this plugin needs.
  2. A private constructor means nobody can create a PlayerNames object. The class only holds a helper method.
  3. Compares in lower case, so typing al still finds Alex. Locale.ROOT makes lower-casing behave the same on every computer.
  4. Every player who is online right now. The loop keeps only the matching names.

Registering several commands

registerCommand is a shortcut. Inside, it uses Paper's lifecycle event for commands, LifecycleEvents.COMMANDS. Most real plugins use that event directly, because it can register many commands in one place, and because it is the same way you register the Brigadier commands in part 2. This is the demo plugin's main class:

CommandsBasicPlugin.javaCompiles on Paper 26.3Compile Lab
Where this file livescommands-basicsrcmainjavacomexamplecommandsbasicCommandsBasicPlugin.java

The package com.example.commandsbasic is the folder path com/example/commandsbasic 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-basic/
    • src/main/
      • java/com/example/commandsbasic/Package com.example.commandsbasic
        • CommandsBasicPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
        • FlyCommand.javaCommand (BasicCommand)
        • HelloCommand.javaCommand (BasicCommand)
        • PayLevelsCommand.javaCommand (BasicCommand)
        • PlayerNames.javaHelper class
        • WhoAmICommand.javaCommand (BasicCommand)
      • 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.commandsbasic;2 3import io.papermc.paper.command.brigadier.Commands;4import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;5import java.util.List;6import org.bukkit.plugin.java.JavaPlugin;7 8public final class CommandsBasicPlugin extends JavaPlugin {9 10    @Override11    public void onEnable() {12        getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> {13            Commands commands = event.registrar();14            commands.register("hello", "Say hello, or wave at another player", new HelloCommand());15            commands.register("fly", "Turn flying on or off", new FlyCommand());16            commands.register("paylevels", "Give some of your experience levels to another player",17                List.of("paylvl"), new PayLevelsCommand());18            commands.register("whoami", "Show who is running this command", new WhoAmICommand());19        });20    }21}
  1. Every plugin has a lifecycle manager. It lets you register handlers for moments in the server's life, such as "now is the time to register commands".
  2. The moment Paper builds its list of commands. Paper calls your handler at that time, and may call it again later, for example when data packs are reloaded. So only register commands inside it.
  3. A lambda: a small piece of code you hand to Paper to run later. event is the name you give its parameter. See Lambdas.
  4. The registrar is the object that accepts commands. You call register on it once per command.
  5. Name, description and the command object. Without a list of aliases, the command has only its main name.
  6. With aliases, the list goes between the description and the command object.
  • commands-basic/
    • src/
      • main/
        • java/
          • com/example/commandsbasic/
            • CommandsBasicPlugin.javaThe main class: registers all four commands
            • HelloCommand.java/hello and /hello player
            • FlyCommand.java/fly, a toggle with a permission
            • PayLevelsCommand.java/paylevels player levels, with checks and suggestions
            • WhoAmICommand.java/whoami, shows sender and executor
            • PlayerNames.javaHelper that lists matching online player names
        • resources/
          • plugin.ymlName, main class and the commandsbasic.fly permission

You can download the demo plugin as a Gradle project, or open it in the Compile Lab to change it and compile it in your browser.

Mistakes to avoid

  • Forgetting to register the command. The class compiles, but the game says "Unknown or incomplete command". Check onEnable first.
  • Reading args[0] without checking args.length. The command crashes the moment someone types it with no arguments.
  • Casting the sender with (Player) sender without a check. It crashes when the console runs the command. Use instanceof Player player.
  • Calling Integer.parseInt without try/catch. The first player who types a word instead of a number causes an error.
  • Silent failures. Every return before the real work should send a message that says what was wrong and how to fix it.
  • Adding a commands: section to plugin.yml as well. Commands registered in code do not need it. Mixing both ways for the same command name confuses everyone, including you.
Try it

Roll a die

Make a new plugin with a /roll [sides] command. /roll rolls a normal six-sided die; /roll 20 rolls a 20-sided one. Reject anything that is not a whole number from 2 to 100 with a friendly message, and suggest 6, 20 and 100 while the player types. The square brackets in [sides] mean the argument is optional.

Hint 1

Start with int sides = 6; and only change it when args.length is at least 1. That is how optional arguments work.

Hint 2

A random number from 1 to sides: ThreadLocalRandom.current().nextInt(1, sides + 1). The second number is "up to but not including", hence the + 1. See Time, UUIDs and randomness.

Hint 3

The range check comes after the number is parsed: if (sides < 2 || sides > 100).

Show the solution

The default value makes the argument optional. The checks follow the same ladder as /paylevels: parse, check the range, and only then do the work.

RollCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livescommands-basic-exercisesrcmainjavacomexamplecommandsbasicexerciseRollCommand.java

The package com.example.commandsbasicexercise is the folder path com/example/commandsbasicexercise 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-basic-exercise/
    • src/main/
      • java/com/example/commandsbasicexercise/Package com.example.commandsbasicexercise
        • RollCommand.javayou are hereCommand (BasicCommand)
        • RollPlugin.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.commandsbasicexercise;2 3import io.papermc.paper.command.brigadier.BasicCommand;4import io.papermc.paper.command.brigadier.CommandSourceStack;5import java.util.Collection;6import java.util.List;7import java.util.concurrent.ThreadLocalRandom;8import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;9import org.bukkit.command.CommandSender;10 11public final class RollCommand implements BasicCommand {12 13    @Override14    public void execute(CommandSourceStack source, String[] args) {15        CommandSender sender = source.getSender();16        int sides = 6;17        if (args.length >= 1) {18            try {19                sides = Integer.parseInt(args[0]);20            } catch (NumberFormatException exception) {21                sender.sendRichMessage("<red><typed> is not a whole number.",22                    Placeholder.unparsed("typed", args[0]));23                return;24            }25        }26        if (sides < 2 || sides > 100) {27            sender.sendRichMessage("<red>A die needs between 2 and 100 sides.");28            return;29        }30        int result = ThreadLocalRandom.current().nextInt(1, sides + 1);31        sender.sendRichMessage("<gray>You rolled a <gold><result></gold> on a <sides>-sided die.",32            Placeholder.unparsed("result", String.valueOf(result)),33            Placeholder.unparsed("sides", String.valueOf(sides)));34    }35 36    @Override37    public Collection<String> suggest(CommandSourceStack source, String[] args) {38        if (args.length <= 1) {39            return List.of("6", "20", "100");40        }41        return List.of();42    }43}
  1. The default, used when the player typed no argument.
  2. Only read args[0] when it exists.
  3. A one-sided die makes no sense, and a huge one is just silly.
  4. A random whole number from 1 up to and including sides.
RollPlugin.javaCompiles on Paper 26.3Compile Lab
Where this file livescommands-basic-exercisesrcmainjavacomexamplecommandsbasicexerciseRollPlugin.java

The package com.example.commandsbasicexercise is the folder path com/example/commandsbasicexercise 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-basic-exercise/
    • src/main/
      • java/com/example/commandsbasicexercise/Package com.example.commandsbasicexercise
        • RollCommand.javaCommand (BasicCommand)
        • RollPlugin.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.commandsbasicexercise;2 3import java.util.List;4import org.bukkit.plugin.java.JavaPlugin;5 6public final class RollPlugin extends JavaPlugin {7 8    @Override9    public void onEnable() {10        registerCommand("roll", "Roll a die with any number of sides", List.of("dice"), new RollCommand());11    }12}
You rolled a 17 on a 20-sided die.
A die needs between 2 and 100 sides.
Try it

Another way to say hello

In the demo plugin, make /hey work exactly like /hello, without writing a new class.

Hint

Other names for a command are aliases. Look at how /paylevels got /paylvl.

Show the solution

Add a list of aliases between the description and the command object:

CommandsBasicPlugin.java (changed line)
1commands.register("hello", "Say hello, or wave at another player", List.of("hey"), new HelloCommand());

When to move to Brigadier

BasicCommand is perfect for small commands. As commands grow, its limits start to show:

  • You parse everything yourself. Every number, every player name, every check is your code. Brigadier has ready-made argument types for players, numbers, items, worlds, game modes and more.
  • The game does not know your arguments. To Minecraft, everything after /paylevels is just text. With Brigadier, the game colors wrong arguments red while the player types, and even understands selectors like @a and @p.
  • Subcommands get messy. /team create, /team join, /team leave turn into a big if/else on args[0]. Brigadier makes each subcommand its own branch of a tree.

A good rule: one or two simple arguments, use BasicCommand. Typed arguments, selectors or subcommands, use Brigadier. Both are modern Paper, and one plugin can mix them freely.

Recap

  • A command runs your code when a player, the console or a command block asks for it. Whoever runs it is the sender.
  • A BasicCommand has an execute(source, args) method, and optionally suggest and permission.
  • Register commands in onEnable, with registerCommand(...) or with the LifecycleEvents.COMMANDS event. No plugin.yml entry is needed.
  • source.getSender() is who to reply to; source.getExecutor() is who to act on. Check instanceof Player player before doing player things.
  • args is an array of text that starts at index 0. Check args.length before reading, and parse numbers with Integer.parseInt inside try/catch.
  • Check every way the input can be wrong, send a helpful message for each, and do the real work last.

Quick quiz

  1. A player types /paylevels Alex 5. What is args[1]?

  2. The console runs fly. What happens with the FlyCommand from this page?

  3. What happens when Integer.parseInt("abc") runs outside a try block?

  4. An admin types execute as Steve run whoami in the console. Who is the sender and who is the executor?

  5. Your BasicCommand returns "myplugin.fly" from permission(). What does a player without that permission experience?

Next steps