Commands, part 1: simple commands
Make your first /command with BasicCommand and register it the modern Paper way.
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 it | How | Java type |
|---|---|---|
| A player | Types /hello in chat. | Player |
| The server console | Types hello in the server window (no slash needed there). | ConsoleCommandSender |
| A command block | Runs 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.ymlway (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:
Write a class that implements BasicCommand
BasicCommandis an interface from Paper. By writingimplements BasicCommand, your class promises to have anexecutemethod, and Paper knows how to run it.Fill in the execute method
Paper calls
executeevery time someone runs the command. Whatever you put inside it is what the command does.Register the command in onEnable
Tell Paper "the word
hellobelongs to this class". Until you register it, typing/hellogives 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.
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
- java/com/example/commandsbasichello/Package com.example.commandsbasichello
- 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.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}- Paper keeps
BasicCommandin itsbrigadierpackage, because under the hood Paper turns every basic command into a Brigadier command. You do not need to know Brigadier to use it. - This class promises to be a command. Paper will only accept objects of classes that implement
BasicCommand. - An
annotation that says "this method fills in a method from the interface". If you misspellexecute, the compiler sees@Overrideand stops with a clear error instead of quietly treating it as a different method. - Paper calls this method each time the command runs. It gets two things:
source(who ran it and where) andargs(the words typed after the command name). This first command ignoresargs. - 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. - 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. - 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:
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
- java/com/example/commandsbasichello/Package com.example.commandsbasichello
- 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.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}- Paper calls
onEnableonce when your plugin starts. Commands are registered here, just like listeners. See the plugin lifecycle for the full story. - 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. - The aliases. Now
/hidoes the same as/hello. If you want no aliases, useList.of(), or call the shorterregisterCommand("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:
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
- java/com/example/commandsbasichello/Package com.example.commandsbasichello
- 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: HelloCommands2version: '1.0.0'3main: com.example.commandsbasichello.HelloPlugin4api-version: '26.3'5description: Adds a /hello command that greets whoever runs it.- 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:
Now type hello in the server console window. The console is a sender too, and its name is 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:
| Method | What 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:
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
- java/com/example/commandsbasic/Package com.example.commandsbasic
- 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.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}- May be
null.Entityis the type for everything that moves in the world: players, mobs, arrows, minecarts. - Two placeholders in one message: the name and the kind of sender.
- Always check for
nullbefore using the executor, or the command crashes when the console runs it. - A small helper
method that turns a sender into words. Keeping it separate keepsexecuteshort and readable. - The
instanceofoperator asks "is this sender a player?" and answerstrueorfalse. 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):
Executor: Steve
execute as Steve run whoami[14:06:41 INFO]: Sender: CONSOLE (the server console)[14:06:41 INFO]: Executor: SteveA 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:
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
- java/com/example/commandsbasic/Package com.example.commandsbasic
- 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.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}- This does two things at once. It checks "is the executor a player?", and if so, it creates a variable
playerof typePlayerthat you can use below. This is calledpattern matching . - The
!flips the answer: "if the executor is not a player". Mind the brackets: the!must apply to the wholeinstanceofcheck. - The console gets a clear reason instead of a crash. Then
returnends the method right away, aguard clause . - Reads whether the player may fly now, and flips it. If flying was allowed,
flightOnbecomesfalse, and the other way round. That is the whole toggle. - Lets the player fly by double-tapping jump, like in creative mode. Turning it off also brings a flying player back down.
- An optional method from
BasicCommand. Return apermission 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:
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
- java/com/example/commandsbasic/Package com.example.commandsbasic
- 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: 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- The permission's name, starting with the plugin's name so it never clashes with another plugin.
- 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 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:
fly<--[HERE]
And when the console types fly, the guard clause answers:
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:
This small Java program does the same splitting, so you can watch it happen. Press Run, then try your own commands with Edit:
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}- Removes the first character, the slash.
- Cuts the text at every space, giving an array of words.
- Copies every word except the first one. Those are the arguments.
- How many arguments there are. It is
0when the player typed only the command name. - Arrays count from 0, so the first argument is
args[0]and the second isargs[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 Alexstill givesargs[0]=Alex. - Everything is text. Even when a player types
5, you get theString"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]whenargs.lengthis0crashes your command.
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:
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
- java/com/example/commandsbasic/Package com.example.commandsbasic
- 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.
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}- Checks the length before reading anything. No arguments means "just greet me".
- Stop here. Without this
return, the code would carry on and readargs[0], which does not exist. - Looks up an online player by name, ignoring upper and lower case. If nobody with that exact name is online, you get
null. - Players make typos. Tell them which name was not found, so they can fix it.
- One message to the other player, then one confirmation to the sender. Always tell the sender the command worked.
No online player is called Alxe.
And Alex, on the other side, sees:
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:
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}- Five things a player might type. Watch the last one:
99999999999is a whole number, but it is too big for anint(the limit is about 2.1 billion), so it fails too. - Run the code inside, but be ready for it to fail.
- Turns text into an
int. Works for"5"and"-3"; throws for anything else. - If
parseIntfailed, Java jumps here instead of crashing. This is where you explain the problem. - 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:
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
- java/com/example/commandsbasic/Package com.example.commandsbasic
- 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.
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}- Only players have levels. The pattern variable is called
payerhere, a name that says what it is. - This command needs exactly two arguments. Anything else gets the usage line.
- A usage line shows how to type the command.
<player>in angle brackets means "required"; that is the common style. It usesComponent.textinstead of MiniMessage so the angle brackets are not mistaken for tags. See Text and colors. - Players will try to pay themselves to see what happens. Catch it.
- Declared before the
try, so it still exists after thetryblock ends. - The second argument is the amount, as text. This turns it into a number or jumps to
catch. - The last check: does the payer have enough? Only after every check passes does anything change.
- The actual work, two lines: take from one player, give to the other.
- 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:
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 typed | args in suggest | So suggest |
|---|---|---|
/paylevels | empty, length 0 | player names |
/paylevels Al | ["Al"], length 1 | player names starting with "Al" |
/paylevels Alex | ["Alex", ""], length 2 | amounts |
/paylevels Alex 1 | ["Alex", "1"], length 2 | amounts |
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
- java/com/example/commandsbasic/Package com.example.commandsbasic
- 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.
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}- The method returns a collection of words. Any list works, for example
List.of("1", "5", "10"). - Length 0 or 1 means the player is on the first argument, the name.
- What they have typed of the name so far, or nothing yet.
- A helper method, shared with
/hello, that lists online players whose names start with what was typed. - Second argument: a few handy amounts. Players can still type any number.
- 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:
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
- java/com/example/commandsbasic/Package com.example.commandsbasic
- 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.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}- A helper class with no
public: only classes in the samepackage can use it, which is all this plugin needs. - A private
constructor means nobody can create aPlayerNamesobject. The class only holds a helper method. - Compares in lower case, so typing
alstill findsAlex.Locale.ROOTmakes lower-casing behave the same on every computer. - 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:
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
- java/com/example/commandsbasic/Package com.example.commandsbasic
- 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.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}- 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".
- 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.
- A
lambda : a small piece of code you hand to Paper to run later.eventis the name you give its parameter. See Lambdas. - The registrar is the object that accepts commands. You call
registeron it once per command. - Name, description and the command object. Without a list of aliases, the command has only its main name.
- 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
- com/example/commandsbasic/
- resources/
- plugin.ymlName, main class and the commandsbasic.fly permission
- java/
- main/
- src/
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
onEnablefirst. - Reading
args[0]without checkingargs.length. The command crashes the moment someone types it with no arguments. - Casting the sender with
(Player) senderwithout a check. It crashes when the console runs the command. Useinstanceof Player player. - Calling
Integer.parseIntwithouttry/catch. The first player who types a word instead of a number causes an error. - Silent failures. Every
returnbefore the real work should send a message that says what was wrong and how to fix it. - Adding a
commands:section toplugin.ymlas well. Commands registered in code do not need it. Mixing both ways for the same command name confuses everyone, including you.
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.
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
- java/com/example/commandsbasicexercise/Package com.example.commandsbasicexercise
- 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.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}- The default, used when the player typed no argument.
- Only read
args[0]when it exists. - A one-sided die makes no sense, and a huge one is just silly.
- A random whole number from 1 up to and including
sides.
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
- java/com/example/commandsbasicexercise/Package com.example.commandsbasicexercise
- 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.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}A die needs between 2 and 100 sides.
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:
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
/paylevelsis just text. With Brigadier, the game colors wrong arguments red while the player types, and even understands selectors like@aand@p. - Subcommands get messy.
/team create,/team join,/team leaveturn into a bigif/elseonargs[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
BasicCommandhas anexecute(source, args)method, and optionallysuggestandpermission. - Register commands in
onEnable, withregisterCommand(...)or with theLifecycleEvents.COMMANDSevent. Noplugin.ymlentry is needed. source.getSender()is who to reply to;source.getExecutor()is who to act on. Checkinstanceof Player playerbefore doing player things.argsis an array of text that starts at index 0. Checkargs.lengthbefore reading, and parse numbers withInteger.parseIntinsidetry/catch.- Check every way the input can be wrong, send a helpful message for each, and do the real work last.
Quick quiz
A player types
/paylevels Alex 5. What isargs[1]?Arguments always arrive as text, and counting starts at 0:args[0]is"Alex"andargs[1]is"5". To get the number, useInteger.parseInt(args[1]).The console runs
fly. What happens with theFlyCommandfrom this page?The console has no executor entity, sogetExecutor()isnull, andnull instanceof Playerisfalse. The guard clause sends a message and returns. A crash would only happen with an unchecked cast like(Player) sender.What happens when
Integer.parseInt("abc")runs outside atryblock?parseIntcannot return a number for text that is not a number, so it throws an exception. Catch it withtry/catchand send the player a friendly message instead.An admin types
execute as Steve run whoamiin the console. Who is the sender and who is the executor?The sender is whoever started the command, here the console./execute as Stevechanges the executor to Steve. Reply to the sender, act on the executor.Your
BasicCommandreturns"myplugin.fly"frompermission(). What does a player without that permission experience?Paper checks the permission before the player can see or run the command, so yourexecutemethod never runs for them.
Next steps
- Commands, part 2: Brigadier command trees: typed arguments, selectors and subcommands.
- Commands, part 3: the classic plugin.yml way: read older tutorials with confidence.
- Permissions: decide who may use which command.
- Cooldowns, toggles and player state: stop players from spamming your commands.