Paper Plugin Guide
File mode0

Projects

Project: /heal and /feed with cooldowns

Two Brigadier commands with permissions, targets, cooldowns and configurable messages.

BeginnerProject, about 50 min read

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:

You feel much better.

Steve, who is an admin, can also heal Alex by naming him. Steve sees one message and Alex sees another:

You healed Alex.
Steve healed you.

If Steve tries again too soon, the command refuses and says how long to wait:

You can use /heal again in 42 seconds.

These are the two commands and the rules behind them:

CommandWhat it doesPermission for yourselfPermission for others
/heal [player]Sets health to the maximum and puts out firehealfeed.healhealfeed.heal.others
/feed [player]Fills the hunger bar and the saturationhealfeed.feedhealfeed.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:

The checks before /heal or /feed does its job Paper first hides the command from players without the permission. Then the command checks whether a target was named and whether the sender may use the others permission. Then it checks the cooldown. Only after all checks pass does it heal or feed the target and send the messages. /heal or /heal Alex Permission check in requires(...) healfeed.heal, or healfeed.heal.others for a target Unknown command Who is the target? named player, or the sender (console needs a name) Cooldown still running? yes Tell the time left no (or bypass) Start cooldown, apply the effect health to full, or hunger bar to full Messages from config.yml to the sender, and to the target if someone else
Everything that happens between typing /heal and getting healed. Each box is one thing we will write.

And here is how each requirement maps to a piece of code, with the chapter that teaches it:

RequirementThe piece that does itChapter
Commands with an optional playerA Brigadier command tree with an argument nodeCommands, part 2
Who may use whatPermissions in plugin.yml and requires(...)Permissions
Waiting between usesA Cooldowns class that stores a ready time per player UUIDCooldowns, toggles and player state
Editable messagesconfig.yml and MiniMessage placeholdersConfiguration files
One design for both commandsAn enum that describes what is differentEnums, records and modern Java

The build steps are:

  1. Create the project and plugin.yml permissions

    Declare who may do what before writing any code.

  2. Write /heal for yourself

    The smallest working command, and the one new idea: how a Brigadier command is registered.

  3. Add the optional player target

    An argument node, a second permission and the first real branch.

  4. Share one design between /heal and /feed

    An enum describes the difference, so we do not copy and paste a whole class.

  5. Add the cooldown

    A map from UUID to the time when the player may use the command again.

  6. Put the messages in config.yml

    A small helper that sends a configured message with placeholders.

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

plugin.ymlCompiles on Paper 26.3Compile Lab
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
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1name: 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
  1. The section that lists every permission this plugin uses. Declaring them makes them show up in permission plugins and gives them a sensible default.
  2. A permission is just a name. Starting every name with the plugin's name keeps it from clashing with other plugins.
  3. 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".
  4. Who has the permission when nobody has granted it. op means server operators only. Other values are true (everyone), false (nobody) and not op.
  5. A permission that does not belong to a command: it switches the cooldown off for whoever has it.
  6. A convenience for admins: one permission that contains all the others through children. In a permissions plugin, granting healfeed.* 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.

HealCommand.javaCompiles on Paper 26.3Compile Lab
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
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.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}
  1. A constructor is the special method that runs when you write new. Making it private means nobody can create a HealCommand object, so the class is only a home for static methods.
  2. The first word of the command: players type /heal. A literal node is a fixed word.
  3. The gatekeeper. Players for whom this returns false do not see the command in tab completion, and typing it gives the usual "Unknown or incomplete command" error. The part with the arrow is a lambda: a tiny function that gets the command source and answers yes or no.
  4. What to run when the command is typed. HealCommand::healSender is a method reference: "run that method".
  5. Finishes the tree and returns its root node, ready to hand to Paper.
  6. The sender is whoever typed the command. That can be a player, but also the console or a command block.
  7. Asks "is the sender a player?" and, when it is, gives the same object a second name, player, typed as a Player. The ! flips the question: this branch handles senders that are not players, such as the console, which has no health to restore.
  8. 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.
  9. Sets the health. Giving it more than the maximum would throw an error, which is why we ask for the maximum first.
  10. Puts out fire. Burning players would otherwise take damage right after being healed.
  11. A command method returns a number. 1 (this constant) means "it worked". Returning 0 means "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:

HealFeedPlugin.javaCompiles on Paper 26.3Compile Lab
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
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.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}
  1. Asks Paper: "when the COMMANDS event happens, run this code".
  2. 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:

You feel much better.

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:

CareCommand.javaCompiles on Paper 26.3Compile Lab
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
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

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    }
  1. The name of the permission that switches the cooldown off. A static final text is a constant: written once, never changed, and the name in capitals says so.
  2. 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 creates CareType).
  3. The helper that sends configured messages, written in step 6.
  4. How long a player must wait between uses. Duration is Java's type for a length of time.
  5. The notebook of who may use the command again and when, written in step 5. Every CareCommand object makes its own.
  6. 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:

CareCommand.javaCompiles on Paper 26.3Compile Lab
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
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

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}
  1. Builds the permission name from the command name: healfeed.heal or healfeed.feed. The same code now serves both commands.
  2. The real permission check. The sender needs healfeed.heal to use the command at all.
  3. Runs when the player typed only /heal.
  4. Hangs a child node under the command, for the word after /heal.
  5. The argument's name. We use it later to get the player back out.
  6. The type of the argument: exactly one player. It gives players tab completion for online names and accepts selectors like @s and @r.
  7. This node needs a different, stronger permission. A player without it can still use plain /heal but cannot name a target, and tab completion does not even offer player names.
  8. 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:

