Writing a Command
This tutorial adds a /greet <player> command to the greet module from the previous tutorial. When run, it sends the configured greeting to the named player and confirms to the sender. By the end you will understand how Paper’s Brigadier integration works, how CommandRegistrar wraps it, and how to add a permission.
How Brigadier commands work in Paper
Paper exposes Brigadier through the LifecycleEvents.COMMANDS lifecycle event. At that point you hand Mojang’s CommandDispatcher a tree of CommandNode objects. Each LiteralCommandNode corresponds to one word in a command path; ArgumentCommandNode nodes consume user input.
The entry point for every command is a LiteralCommandNode<CommandSourceStack>. CommandSourceStack carries the sender and, for commands run from an entity, the entity’s location. You get a CommandSender with ctx.getSource().getSender().
You never touch the CommandDispatcher directly. PaperCommandRegistrar registers a lifecycle handler once in its constructor and queues nodes in a pending list. When LifecycleEvents.COMMANDS fires, all queued nodes are registered in bulk. This means you can call ctx.commands().register(...) at any point during module enable() without worrying about timing.
1. Create the command class
Create src/main/java/de/kreiscraft/modules/greet/commands/GreetCommand.java:
package de.kreiscraft.modules.greet.commands;
import com.mojang.brigadier.Command;import com.mojang.brigadier.arguments.StringArgumentType;import com.mojang.brigadier.tree.LiteralCommandNode;import de.kreiscraft.core.message.MessageService;import de.kreiscraft.modules.greet.GreetConfig;import io.papermc.paper.command.brigadier.CommandSourceStack;import io.papermc.paper.command.brigadier.Commands;import org.bukkit.Server;import org.bukkit.command.CommandSender;import org.bukkit.entity.Player;
public final class GreetCommand {
private GreetCommand() {}
public static LiteralCommandNode<CommandSourceStack> build( GreetConfig config, MessageService messages, Server server) { return Commands.literal("greet") .requires(src -> src.getSender().hasPermission("kreiscraft.greet.use")) .then(Commands.argument("player", StringArgumentType.word()) .executes(ctx -> { CommandSender sender = ctx.getSource().getSender(); String targetName = StringArgumentType.getString(ctx, "player"); Player target = server.getPlayer(targetName); if (target == null) { messages.send(sender, "greet.target-not-found", "player", targetName); return Command.SINGLE_SUCCESS; } messages.send(target, "greet.join", "message", config.message()); messages.send(sender, "greet.sent", "player", target.getName()); return Command.SINGLE_SUCCESS; })) .build(); }}Walk through the structure:
Commands.literal("greet")— the root node that matches the literal stringgreet..requires(src -> ...)— a predicate evaluated before the command tree is shown in tab-completion and before execution. Returningfalsehides the command entirely from that sender..then(Commands.argument("player", StringArgumentType.word()))— appends a child node that consumes exactly one word as a string. The registered name"player"is the key you use when reading the value back withStringArgumentType.getString(ctx, "player")..executes(ctx -> { ... return Command.SINGLE_SUCCESS; })— the executor attached to this node.Command.SINGLE_SUCCESS(value1) is the conventional success return..build()— materialises theLiteralCommandNode. Without this call the builder returns aLiteralArgumentBuilder, not theLiteralCommandNodethatCommandRegistrarexpects.
2. Add the lang keys
Open src/main/resources/lang/de.yml and extend the greet: block:
greet: join: "<prefix> <green><message>" sent: "<prefix> <green>Begrüßung an <player> gesendet." target-not-found: "<prefix> <red>Spieler <player></red> nicht gefunden."Open src/main/resources/lang/en.yml:
greet: join: "<prefix> <green><message>" sent: "<prefix> <green>Greeting sent to <player>." target-not-found: "<prefix> <red>Player <player></red> not found."3. Wire the command into the module
Open GreetModule.java and update enable() to pass Server to the command builder:
import de.kreiscraft.modules.greet.commands.GreetCommand;import org.bukkit.Server;
@Overridepublic void enable(ModuleContext ctx) { try { this.config = ctx.moduleConfig().get(GreetConfig.class); } catch (SerializationException e) { throw new ModuleException("failed to load config for module " + id(), e); } if (config == null) config = new GreetConfig(null);
Server server = ctx.plugin().getServer();
ctx.events().register(new GreetListener(() -> this.config, ctx.messages())); ctx.commands().register( GreetCommand.build(config, ctx.messages(), server), "Send the configured greeting to a player");}ctx.commands().register(node, description) queues the node. Paper’s lifecycle machinery picks it up on the next LifecycleEvents.COMMANDS event. Commands registered this way are fully mod-managed — they disappear when the plugin is reloaded or disabled without any extra teardown code.
4. Add the permission to plugin.yml
Open src/main/resources/plugin.yml and append to the permissions: block:
kreiscraft.greet.use: description: Send the configured greeting to a named player default: opdefault: op means ops have the permission automatically. Use default: true to grant it to all players, or default: false to require explicit assignment.
5. Build and run
./gradlew build./gradlew runServerLog in with an op account. In the game chat:
/greet SteveSteve (if online) receives the greeting in chat. You see the confirmation.
If Steve is offline:
[Kreiscraft] Player Steve not found.Try running /greet Steve as a non-op account. The command should not appear in tab-completion and executing it produces no output — the .requires() predicate hid it entirely.
How the pieces connect
GreetModule.enable() └─ ctx.commands().register(GreetCommand.build(...), "...") └─ PaperCommandRegistrar queues the LiteralCommandNode └─ LifecycleEvents.COMMANDS fires → Paper registers it └─ Player types /greet <tab> or executes /greet Steve └─ GreetCommand executor runs → MessageService.send()The pattern — static build() factory taking only the dependencies it needs, returning a LiteralCommandNode — keeps commands unit-testable: you can call build(mockConfig, mockMessages, mockServer) in a test without a running server.
Going further
- Add a second literal subcommand (e.g.
/greet reload) by chaining another.then(Commands.literal("reload").executes(...))off the root node. - Use
EntitySelectorArgumentType.players()instead ofStringArgumentType.word()to get proper player tab-completion from Paper’s built-in argument types. - Check how
PingCommandinmodules/admin/commands/handles the optional-vs-required argument pattern if you need/greetto greet the sender themselves and/greet <player>to target someone else.