Core concepts
Four types carry the whole API: ICommandManager, BaseCommand, SubCommand
and CommandDispatch.
Registration is two-phase
public class MyPlugin extends JavaPlugin {
private final ICommandManager<MyPlugin> commands = new CommandManager<>(this);
@Override
public void onEnable() {
this.commands.trackCommand(new HealCommand(this));
this.commands.flushRegistrations();
}
@Override
public void onDisable() {
this.commands.unregisterAll(this);
}
}
trackCommand records a command; flushRegistrations pushes the tracked set to
the server. Commands tracked after the flush are still registered, so the
ordering is not load-bearing.
registerCommand, registerCommands and unregisterCommands() still work but
are deprecated, because they were easy to mix up: registerCommand never
touched the server, and unregisterCommands() removed only reloadable
commands despite its name.
Use trackCommand, flushRegistrations and unregisterReloadableCommands.
Note unregisterAll(this) over the no-arg unregisterAll(). The no-arg form
removes commands owned by any plugin, which is only what you want for a host
plugin managing others' commands.
A command
Subclass BaseCommand<T extends Plugin> and implement perform:
public class HealCommand extends BaseCommand<MyPlugin> {
public HealCommand(MyPlugin plugin) {
super(plugin, "heal", "h"); // name + aliases
this.setDescription("Heal a player");
this.setPermission("myplugin.heal");
this.setReloadable(true); // unregisterReloadableCommands() drops it
// Positional and omittable -> optional argument.
this.addOptionalArgument("target", ArgumentTypes.player());
// Modifiers -> flags. Always parsed after every positional argument.
this.addFlag(Flags.boolFlag("silent").alias("s"));
this.addFlag(Flags.doubleFlag("amount", 0.0, 20.0).defaultTo(20.0));
}
@Override
protected CommandResultType perform(CommandDispatch<MyPlugin> dispatch) {
Player target = dispatch.resolvePlayer("target")
.orElse(dispatch.getSenderAsPlayer());
if (target == null) {
dispatch.getSender().sendMessage("Specify a player from console.");
return CommandResultType.FAILURE;
}
target.setHealth(dispatch.getFlagValue("amount", Double.class));
if (!dispatch.hasFlag("silent")) {
target.sendMessage("You have been healed.");
}
return CommandResultType.SUCCESS;
}
}
perform returns CommandResultType.SUCCESS or FAILURE — that is the whole
enum.
Three ways to take input
Worth getting straight early, because they are not interchangeable:
| Kind | Declared with | Syntax | Omittable |
|---|---|---|---|
| Required argument | addRequiredArgument | /ban Notch | no |
| Optional argument | addOptionalArgument | /heal [Notch] | yes |
| Flag | addFlag | /heal --silent | yes |
Rules of thumb:
- Positional data is an argument; a modifier is a flag.
- A required argument means the command does not match at all without it — there
is no absence to handle in
perform. - Declare in parse order: required, then optional, then flags. Flags must come last and cannot be followed by anything.
- Flags are expensive: one Brigadier node per ordered subset of the flag set. An optional argument is far cheaper than a fourth boolean flag.
Arguments covers this in full, including how to read each kind back and the two traps (reserved names, and values that look like flags).
Without subclassing
BaseCommand<MyPlugin> ping = BaseCommand.builder(this, "ping")
.description("Check latency")
.permission("myplugin.ping")
.executes(dispatch -> {
dispatch.getSender().sendMessage("Pong!");
return CommandResultType.SUCCESS;
})
.build();
this.commands.trackCommand(ping);
trackCommand also accepts a builder directly and returns the built command, so
the .build() is optional.
Sub-commands
super(plugin, "guild");
this.addSubCommand(new GuildCreateSubCommand(plugin)); // /guild create <name>
this.addSubCommand(new GuildInviteSubCommand(plugin)); // /guild invite <player>
Sender, executor, location
Three distinct concepts, deliberately not merged:
dispatch.getSender(); // who typed it — use for feedback and permissions
dispatch.getSenderAsPlayer(); // the same as a Player, or null from console
dispatch.getExecutor(); // the acting entity, which /execute as changes
dispatch.getLocation(); // where it runs, which /execute positioned changes
getSenderAsPlayer() is @Nullable even when you have called
setPlayerOnly(). Null-check it anyway rather than relying on a requirement
staying in place.
resolvePlayer vs getSenderAsPlayerresolvePlayer("target") reads a named argument. getSenderAsPlayer()
reads the sender. An earlier API had getPlayer() for the sender and
getOptionalPlayer(name) for an argument, which read as a nullable/non-null
pair of one concept and were unrelated. The old names remain as deprecated
delegates.