docs/paper/commands/SKILL.md
# Commands Skill — Paper ## Purpose Reference this skill when registering commands on a Paper 1.21 server. Paper uses native Brigadier via `LifecycleEvents.COMMANDS`; this skill explains when and how to use it versus the legacy `CommandExecutor` approach. ## When to Use This Skill - Creating any `/command` that players or operators can run - Adding tab completion to commands - Restricting commands to players with specific permissions - Supporting subcommands with typed arguments (integers, pla
npx skillsauth add MrPippi/MPS docs/paper/commandsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Reference this skill when registering commands on a Paper 1.21 server. Paper uses native Brigadier via LifecycleEvents.COMMANDS; this skill explains when and how to use it versus the legacy CommandExecutor approach.
/command that players or operators can run| Class / Method | Purpose | Notes |
|---------------|---------|-------|
| LifecycleEvents.COMMANDS | The lifecycle event type for command registration | Use with getLifecycleManager().registerEventHandler() |
| Commands (registrar) | The Brigadier command registrar provided by Paper | Obtained from the lifecycle event |
| Commands#register(LiteralCommandNode, String) | Register a root command node with description | Description appears in /help |
| Commands.literal(String) | Start building a command literal node | From com.mojang.brigadier.builder.LiteralArgumentBuilder via Paper wrapper |
| Commands.argument(String, ArgumentType<T>) | Add a typed argument | From Brigadier's argument builder |
| ArgumentTypes | Paper's built-in argument types | ArgumentTypes.player(), ArgumentTypes.world(), etc. |
| StringArgumentType.word() | Single-word string argument | From com.mojang.brigadier.arguments |
| IntegerArgumentType.integer(min, max) | Bounded integer argument | |
| CommandSourceStack | The command source (wraps CommandSender) | Access via ctx.getSource() |
| ctx.getSource().getSender() | Get the underlying CommandSender | Cast to Player if needed |
| Command.SINGLE_SUCCESS | Return value indicating success | Return 1 from executes lambda |
| .requires(source -> ...) | Permission predicate for the command | Check source.getSender().hasPermission(...) |
package com.yourorg.myplugin.commands;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.context.CommandContext;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.command.brigadier.Commands;
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
public class CommandRegistrar {
public static void register(JavaPlugin plugin) {
plugin.getLifecycleManager().registerEventHandler(
LifecycleEvents.COMMANDS,
event -> {
Commands commands = event.registrar();
// Simple command: /myplugin
commands.register(
Commands.literal("myplugin")
.requires(source -> source.getSender().hasPermission("myplugin.use"))
.executes(ctx -> executeRoot(ctx))
// Subcommand: /myplugin info <playerName>
.then(Commands.literal("info")
.then(Commands.argument("player", StringArgumentType.word())
.executes(ctx -> executeInfo(ctx))))
// Subcommand: /myplugin level <number>
.then(Commands.literal("level")
.requires(source -> source.getSender().hasPermission("myplugin.admin"))
.then(Commands.argument("amount", IntegerArgumentType.integer(1, 100))
.executes(ctx -> executeLevel(ctx))))
.build(),
"MyPlugin main command"
);
}
);
}
private static int executeRoot(CommandContext<CommandSourceStack> ctx) {
CommandSender sender = ctx.getSource().getSender();
sender.sendMessage(Component.text("MyPlugin v1.0.0").color(NamedTextColor.GOLD));
return Command.SINGLE_SUCCESS;
}
private static int executeInfo(CommandContext<CommandSourceStack> ctx) {
CommandSender sender = ctx.getSource().getSender();
String targetName = StringArgumentType.getString(ctx, "player");
Player target = sender.getServer().getPlayer(targetName);
if (target == null) {
sender.sendMessage(Component.text("Player not found: " + targetName)
.color(NamedTextColor.RED));
return 0; // return 0 to signal failure
}
sender.sendMessage(Component.text("Player: " + target.getName())
.color(NamedTextColor.AQUA));
return Command.SINGLE_SUCCESS;
}
private static int executeLevel(CommandContext<CommandSourceStack> ctx) {
CommandSender sender = ctx.getSource().getSender();
// Only allow players (not console) for level command
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("Only players can use this command.")
.color(NamedTextColor.RED));
return 0;
}
int amount = IntegerArgumentType.getInteger(ctx, "amount");
player.setLevel(player.getLevel() + amount);
player.sendMessage(Component.text("Level set to " + player.getLevel())
.color(NamedTextColor.GREEN));
return Command.SINGLE_SUCCESS;
}
}
Call from main class onEnable:
CommandRegistrar.register(this);
Mixing Brigadier with plugin.yml command declarations: Brigadier commands registered via LifecycleEvents.COMMANDS do NOT need to be declared in plugin.yml. Declaring them there AND in Brigadier causes duplicate registration in some builds.
Returning 0 without a message: When a command fails, always send an error message before returning 0. Silent failures confuse players.
Using CommandSender as Player without checking: Console can run commands too. Always instanceof check before casting: if (sender instanceof Player player) { ... }.
Registering commands outside LifecycleEvents.COMMANDS: Calling Commands.register() directly in onEnable() (not inside the lifecycle handler) is not supported and will throw an error.
Forgetting .requires() for admin commands: Without a permission check, any player can run admin subcommands.
LifecycleEvents.COMMANDS is the supported way to register Brigadier commands. Legacy CommandExecutor still works but does not support typed arguments or native client-side suggestions.SuggestionProvider for built-in types. See command-completion.md for custom suggestions.development
透過 NMS Scoreboard/Objective/Team API 操作 sidebar、tablist 顯示名稱與計分板(Paper NMS + Mojang-mapped)/ Operate sidebar, tablist, and scoreboard via NMS Scoreboard/Objective/Team API
research
操作 GameProfile 進行 skin 注入,用於 NPC 外觀設定與假玩家實體(Paper NMS + Mojang-mapped)/ Manipulate GameProfile for skin injection used in NPC appearance and fake player entities
tools
透過 ClientboundLevelParticlesPacket 實現進階 NMS 粒子效果:客戶端專屬、大量粒子、自定義參數(Paper NMS + Mojang-mapped)/ Advanced NMS particle effects via ClientboundLevelParticlesPacket with per-client and bulk support
documentation
直接操作 CompoundTag 讀寫物品、實體、方塊實體的 NBT 資料(Paper NMS + Mojang-mapped)/ Read and write NBT data on items, entities, and block entities via CompoundTag