{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreibne5xxj2ewlrzviegnzshysg54k6jhp6bgx4vghpv7zgnrjp6m2e",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mpofmkqoy672"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreiesgl6f4h7c5p44rugsshs4jy6idn3du2slndkqqlrkt45kx47mze"
},
"mimeType": "image/webp",
"size": 100120
},
"path": "/eande171/youre-writing-paper-commands-wrong-2o8i",
"publishedAt": "2026-07-02T15:56:40.000Z",
"site": "https://dev.to",
"tags": [
"programming",
"java",
"gamedev",
"minecraft",
"ModGUI",
"here",
"@Override",
"@a",
"@p"
],
"textContent": "You've probably written a `CommandExecutor` before. Everyone who's touched Bukkit has.\n\nDeclare the command in `plugin.yml`, implement `onCommand`, cast `args[0]` to whatever you need, hope nobody fat-fingers the input. It compiles. It runs. It's confusing to debug. And it's the wrong way to do it in 2026.\n\n\n\n # plugin.yml\n commands:\n punish:\n description: Opens the punishment GUI\n usage: /punish <player>\n\n\n\n public class PunishCommand implements CommandExecutor {\n @Override\n public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {\n if (!(sender instanceof Player staff)) return true;\n if (args.length < 1) return true;\n\n Player target = Bukkit.getPlayer(args[0]);\n if (target == null) {\n sender.sendMessage(\"Player not found.\");\n return true;\n }\n // ... open the GUI\n return true;\n }\n }\n\n\nTie it together in `onEnable()` with `getCommand(\"punish\").setExecutor(new PunishCommand())`, add a separate `TabCompleter` implementation to handle suggestions, and you're done.\n\nSeems perfectly fine... totally not confusing at all... (if you understood any of that, you're doing better than I am :P)\n\nThis implementation has many issues... like `Bukkit.getPlayer(args[0])` only matching an exact, currently-online name. No selectors. No partial matching. You write all of that yourself or not at all.\n\nTab completion lives in a second method you keep in sync with parsing by hand. Change one, forget the other, and tab completion starts \"lying\" to your players (a problem that has taken me HOURS to solve in the past... i'm getting flashbacks ;-;).\n\nAnd the tree itself is static, fixed in `plugin.yml`. Want `/report` to take a severity argument **only** when severities are configured? You can't say that in `plugin.yml` and you end up with a tangled mess that is almost never clean (either to you, or the players).\n\nPaper ships Mojang's Brigadier (the same framework vanilla Minecraft uses for everything) through a lifecycle hook: `LifecycleEvents.COMMANDS`. You register a tree of literals and arguments. No `commands:` block needed.\n\nHere's the registration from ModGUI (a moderation plugin I've been building), it starts inside a `LifecycleEvents.COMMANDS` handler, registering `/punish`:\n\n\n\n event.registrar().register(\n Commands.literal(\"punish\")\n .requires(ctx -> {\n var s = ctx.getSender();\n return s.hasPermission(Permissions.PUNISH_ALL)\n || s.hasPermission(Permissions.PUNISH_WARN)\n || s.hasPermission(Permissions.PUNISH_KICK)\n || s.hasPermission(Permissions.PUNISH_MUTE)\n || s.hasPermission(Permissions.PUNISH_TEMPBAN)\n || s.hasPermission(Permissions.PUNISH_BAN);\n })\n .then(Commands.argument(\"target\", ArgumentTypes.player())\n .executes(cmd::onPunish))\n .build()\n );\n\n // investigate and modgui (spyglass/reload/version/help) register the same way\n\n\n`.requires()` lives on the tree, not the handler, and Brigadier checks it when building tab completion too, a player without `modgui.punish.*` never sees `/punish` suggested.\n\n`/report` is the interesting one, because it doesn't always have the same shape. Its tree gets built conditionally, based on config, read once before either branch registers:\n\n\n\n ReportConfig reportConfig = getConfigService().getReportConfig();\n\n if (reportConfig.isSeverityEnabled()) {\n event.registrar().register(\n Commands.literal(\"report\")\n .requires(ctx -> ctx.getSender().hasPermission(Permissions.REPORT))\n .then(Commands.argument(\"target\", ArgumentTypes.player())\n .then(Commands.argument(\"severity\", StringArgumentType.word())\n .suggests((ctx, builder) -> {\n for (String key : reportConfig.getSeverities().keySet()) {\n builder.suggest(key);\n }\n return builder.buildFuture();\n })\n .then(Commands.argument(\"reason\", StringArgumentType.greedyString())\n .executes(cmd::onReport))))\n .build()\n );\n }\n\n\nThat `.suggests()` block on `severity` is doing the same thing `TabCompleter` did... just better...\n\nAnd if severities aren't enabled, the branch registered is a different, simpler tree:\n\n\n\n else {\n event.registrar().register(\n Commands.literal(\"report\")\n .requires(ctx -> ctx.getSender().hasPermission(Permissions.REPORT))\n .then(Commands.argument(\"target\", ArgumentTypes.player())\n .then(Commands.argument(\"reason\", StringArgumentType.greedyString())\n .executes(cmd::onReport)))\n .build()\n );\n }\n\n\nWhether severity is part of the grammar gets decided once, at registration, by reading config. Tab completion and error messages stay accurate and are much easier to debug (yay!).\n\n## `ArgumentTypes.player()` Isn't What It Looks Like\n\n`ArgumentTypes.player()` doesn't actually give you a `Player` (you're probably thinking _'but wait... that's counterintuitive'_ but just wait...), it gives you something you have to turn into one:\n\n\n\n private Player resolvePlayer(CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {\n PlayerSelectorArgumentResolver resolver = ctx.getArgument(\"target\", PlayerSelectorArgumentResolver.class);\n\n List<Player> players = resolver.resolve(ctx.getSource());\n return players.isEmpty() ? null : players.getFirst();\n }\n\n\nIt looks like a bunch of jargon but TLDR; you can now accept `Steve`, `@a`, `@p` without implementing it yourself.\n\nAnd don't forget the empty case, as a selector can match nobody:\n\n\n\n public int onPunish(CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {\n CommandSender sender = ctx.getSource().getSender();\n Player staff = requirePlayer(ctx);\n\n if (staff == null) return Command.SINGLE_SUCCESS;\n Player target = resolvePlayer(ctx);\n\n if (target == null) {\n sender.sendMessage(plugin.getMessageService().noTarget());\n return Command.SINGLE_SUCCESS;\n }\n\n if (target.hasPermission(Permissions.EXEMPT)) {\n sender.sendMessage(plugin.getMessageService().exemptPunish());\n return Command.SINGLE_SUCCESS;\n }\n\n PunishGui.open(plugin, staff, target);\n return Command.SINGLE_SUCCESS;\n }\n\n\nActual functionality here is handled by PunishGui, keeping everything nice and tidy :D\n\n`plugin.yml` commands aren't going anywhere, Bukkit's command map still works fine (ig...). But it was always a workaround, not a first choice.\n\nIf any of you made it to this bit... THANK YOU!!! If you wanna poke around the plugin I used as an example in this blog, you can find that here.",
"title": "You're Writing Paper Commands Wrong"
}