CareCommand.javaCompiles on Paper 26.3Compile Lab
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
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

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}
  1. 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.
  2. Gets the argument by the name we gave it. The second part is the type we expect back.
  3. Works out which players the argument stands for. The answer is a list, even though this argument type only ever allows one player.
  4. 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.

CareCommand.javaCompiles on Paper 26.3Compile Lab
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
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

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}
  1. If a player typed the command, the target is that same player.
  2. Sender and target are the same object. All the real work lives in care, which we write below.
  3. 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:

CareType.javaCompiles on Paper 26.3Compile Lab
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
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.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}
  1. An enum is a type with a fixed set of values. Here there are exactly two: HEAL and FEED. We will add a third in the exercise.
  2. Each value is created with its own description text, which Paper shows in /help.
  3. Each value keeps its own copy of the description.
  4. Every enum value knows its own name as text. HEAL becomes heal, and that word is reused for the command, the permission and the config keys. Locale.ROOT makes the lowercasing behave the same on every computer, including ones set to Turkish.
  5. Chooses what to do depending on which value we are. this is the value the method was called on: HEAL.apply(player) runs the first branch.
  6. The hunger bar goes from 0 to 20 (ten food icons). 20 means full.
  7. 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 f after the number marks it as a float.

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:

Cooldowns.javaCompiles on Paper 26.3Compile Lab
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
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.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}
  1. The notebook. Each UUID maps to the moment, in milliseconds on the system clock, when that player may use the command again.
  2. Looks up the player. The type is Long with a capital L, because a map can hold "nothing" and then get returns null. The plain long cannot be null.
  3. The player has no entry, so there is nothing to wait for.
  4. Ready time minus now equals how long is left. A negative number means the time has passed.
  5. Tidies up: once a cooldown has run out, forget it.
  6. Writes the entry: now plus the cooldown length. Duration is 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:

CareCommand.javaCompiles on Paper 26.3Compile Lab
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
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

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}
  1. 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.
  2. 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.
  3. Still waiting: tell them, and stop with return 0 before anything is healed.
  4. Allowed to proceed, so start the cooldown for next time.
  5. The one line that actually heals or feeds. Everything above it was checking.
  6. 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.
  7. 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:

config.ymlCompiles on Paper 26.3Compile Lab
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
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

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>."
  1. How long each command makes the sender wait. The name heal-seconds is built by the plugin from the command name, so a command called feed reads feed-seconds.
  2. A whole number of seconds. Setting it to 0 means no cooldown.
  3. Shown to the sender when they healed someone else. <target> is a placeholder for that player's name.
  4. Shown to the player who was healed, with <sender> replaced by the name of whoever healed them.
  5. 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:

Messages.javaCompiles on Paper 26.3Compile Lab
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
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.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}
  1. The three dots mean "any number of these, including none". Calls like messages.send(sender, "heal.self") pass none; others pass one or two.
  2. Joins the section name and the path: messages plus heal.self becomes messages.heal.self.
  3. A missing or empty text means "send nothing". A server owner can silence a message by writing "".
  4. 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:

HealFeedPlugin.javaCompiles on Paper 26.3Compile Lab
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
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.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}
  1. Creates plugins/HealFeed/config.yml on the first start.
  2. One Messages helper, shared by both commands.
  3. Gives you every value of the enum, here HEAL and then FEED. The loop below runs once for each.
  4. Reads cooldowns.heal-seconds, then cooldowns.feed-seconds. The second argument is the default if the setting is missing.
  5. Each command gets its own object, so each has its own Cooldowns notebook.
  6. 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
        • resources/
          • plugin.ymlName, main class and all permissions
          • config.ymlCooldown lengths and every message

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 10 and wait until the food bar is low, then /feed. The bar fills.
  • Run /heal twice quickly. The second try shows the red cooldown message with the seconds left, and counts down as you retry.
  • Run /feed right 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. Run heal 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 only healfeed.heal with a permissions plugin: /heal works, but /heal Alex does 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, as CareType does.
  • Forgetting saveDefaultConfig(). Without it there is no config.yml, every message is missing and the commands feel silent.
  • Checking the permission in requires but typing it differently in plugin.yml. healfeed.heal and healfeed.Heal are 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 Player object instead of the UUID. A Player object is only valid while the player is online. Maps keyed by Player leak 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

Try it

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.

CareType.javaCompiles on Paper 26.3Compile Lab
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
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.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}
  1. The third value. Its lowercase name, cleanse, becomes the command word.
  2. The new branch of the switch.
  3. 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:

config.ymlCompiles on Paper 26.3Compile Lab
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
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

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>."
plugin.ymlCompiles on Paper 26.3Compile Lab
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
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1name: 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: true
Try it

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

config.yml (changed lines)
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() for ArgumentTypes.players() so /heal @a works, 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 reload and rebuild the messages from the new config. See Configuration files.
  • Different cooldowns per rank. Add a cooldowns.vip section with shorter times and read from it when the player has a healfeed.vip permission.

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 executes on every node that may be the end of the command.
  • requires(...) hides a command from players without a permission; declare the permissions in plugin.yml with 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 Player object.
  • Put every player-facing message in config.yml and fill the names in with placeholders.

Quick quiz

  1. A player has healfeed.heal but not healfeed.heal.others. What happens when they type /heal Alex?

  2. Why does Cooldowns use the player's UUID as the map key, instead of the Player object?

  3. What does return Command.SINGLE_SUCCESS; tell Paper?

  4. You add a new value HEAL_ALL to the CareType enum and run the server. What do you still have to do for it to work?

  5. Who should the cooldown belong to when Steve types /heal Alex?

Next steps