Project: /heal and /feed with cooldowns
Two Brigadier commands with permissions, targets, cooldowns and configurable messages.
In this project you build two commands, /heal and /feed, that work on yourself or on another player, are protected by permissions, make players wait between uses, and take every message from a config file.
What you will build
Almost every server has commands like these, and they are the perfect practice project: each one is small, but together they use the things that real command plugins need. By the end you will have written a command tree with an optional player argument, permission checks, a cooldown, and messages the server owner can edit.
Here is what the finished plugin does. Steve types /heal and is healed:
Steve, who is an admin, can also heal Alex by naming him. Steve sees one message and Alex sees another:
If Steve tries again too soon, the command refuses and says how long to wait:
These are the two commands and the rules behind them:
| Command | What it does | Permission for yourself | Permission for others |
|---|---|---|---|
/heal [player] | Sets health to the maximum and puts out fire | healfeed.heal | healfeed.heal.others |
/feed [player] | Fills the hunger bar and the saturation | healfeed.feed | healfeed.feed.others |
In the usage line, square brackets such as [player] mean "optional". A player with healfeed.bypass-cooldown never has to wait. By default only server operators have these permissions, and a permissions plugin such as LuckPerms can hand them to anyone else.
The plan
The plan matters more here than in the last project, because a command does several checks before it acts, and the order of the checks decides how the plugin feels to use. Here is the path a command takes:
And here is how each requirement maps to a piece of code, with the chapter that teaches it:
| Requirement | The piece that does it | Chapter |
|---|---|---|
| Commands with an optional player | A Brigadier command tree with an argument node | Commands, part 2 |
| Who may use what | Permissions in plugin.yml and requires(...) | Permissions |
| Waiting between uses | A Cooldowns class that stores a ready time per player UUID | Cooldowns, toggles and player state |
| Editable messages | config.yml and MiniMessage placeholders | Configuration files |
| One design for both commands | An enum that describes what is different | Enums, records and modern Java |
The build steps are:
Create the project and plugin.yml permissions
Declare who may do what before writing any code.
Write /heal for yourself
The smallest working command, and the one new idea: how a Brigadier command is registered.
Add the optional player target
An argument node, a second permission and the first real branch.
Share one design between /heal and /feed
An enum describes the difference, so we do not copy and paste a whole class.
Add the cooldown
A map from UUID to the time when the player may use the command again.
Put the messages in config.yml
A small helper that sends a configured message with placeholders.
Register both commands and test
Wire everything together and run a checklist with two players.
If you have not read Commands, part 2: Brigadier command trees yet, do that first. This page uses its words (literal, argument, node) without explaining them again from zero. If you are new to configs, the Welcome kit project walks through config.yml more slowly.
Step 1: create the project and the permissions
Create a project the same way as in the previous project (copy the starter or run new-plugin.cmd -Name HealFeed from the paper-templates folder) and use the package com.example.projecthealfeed. Then open plugin.yml. Besides the usual name and main class, we declare every permission the plugin will check:
Where this file livesproject-heal-feedsrcmainresourcesplugin.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.
- project-heal-feed/
- src/main/
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- CareCommand.javaCommand (Brigadier tree)
- CareType.javaEnum: a fixed list of choices
- Cooldowns.javaHelper class
- HealFeedPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- Messages.javaHelper class
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlyou are hereTells Paper the plugin's name, version and main class
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- 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: HealFeed2version: '1.0.0'3main: com.example.projecthealfeed.HealFeedPlugin4api-version: '26.3'5description: The /heal and /feed commands with permissions, targets, cooldowns and configurable messages.6permissions:7 healfeed.heal:8 description: Use /heal on yourself.9 default: op10 healfeed.heal.others:11 description: Use /heal on another player.12 default: op13 healfeed.feed:14 description: Use /feed on yourself.15 default: op16 healfeed.feed.others:17 description: Use /feed on another player.18 default: op19 healfeed.bypass-cooldown:20 description: Use the commands without waiting for the cooldown.21 default: op22 healfeed.*:23 description: Every HealFeed permission.24 default: op25 children:26 healfeed.heal: true27 healfeed.heal.others: true28 healfeed.feed: true29 healfeed.feed.others: true30 healfeed.bypass-cooldown: true- The section that lists every permission this plugin uses. Declaring them makes them show up in permission plugins and gives them a sensible default.
- A
permission is just a name. Starting every name with the plugin's name keeps it from clashing with other plugins. - The dot is only part of the name; Paper does not treat it as anything special. We use it by convention, so "heal others" reads like a sub-permission of "heal".
- Who has the permission when nobody has granted it.
opmeans server operators only. Other values aretrue(everyone),false(nobody) andnot op. - A permission that does not belong to a command: it switches the cooldown off for whoever has it.
- A convenience for admins: one permission that contains all the others through
children. In a permissions plugin, grantinghealfeed.*means granting them all.
Step 2: /heal for yourself
Start with the smallest version that works: /heal heals the player who typed it. Here is the command class. It does not do anything by itself; it builds a description of the command, a command tree, that Paper uses later.
Where this file livesproject-heal-feed-step1srcmainjavacomexampleprojecthealfeedstep1HealCommand.java
The package com.example.projecthealfeedstep1 is the folder path com/example/projecthealfeedstep1 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.
- project-heal-feed-step1/
- src/main/
- java/com/example/projecthealfeedstep1/Package com.example.projecthealfeedstep1
- HealCommand.javayou are hereCommand (Brigadier tree)
- HealFeedPlugin.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/projecthealfeedstep1/Package com.example.projecthealfeedstep1
- 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.projecthealfeedstep1;2 3import com.mojang.brigadier.Command;4import com.mojang.brigadier.context.CommandContext;5import com.mojang.brigadier.tree.LiteralCommandNode;6import io.papermc.paper.command.brigadier.CommandSourceStack;7import io.papermc.paper.command.brigadier.Commands;8import org.bukkit.attribute.Attribute;9import org.bukkit.attribute.AttributeInstance;10import org.bukkit.command.CommandSender;11import org.bukkit.entity.Player;12 13final class HealCommand {14 15 private HealCommand() {16 }17 18 static LiteralCommandNode<CommandSourceStack> create() {19 return Commands.literal("heal")20 .requires(source -> source.getSender().hasPermission("healfeed.heal"))21 .executes(HealCommand::healSender)22 .build();23 }24 25 private static int healSender(CommandContext<CommandSourceStack> context) {26 CommandSender sender = context.getSource().getSender();27 if (!(sender instanceof Player player)) {28 sender.sendRichMessage("<red>Only players can heal themselves.");29 return 0;30 }31 32 AttributeInstance maxHealth = player.getAttribute(Attribute.MAX_HEALTH);33 player.setHealth(maxHealth == null ? 20.0 : maxHealth.getValue());34 player.setFireTicks(0);35 player.sendRichMessage("<green>You feel much better.");36 return Command.SINGLE_SUCCESS;37 }38}- A
constructor is the special method that runs when you writenew. Making itprivatemeans nobody can create aHealCommandobject, so the class is only a home for static methods. - The first word of the command: players type
/heal. Aliteral node is a fixed word. - The gatekeeper. Players for whom this returns
falsedo not see the command in tab completion, and typing it gives the usual "Unknown or incomplete command" error. The part with the arrow is alambda : a tiny function that gets the commandsourceand answers yes or no. - What to run when the command is typed.
HealCommand::healSenderis a method reference: "run that method". - Finishes the tree and returns its root node, ready to hand to Paper.
- The sender is whoever typed the command. That can be a player, but also the console or a command block.
- Asks "is the sender a player?" and, when it is, gives the same object a second name,
player, typed as aPlayer. The!flips the question: this branch handles senders that are not players, such as the console, which has no health to restore. - A player's maximum health is an attribute, because potions and gear can raise or lower it. It is normally 20.0, which is ten hearts. The method can return
null, so the next line checks. - Sets the health. Giving it more than the maximum would throw an error, which is why we ask for the maximum first.
- Puts out fire. Burning players would otherwise take damage right after being healed.
- A command method returns a number.
1(this constant) means "it worked". Returning0means "it did nothing", which matters for command blocks and/execute.
Now register it. Brigadier commands are registered while the server is still starting, through a lifecycle event called COMMANDS. Paper fires it at the right moment and gives you a registrar to hand your command tree to:
Where this file livesproject-heal-feed-step1srcmainjavacomexampleprojecthealfeedstep1HealFeedPlugin.java
The package com.example.projecthealfeedstep1 is the folder path com/example/projecthealfeedstep1 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.
- project-heal-feed-step1/
- src/main/
- java/com/example/projecthealfeedstep1/Package com.example.projecthealfeedstep1
- HealCommand.javaCommand (Brigadier tree)
- HealFeedPlugin.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/projecthealfeedstep1/Package com.example.projecthealfeedstep1
- 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.projecthealfeedstep1;2 3import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;4import org.bukkit.plugin.java.JavaPlugin;5 6public final class HealFeedPlugin extends JavaPlugin {7 8 @Override9 public void onEnable() {10 getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event ->11 event.registrar().register(HealCommand.create(), "Restore your health"));12 }13}- Asks Paper: "when the
COMMANDSevent happens, run this code". - Hands the finished tree to Paper, with a short description that shows up in help listings.
Build, start the server, and make yourself an operator by typing op YourName in the server console window. Without that, you do not have the permission, and the command does not even exist for you. Hurt yourself with /damage @s 8 (a vanilla command), then type /heal:
Step 3: add the optional player target
Now the interesting part: /heal Alex. In a Brigadier tree this is a second node hanging under heal: an argument node that reads a player. Because executes is also on the heal node itself, both /heal and /heal Alex are valid. That is how Brigadier says "optional".
From here on we work with the finished project. Its command class is called CareCommand (step 4 explains the name) and it replaces HealCommand, so you can delete the old file. The code below says type.commandName() where step 2 had the fixed word "heal"; for now, simply read it as "heal". First, the top of the class, where it keeps what it needs:
Where this file livesproject-heal-feedsrcmainjavacomexampleprojecthealfeedCareCommand.java
The package com.example.projecthealfeed is the folder path com/example/projecthealfeed 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.
- project-heal-feed/
- src/main/
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- CareCommand.javayou are hereCommand (Brigadier tree)
- CareType.javaEnum: a fixed list of choices
- Cooldowns.javaHelper class
- HealFeedPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- Messages.javaHelper class
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- 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.
16final class CareCommand {17 18 private static final String BYPASS_PERMISSION = "healfeed.bypass-cooldown";19 20 private final CareType type;21 private final Messages messages;22 private final Duration cooldownLength;23 private final Cooldowns cooldowns = new Cooldowns();24 25 CareCommand(CareType type, Messages messages, Duration cooldownLength) {26 this.type = type;27 this.messages = messages;28 this.cooldownLength = cooldownLength;29 }- The name of the permission that switches the cooldown off. A
static finaltext is a constant: written once, never changed, and the name in capitals says so. - A
field : a variable that belongs to the object and stays available to every method in the class. This one says which command this object is: heal or feed (the next step createsCareType). - The helper that sends configured messages, written in step 6.
- How long a player must wait between uses.
Durationis Java's type for a length of time. - The notebook of who may use the command again and when, written in step 5. Every
CareCommandobject makes its own. - The constructor: whoever writes
new CareCommand(...)must hand over these three values, and the lines inside copy them into the fields.
Now the create() method, which has grown. Read it line by line:
Where this file livesproject-heal-feedsrcmainjavacomexampleprojecthealfeedCareCommand.java
The package com.example.projecthealfeed is the folder path com/example/projecthealfeed 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.
- project-heal-feed/
- src/main/
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- CareCommand.javayou are hereCommand (Brigadier tree)
- CareType.javaEnum: a fixed list of choices
- Cooldowns.javaHelper class
- HealFeedPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- Messages.javaHelper class
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- 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.
31LiteralCommandNode<CommandSourceStack> create() {32 String permission = "healfeed." + type.commandName();33 return Commands.literal(type.commandName())34 .requires(source -> source.getSender().hasPermission(permission))35 .executes(this::runOnSelf)36 .then(Commands.argument("target", ArgumentTypes.player())37 .requires(source -> source.getSender().hasPermission(permission + ".others"))38 .executes(this::runOnTarget))39 .build();40}- Builds the permission name from the command name:
healfeed.healorhealfeed.feed. The same code now serves both commands. - The real permission check. The sender needs
healfeed.healto use the command at all. - Runs when the player typed only
/heal. - Hangs a child node under the command, for the word after
/heal. - The argument's name. We use it later to get the player back out.
- The type of the argument: exactly one player. It gives players tab completion for online names and accepts selectors like
@sand@r. - This node needs a different, stronger permission. A player without it can still use plain
/healbut cannot name a target, and tab completion does not even offer player names. - Runs when a target was named.
Reading the player out of the command takes two small steps: ask for the argument by name and turn the answer into real players. Why two steps? A selector like @r ("a random player") has to be worked out at the moment the command runs, not when it was parsed. The thing you get back is called a resolver, and resolve does the working out:
Where this file livesproject-heal-feedsrcmainjavacomexampleprojecthealfeedCareCommand.java
The package com.example.projecthealfeed is the folder path com/example/projecthealfeed 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.
- project-heal-feed/
- src/main/
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- CareCommand.javayou are hereCommand (Brigadier tree)
- CareType.javaEnum: a fixed list of choices
- Cooldowns.javaHelper class
- HealFeedPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- Messages.javaHelper class
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
51private int runOnTarget(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {52 CommandSender sender = context.getSource().getSender();53 PlayerSelectorArgumentResolver resolver = context.getArgument("target", PlayerSelectorArgumentResolver.class);54 Player target = resolver.resolve(context.getSource()).getFirst();55 return care(sender, target);56}- Resolving can fail, for example when the named player is not online. The method declares that it may throw this error, and Brigadier catches it and shows the player the usual red message. That saves us writing "player not found" code.
- Gets the argument by the name we gave it. The second part is the type we expect back.
- Works out which players the argument stands for. The answer is a list, even though this argument type only ever allows one player.
- Takes the first (and only) player from that list.
And runOnSelf, the version without a target, needs one more decision: the console has no health, so it must name somebody.
Where this file livesproject-heal-feedsrcmainjavacomexampleprojecthealfeedCareCommand.java
The package com.example.projecthealfeed is the folder path com/example/projecthealfeed 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.
- project-heal-feed/
- src/main/
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- CareCommand.javayou are hereCommand (Brigadier tree)
- CareType.javaEnum: a fixed list of choices
- Cooldowns.javaHelper class
- HealFeedPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- Messages.javaHelper class
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- 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.
42private int runOnSelf(CommandContext<CommandSourceStack> context) {43 CommandSender sender = context.getSource().getSender();44 if (sender instanceof Player player) {45 return care(player, player);46 }47 messages.send(sender, "console-needs-target", Placeholder.unparsed("command", type.commandName()));48 return 0;49}- If a player typed the command, the target is that same player.
- Sender and target are the same object. All the real work lives in
care, which we write below. - The console gets a message that explains how to use the command correctly. We add this text to the config in step 6.
Step 4: share one design between /heal and /feed
You could now copy the whole HealCommand class, rename it to FeedCommand and change two lines. It works, and it is how most beginners start. The problem arrives later: when you fix a bug or add a cooldown, you have to fix it in both copies, and one of them will be forgotten.
Look at what is really different between the two commands: the name, the permission, the description, and what happens to the player. Everything else is identical. A Java enum is a good fit: a fixed list of named choices that can carry their own details. Here are our two choices:
Where this file livesproject-heal-feedsrcmainjavacomexampleprojecthealfeedCareType.java
The package com.example.projecthealfeed is the folder path com/example/projecthealfeed 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.
- project-heal-feed/
- src/main/
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- CareCommand.javaCommand (Brigadier tree)
- CareType.javayou are hereEnum: a fixed list of choices
- Cooldowns.javaHelper class
- HealFeedPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- Messages.javaHelper class
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- 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.projecthealfeed;2 3import java.util.Locale;4import org.bukkit.attribute.Attribute;5import org.bukkit.attribute.AttributeInstance;6import org.bukkit.entity.Player;7 8enum CareType {9 10 HEAL("Restore your health, or the health of another player"),11 FEED("Refill your hunger bar, or the hunger bar of another player");12 13 private final String description;14 15 CareType(String description) {16 this.description = description;17 }18 19 String description() {20 return description;21 }22 23 String commandName() {24 return name().toLowerCase(Locale.ROOT);25 }26 27 void apply(Player player) {28 switch (this) {29 case HEAL -> {30 AttributeInstance maxHealth = player.getAttribute(Attribute.MAX_HEALTH);31 player.setHealth(maxHealth == null ? 20.0 : maxHealth.getValue());32 player.setFireTicks(0);33 }34 case FEED -> {35 player.setFoodLevel(20);36 player.setSaturation(20.0f);37 }38 }39 }40}- An enum is a type with a fixed set of values. Here there are exactly two:
HEALandFEED. We will add a third in the exercise. - Each value is created with its own description text, which Paper shows in
/help. - Each value keeps its own copy of the description.
- Every enum value knows its own name as text.
HEALbecomesheal, and that word is reused for the command, the permission and the config keys.Locale.ROOTmakes the lowercasing behave the same on every computer, including ones set to Turkish. - Chooses what to do depending on which value we are.
thisis the value the method was called on:HEAL.apply(player)runs the first branch. - The hunger bar goes from 0 to 20 (ten food icons). 20 means full.
- Saturation is the hidden buffer that keeps your hunger bar from dropping. Setting it to 20 means the player will not get hungry again for a good while. The
fafter the number marks it as afloat.
Now there is one command class, CareCommand, and the plugin creates one object of it for each value of the enum. Back in step 3 you saw that the class reads type.commandName() instead of the fixed word "heal". That is the whole trick.
Step 5: add the cooldown
A cooldown is a wait between uses. The idea is simple: when a player uses the command, we write down when they may use it again. Next time, we compare that time with the clock. The notebook where we write the times is a Map, a collection that stores a value under a key. The key is the player's UUID, a unique id that never changes, even if the player changes their name:
Where this file livesproject-heal-feedsrcmainjavacomexampleprojecthealfeedCooldowns.java
The package com.example.projecthealfeed is the folder path com/example/projecthealfeed 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.
- project-heal-feed/
- src/main/
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- CareCommand.javaCommand (Brigadier tree)
- CareType.javaEnum: a fixed list of choices
- Cooldowns.javayou are hereHelper class
- HealFeedPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- Messages.javaHelper class
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- 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.projecthealfeed;2 3import java.time.Duration;4import java.util.HashMap;5import java.util.Map;6import java.util.UUID;7 8final class Cooldowns {9 10 private final Map<UUID, Long> readyAt = new HashMap<>();11 12 long remainingMillis(UUID id) {13 Long ready = readyAt.get(id);14 if (ready == null) {15 return 0L;16 }17 long remaining = ready - System.currentTimeMillis();18 if (remaining <= 0L) {19 readyAt.remove(id);20 return 0L;21 }22 return remaining;23 }24 25 void start(UUID id, Duration length) {26 readyAt.put(id, System.currentTimeMillis() + length.toMillis());27 }28}- The notebook. Each UUID maps to the moment, in milliseconds on the system clock, when that player may use the command again.
- Looks up the player. The type is
Longwith a capital L, because a map can hold "nothing" and thengetreturnsnull . The plainlongcannot be null. - The player has no entry, so there is nothing to wait for.
- Ready time minus now equals how long is left. A negative number means the time has passed.
- Tidies up: once a cooldown has run out, forget it.
- Writes the entry: now plus the cooldown length.
Durationis Java's type for lengths of time, which is clearer than a bare number of milliseconds.
One Cooldowns object belongs to one command (see the field in CareCommand), so waiting for /heal does not stop you from using /feed. Using it is the first thing the care method does:
Where this file livesproject-heal-feedsrcmainjavacomexampleprojecthealfeedCareCommand.java
The package com.example.projecthealfeed is the folder path com/example/projecthealfeed 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.
- project-heal-feed/
- src/main/
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- CareCommand.javayou are hereCommand (Brigadier tree)
- CareType.javaEnum: a fixed list of choices
- Cooldowns.javaHelper class
- HealFeedPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- Messages.javaHelper class
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- 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.
58private int care(CommandSender sender, Player target) {59 if (sender instanceof Player player && !player.hasPermission(BYPASS_PERMISSION)) {60 long remaining = cooldowns.remainingMillis(player.getUniqueId());61 if (remaining > 0L) {62 messages.send(sender, "cooldown",63 Placeholder.unparsed("command", type.commandName()),64 Placeholder.unparsed("time", formatSeconds(remaining)));65 return 0;66 }67 cooldowns.start(player.getUniqueId(), cooldownLength);68 }69 70 type.apply(target);71 72 String name = type.commandName();73 if (sender.equals(target)) {74 messages.send(sender, name + ".self");75 } else {76 messages.send(sender, name + ".other", Placeholder.unparsed("target", target.getName()));77 messages.send(target, name + ".received", Placeholder.unparsed("sender", sender.getName()));78 }79 return Command.SINGLE_SUCCESS;80}- The cooldown only applies to players, and only to players who do not have the bypass permission. The console and admins with the bypass never wait.
- How long this player still has to wait. The cooldown belongs to the person who typed the command, not to the target: a player cannot heal three friends in a row to dodge the wait.
- Still waiting: tell them, and stop with
return 0before anything is healed. - Allowed to proceed, so start the cooldown for next time.
- The one line that actually heals or feeds. Everything above it was checking.
- Two cases need two sets of messages: healing yourself, or healing someone else. When it is someone else, the target is told who helped them.
- Builds the config path, for example
heal.received. One line serves both commands.
Step 6: put the messages in config.yml
All the texts are in the config, like in the Welcome kit project. Each command has its own group, with one text for the sender, one for the sender when a target was named, and one for the target. A few shared texts sit beside them:
Where this file livesproject-heal-feedsrcmainresourcesconfig.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.
- project-heal-feed/
- src/main/
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- CareCommand.javaCommand (Brigadier tree)
- CareType.javaEnum: a fixed list of choices
- Cooldowns.javaHelper class
- HealFeedPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- Messages.javaHelper class
- resources/Files copied into the jar as they are
- config.ymlyou are hereDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- 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.
1cooldowns:2 heal-seconds: 603 feed-seconds: 304 5messages:6 heal:7 self: "<green>You feel much better."8 other: "<green>You healed <yellow><target></yellow>."9 received: "<green><yellow><sender></yellow> healed you."10 feed:11 self: "<green>Your hunger is gone."12 other: "<green>You fed <yellow><target></yellow>."13 received: "<green><yellow><sender></yellow> fed you."14 cooldown: "<red>You can use <yellow>/<command></yellow> again in <yellow><time></yellow>."15 console-needs-target: "<red>The console must name a player, like <yellow>/<command> Steve</yellow>."- How long each command makes the sender wait. The name
heal-secondsis built by the plugin from the command name, so a command calledfeedreadsfeed-seconds. - A whole number of seconds. Setting it to
0means no cooldown. - Shown to the sender when they healed someone else.
<target>is a placeholder for that player's name. - Shown to the player who was healed, with
<sender>replaced by the name of whoever healed them. - Shown when the command is still on cooldown. It has two placeholders: the command and the time left.
Sending a configured message is something every command will do, so it gets a tiny helper class. It looks a text up by its path, skips it when the owner left it empty (a simple way to turn a message off), and sends it with the placeholders:
Where this file livesproject-heal-feedsrcmainjavacomexampleprojecthealfeedMessages.java
The package com.example.projecthealfeed is the folder path com/example/projecthealfeed 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.
- project-heal-feed/
- src/main/
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- CareCommand.javaCommand (Brigadier tree)
- CareType.javaEnum: a fixed list of choices
- Cooldowns.javaHelper class
- HealFeedPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- Messages.javayou are hereHelper class
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- 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.projecthealfeed;2 3import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;4import org.bukkit.command.CommandSender;5import org.bukkit.configuration.file.FileConfiguration;6 7final class Messages {8 9 private final FileConfiguration config;10 11 Messages(FileConfiguration config) {12 this.config = config;13 }14 15 void send(CommandSender receiver, String path, TagResolver... placeholders) {16 String text = config.getString("messages." + path);17 if (text == null || text.isEmpty()) {18 return;19 }20 receiver.sendRichMessage(text, placeholders);21 }22}- The three dots mean "any number of these, including none". Calls like
messages.send(sender, "heal.self")pass none; others pass one or two. - Joins the section name and the path:
messagesplusheal.selfbecomesmessages.heal.self. - A missing or empty text means "send nothing". A server owner can silence a message by writing
"". - Sends the MiniMessage text to the receiver, which can be a player or the console, filling in every placeholder.
Step 7: register both commands and test
The last piece is the main class. It loads the config, builds one CareCommand for each value of the enum, and registers the command trees:
Where this file livesproject-heal-feedsrcmainjavacomexampleprojecthealfeedHealFeedPlugin.java
The package com.example.projecthealfeed is the folder path com/example/projecthealfeed 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.
- project-heal-feed/
- src/main/
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- CareCommand.javaCommand (Brigadier tree)
- CareType.javaEnum: a fixed list of choices
- Cooldowns.javaHelper class
- HealFeedPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- Messages.javaHelper class
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projecthealfeed/Package com.example.projecthealfeed
- 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.projecthealfeed;2 3import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;4import java.time.Duration;5import org.bukkit.plugin.java.JavaPlugin;6 7public final class HealFeedPlugin extends JavaPlugin {8 9 @Override10 public void onEnable() {11 saveDefaultConfig();12 Messages messages = new Messages(getConfig());13 14 getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> {15 for (CareType type : CareType.values()) {16 long seconds = getConfig().getLong("cooldowns." + type.commandName() + "-seconds", 0L);17 CareCommand command = new CareCommand(type, messages, Duration.ofSeconds(seconds));18 event.registrar().register(command.create(), type.description());19 }20 });21 }22}- Creates
plugins/HealFeed/config.ymlon the first start. - One
Messageshelper, shared by both commands. - Gives you every value of the enum, here
HEALand thenFEED. The loop below runs once for each. - Reads
cooldowns.heal-seconds, thencooldowns.feed-seconds. The second argument is the default if the setting is missing. - Each command gets its own object, so each has its own
Cooldownsnotebook. - Builds the tree and registers it with the description.
- project-heal-feed/
- src/
- main/
- java/
- com/example/projecthealfeed/
- HealFeedPlugin.javaMain class: loads the config and registers one command per CareType
- CareType.javaThe enum: HEAL and FEED, with names, descriptions and what each one does
- CareCommand.javaThe command tree, the permission checks, the cooldown check and the messages
- Cooldowns.javaUUID to ready-time notebook
- Messages.javaSends a configured MiniMessage text with placeholders
- com/example/projecthealfeed/
- resources/
- plugin.ymlName, main class and all permissions
- config.ymlCooldown lengths and every message
- java/
- main/
- src/
You can download the finished project and compare it with yours.
Testing checklist
Test with two players if you can: your own account and a friend, or a second account on the same PC. The cooldown and the "others" messages are the parts a single player cannot see. Go through these in order:
- As an operator, run
/damage @s 8, then/heal. You are healed, fire is gone, and the green message appears. - Run
/effect give @s hunger 20 10and wait until the food bar is low, then/feed. The bar fills. - Run
/healtwice quickly. The second try shows the red cooldown message with the seconds left, and counts down as you retry. - Run
/feedright after/heal: it works, because each command has its own cooldown. - Type
/heal Alex(use your friend's name). You see "You healed Alex", and your friend sees "Steve healed you". - Type a name that is not online. Brigadier shows its own red error and nothing is healed.
- In the server console, run
heal: it prints the "must name a player" message. Runheal Alex: it works with no cooldown. - Take away operator status with
deop Steve: the commands disappear from tab completion, and typing them gives "Unknown or incomplete command". Give back onlyhealfeed.healwith a permissions plugin:/healworks, but/heal Alexdoes not. - Give yourself
healfeed.bypass-cooldown: there is no waiting any more.
Mistakes to avoid
- Healing above the maximum.
setHealth(30)on a 20-health player throws an exception. Always read the maximum health first, asCareTypedoes. - Forgetting
saveDefaultConfig(). Without it there is noconfig.yml, every message is missing and the commands feel silent. - Checking the permission in
requiresbut typing it differently inplugin.yml.healfeed.healandhealfeed.Healare two different permissions. - Putting the cooldown on the target. Then anyone could block a friend's cooldown, or dodge their own by healing a second account. The cooldown belongs to the person who typed the command.
- Storing the
Playerobject instead of the UUID. APlayerobject is only valid while the player is online. Maps keyed byPlayerleak memory and break after a relog. Use the UUID. - Typing message texts into the code. Hard-coded messages cannot be translated or edited by the server owner. Every text a player can read belongs in the config.
Exercises
Add a /cleanse command
Add a third command, /cleanse, that removes every potion effect from you, or from another player. It needs its own permissions, cooldown and messages. How much code do you have to change?
Hint 1
Almost none of the command code. Open CareType and add a third value. The compiler will not force you to handle it, so find the switch and add a branch.
Hint 2
A player's potion effects come from player.getActivePotionEffects(). Remove each one with player.removePotionEffect(effect.getType()) inside a loop.
Hint 3
Do not forget the three files besides the Java code: plugin.yml needs healfeed.cleanse and healfeed.cleanse.others, and config.yml needs a cleanse-seconds cooldown and a cleanse message group.
Show the solution
The whole new feature is one enum value and one switch branch, because the enum design from step 4 does the rest: the command name, the permission names and the config paths are all built from the value's name.
Where this file livesproject-heal-feed-exercisesrcmainjavacomexampleprojecthealfeedexerciseCareType.java
The package com.example.projecthealfeedexercise is the folder path com/example/projecthealfeedexercise 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.
- project-heal-feed-exercise/
- src/main/
- java/com/example/projecthealfeedexercise/Package com.example.projecthealfeedexercise
- CareCommand.javaCommand (Brigadier tree)
- CareType.javayou are hereEnum: a fixed list of choices
- Cooldowns.javaHelper class
- HealFeedPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- Messages.javaHelper class
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projecthealfeedexercise/Package com.example.projecthealfeedexercise
- 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.projecthealfeedexercise;2 3import java.util.Locale;4import org.bukkit.attribute.Attribute;5import org.bukkit.attribute.AttributeInstance;6import org.bukkit.entity.Player;7import org.bukkit.potion.PotionEffect;8 9enum CareType {10 11 HEAL("Restore your health, or the health of another player"),12 FEED("Refill your hunger bar, or the hunger bar of another player"),13 CLEANSE("Remove every potion effect from you, or from another player");14 15 private final String description;16 17 CareType(String description) {18 this.description = description;19 }20 21 String description() {22 return description;23 }24 25 String commandName() {26 return name().toLowerCase(Locale.ROOT);27 }28 29 void apply(Player player) {30 switch (this) {31 case HEAL -> {32 AttributeInstance maxHealth = player.getAttribute(Attribute.MAX_HEALTH);33 player.setHealth(maxHealth == null ? 20.0 : maxHealth.getValue());34 player.setFireTicks(0);35 }36 case FEED -> {37 player.setFoodLevel(20);38 player.setSaturation(20.0f);39 }40 case CLEANSE -> {41 for (PotionEffect effect : player.getActivePotionEffects()) {42 player.removePotionEffect(effect.getType());43 }44 }45 }46 }47}- The third value. Its lowercase name,
cleanse, becomes the command word. - The new branch of the
switch. - Returns every effect the player currently has. Removing them one by one inside the loop is the usual, safe way to clear a player.
The config needs the matching entries, and so does plugin.yml:
Where this file livesproject-heal-feed-exercisesrcmainresourcesconfig.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.
- project-heal-feed-exercise/
- src/main/
- java/com/example/projecthealfeedexercise/Package com.example.projecthealfeedexercise
- CareCommand.javaCommand (Brigadier tree)
- CareType.javaEnum: a fixed list of choices
- Cooldowns.javaHelper class
- HealFeedPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- Messages.javaHelper class
- resources/Files copied into the jar as they are
- config.ymlyou are hereDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/projecthealfeedexercise/Package com.example.projecthealfeedexercise
- 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.
1cooldowns:2 heal-seconds: 603 feed-seconds: 304 cleanse-seconds: 455 6messages:7 heal:8 self: "<green>You feel much better."9 other: "<green>You healed <yellow><target></yellow>."10 received: "<green><yellow><sender></yellow> healed you."11 feed:12 self: "<green>Your hunger is gone."13 other: "<green>You fed <yellow><target></yellow>."14 received: "<green><yellow><sender></yellow> fed you."15 cleanse:16 self: "<green>Every potion effect is gone."17 other: "<green>You cleansed <yellow><target></yellow>."18 received: "<green><yellow><sender></yellow> cleansed you."19 cooldown: "<red>You can use <yellow>/<command></yellow> again in <yellow><time></yellow>."20 console-needs-target: "<red>The console must name a player, like <yellow>/<command> Steve</yellow>."Where this file livesproject-heal-feed-exercisesrcmainresourcesplugin.yml
Files in src/main/resources are copied into the jar exactly as they are. Paper looks for plugin.yml at the top of the jar.
- project-heal-feed-exercise/
- src/main/
- java/com/example/projecthealfeedexercise/Package com.example.projecthealfeedexercise
- CareCommand.javaCommand (Brigadier tree)
- CareType.javaEnum: a fixed list of choices
- Cooldowns.javaHelper class
- HealFeedPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- Messages.javaHelper class
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlyou are hereTells Paper the plugin's name, version and main class
- java/com/example/projecthealfeedexercise/Package com.example.projecthealfeedexercise
- 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: HealFeedCleanse2version: '1.0.0'3main: com.example.projecthealfeedexercise.HealFeedPlugin4api-version: '26.3'5description: The /heal and /feed commands with permissions, targets, cooldowns and configurable messages.6permissions:7 healfeed.heal:8 description: Use /heal on yourself.9 default: op10 healfeed.heal.others:11 description: Use /heal on another player.12 default: op13 healfeed.feed:14 description: Use /feed on yourself.15 default: op16 healfeed.feed.others:17 description: Use /feed on another player.18 default: op19 healfeed.cleanse:20 description: Use /cleanse on yourself.21 default: op22 healfeed.cleanse.others:23 description: Use /cleanse on another player.24 default: op25 healfeed.bypass-cooldown:26 description: Use the commands without waiting for the cooldown.27 default: op28 healfeed.*:29 description: Every HealFeed permission.30 default: op31 children:32 healfeed.heal: true33 healfeed.heal.others: true34 healfeed.feed: true35 healfeed.feed.others: true36 healfeed.cleanse: true37 healfeed.cleanse.others: true38 healfeed.bypass-cooldown: trueTune the commands with the config only
Without touching the Java code: make /feed free of cooldown, make /heal wait five minutes, and change the cooldown message so it says "Please wait" instead. Which value switches the /feed cooldown off?
Hint
The seconds are under cooldowns. Remember that edits only apply after a restart.
Show the solution
Set feed-seconds: 0 and heal-seconds: 300. With 0 seconds the plugin stores a ready time equal to "now", which has already passed the next moment, so nobody ever waits. Change the text under messages.cooldown and keep the <time> placeholder in it, or the player will not see how long is left.
1cooldowns:2 heal-seconds: 3003 feed-seconds: 04 5messages:6 cooldown: "<red>Please wait <yellow><time></yellow> before using <yellow>/<command></yellow> again."Extension ideas
- Heal everyone. Swap
ArgumentTypes.player()forArgumentTypes.players()so/heal @aworks, and loop over the list. Think about which permission such a command should need. - Sound and particles. Play a sound and spawn heart particles when somebody is healed. See Potion effects, particles and effects.
- Action bar messages. Send the "you feel much better" text to the action bar instead of the chat. See Titles, action bars and sounds.
- Cooldown cleanup. Remove expired entries from the map every few minutes with a repeating task. See The scheduler.
- A reload command. Add
/healfeed reloadand rebuild the messages from the new config. See Configuration files. - Different cooldowns per rank. Add a
cooldowns.vipsection with shorter times and read from it when the player has ahealfeed.vippermission.
The next project, Homes, saves data per player and adds teleporting with a warmup, and the cooldown idea you learned here comes back again.
Recap
- A Brigadier command is a tree: a literal node for the word, an optional argument node for the target, and
executeson every node that may be the end of the command. requires(...)hides a command from players without a permission; declare the permissions inplugin.ymlwith a default.- A second, stronger permission on the argument node protects "do it to somebody else".
ArgumentTypes.player()gives you a resolver;resolve(source)turns it into a list of players, and a missing player produces Brigadier's own error.- An enum can describe what differs between similar commands, so one command class serves them all.
- A cooldown is a map from UUID to the time the player may act again. Store the UUID, never the
Playerobject. - Put every player-facing message in
config.ymland fill the names in with placeholders.
Quick quiz
A player has
healfeed.healbut nothealfeed.heal.others. What happens when they type/heal Alex?Therequireson the argument node hides it from this player. The wordAlexthen has nowhere to go in the tree, so the command is rejected. Plain/healstill works for them.Why does
Cooldownsuse the player's UUID as the map key, instead of thePlayerobject?Maps can use any object as a key. The real reason is that aPlayerobject stops being useful when the player leaves, so keeping it in a map would break after a relog and leak memory.What does
return Command.SINGLE_SUCCESS;tell Paper?It is simply the number1with a readable name. Vanilla's/executeand command blocks use the result to decide whether a command succeeded.You add a new value
HEAL_ALLto theCareTypeenum and run the server. What do you still have to do for it to work?The command class is reused, and the plugin registers one command per enum value in its loop. What differs per value is the action inapplyand the names it reads from the files.Who should the cooldown belong to when Steve types
/heal Alex?If the cooldown belonged to Alex, other players could keep him from healing himself, and Steve could heal many players in a row. The cooldown limits the person who uses the command.
Next steps
- Project: Homes: store data for every player and teleport them safely.
- Commands, part 2: more argument types, suggestions and sub-commands.
- Practice Arena: challenges for commands, permissions and cooldowns.