Command framework
RootCommand centralises routing, permission checks, player-only checks, usage errors and tab
completion. The root command must still be declared in plugin.yml; Keystone binds its executor
and completer.
commands:
example:
description: Manage Example.
usage: /example help
RootCommand root = new RootCommand(messages, "usage")
.register(SimpleSubcommand.of("give", this::give)
.permission("example.command.give")
.usage("give <item> [player]")
.description("Give an item")
.completer(this::completeGive))
.register(SimpleSubcommand.of("menu", this::menu)
.permission("example.command.menu")
.requiresPlayer()
.description("Open the item browser"))
.register(new PlatformSubcommand(keystone, messages, "example.command.platform"))
.defaultTo(SimpleSubcommand.of("help", this::help));
root.bind(this, "example");
Subcommand rules
aliases(...)String...Additional names routed to the same subcommand.
permission(node)StringDeny execution when the sender lacks the node. Inaccessible subcommands are also hidden from first-argument tab completion.
requiresPlayer()Reject console and command blocks before the handler runs. Inside the handler,
context.requirePlayer()can then be used safely.usage(text)StringArguments shown by your help output or usage response. Use angle brackets for required values and square brackets for optional values.
description(text)StringShort action-oriented help text.
completer(function)Receives the sender and arguments after the subcommand name. Return only values the sender may actually use.
Reading arguments
private void give(CommandContext context) {
CommandArguments args = context.args();
String itemId = args.get(0, "");
Optional<Player> target = args.player(1);
int amount = args.integer(2).orElse(1);
if (itemId.isBlank()) {
messages.send(context.sender(), "usage");
return;
}
}
CommandArguments also provides optional(index), lower(index), joinFrom(index),
withoutFirst(), asList() and toArray(). Parsing methods return Optional so invalid input is
not silently converted into a valid value.
Tab completion
private List<String> completeGive(CommandSender sender, CommandArguments args) {
if (args.size() <= 1) {
return RootCommand.matching(itemIds(), args.get(0, ""));
}
return RootCommand.matching(
Bukkit.getOnlinePlayers().stream().map(Player::getName).toList(),
args.get(1, "")
);
}
RootCommand.matching performs case-insensitive prefix filtering. Do not return secret ids,
staff-only player data or values the sender lacks permission to select.
Verify the command boundary
Test one permitted player, one denied sender and console for every requiresPlayer() branch. Also
tab-complete as the denied sender: hidden first-level subcommands are part of the permission model.
If bind cannot find the root declared in plugin.yml, treat that startup failure as a packaging
error rather than registering an ad-hoc fallback command.