Skip to main content

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.

Deprecated names

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:

KindDeclared withSyntaxOmittable
Required argumentaddRequiredArgument/ban Notchno
Optional argumentaddOptionalArgument/heal [Notch]yes
FlagaddFlag/heal --silentyes

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 getSenderAsPlayer

resolvePlayer("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.

Next​

  • Arguments — required vs optional, and reading them back
  • Flags — the flag API and its two real limits
  • Modules — what the library is split into