Skip to main content

Commands

Package: dev.anchorlight.stonelib.command

A plugin with more than a couple of commands usually ends up with one root command and a set of sub-commands: /rally go, /rally optin, /rally reload. CommandRouter handles that dispatch, so each sub-command is one small class.

SubCommand

public interface SubCommand {
String getName(); // required
default String getPermission() { return null; }
default String getUsage() { return "/" + getName(); }
default String getDescription() { return ""; }
void execute(CommandSender sender, String[] args); // required
default List<String> tabComplete(CommandSender sender, String[] args) { return List.of(); }
}
MethodMeaning
getName()The keyword players type. Matched case-insensitively.
getPermission()A node the sender must hold, or null for no check. Checked by the router before execute runs.
getUsage(), getDescription()Metadata for your own help output. The router does not display them.
execute(sender, args)Runs the sub-command. args does not include the sub-command name.
tabComplete(sender, args)Suggestions for the arguments after the name. args again excludes the name.

CommandRouter

MethodMeaning
new CommandRouter(JavaPlugin plugin, String rootLabel)rootLabel is used in the usage message, without a slash.
register(SubCommand)Adds a sub-command. Registering a second one with the same name replaces the first.
getSubCommands()An unmodifiable view, in registration order. Useful for building a help command.
dispatch(CommandSender, String[] args)Routes args[0]. Always returns true.
tabComplete(CommandSender, String[] args)Completes sub-command names for the first argument, then delegates to the sub-command.

Example

public final class GoCommand implements SubCommand {

private final WaypointService waypoints;

public GoCommand(WaypointService waypoints) {
this.waypoints = waypoints;
}

@Override
public String getName() {
return "go";
}

@Override
public String getPermission() {
return "rally.host";
}

@Override
public void execute(CommandSender sender, String[] args) {
if (args.length == 0) {
sender.sendMessage("Usage: /rally go <waypoint>");
return;
}
waypoints.rallyTo(sender, args[0]);
}

@Override
public List<String> tabComplete(CommandSender sender, String[] args) {
return args.length == 1 ? waypoints.names(args[0]) : List.of();
}
}

Wire it in onEnable:

CommandRouter router = new CommandRouter(this, "rally");
router.register(new GoCommand(waypoints));
router.register(new OptInCommand(optIns));

PluginCommand command = getCommand("rally");
command.setExecutor((sender, cmd, label, args) -> router.dispatch(sender, args));
command.setTabCompleter((sender, cmd, alias, args) -> router.tabComplete(sender, args));

The root command still needs declaring in plugin.yml.

What dispatch does

InputResult
No argumentsSends Usage: /<root> <sub1|sub2|...> in red, listing every registered name.
An unknown first argumentSends Unknown sub-command: <arg> in red.
A sub-command whose permission the sender lacksSends You do not have permission to do that. in red. execute is not called.
Anything elseCalls execute with the remaining arguments.

Tab completion

  • With zero or one argument typed, suggests every sub-command name starting with what has been typed so far.
  • Sub-command names are suggested whether or not the sender holds their permission. If you want to hide staff commands, filter in a wrapper around tabComplete.
  • Past the first argument, it calls the matching sub-command's tabComplete, or returns nothing for an unknown name.

Behaviour to know about

  • The three router messages above are hard-coded English with legacy colour codes. They do not go through MessageService, so they cannot be translated or restyled. If that matters, check the arguments yourself before calling dispatch.
  • Names are stored lower-case, so return a lower-case name from getName() to keep usage output tidy.
  • The router never throws for bad input. An exception thrown from inside your execute propagates as normal, and Paper reports it as an internal command error.