Command Guide
Nukkit-MOT exposes more than one command API. The right choice depends on whether you only need a standard plugin command, or typed parsing, overloads, and selectors.
This page is written against the current Nukkit-MOT source, especially PluginManager, PluginCommand, PluginBase, SimpleCommandMap, Command, ParamTree, and EntitySelectorAPI.
Choose a Registration Path
| Path | Good for | Registration point |
|---|---|---|
plugin.yml + onCommand(...) | Standard plugin commands | plugin.yml |
plugin.yml + setExecutor(...) | Keeping command logic out of the main plugin class | plugin.yml + onEnable() |
registerSimpleCommands(this) | Small raw-string utility or admin commands | onEnable() |
Custom Command + enableParamTree() | Typed parsing, overloads, and target selectors | onEnable() |
1. plugin.yml + onCommand(...)
During plugin loading, PluginManager.parseYamlCommands() reads the commands section and creates a PluginCommand for each entry. The default executor of PluginCommand is the plugin itself, so the normal starting point is to override onCommand(...) in your PluginBase.
commands:
hello:
description: Send a hello message
usage: "/hello <name>"
aliases: ["hi"]
permission: example.command.hello
permission-message: "You need <permission> to use /hello"
package com.example.commanddemo;
import cn.nukkit.command.Command;
import cn.nukkit.command.CommandSender;
import cn.nukkit.plugin.PluginBase;
public final class CommandDemoPlugin extends PluginBase {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!command.getName().equalsIgnoreCase("hello")) {
return false;
}
if (args.length != 1) {
return false;
}
sender.sendMessage("Hello, " + args[0] + "!");
return true;
}
}
If onCommand(...) returns false and the command has a non-empty usage, Nukkit-MOT sends the usage message automatically.
2. Move Logic into a Dedicated CommandExecutor
If your plugin has multiple commands, keep the main class thin and attach an executor in onEnable(). PluginBase.getCommand(name) only returns commands declared in the current plugin's plugin.yml.
package com.example.commanddemo;
import cn.nukkit.command.PluginCommand;
import cn.nukkit.plugin.PluginBase;
public final class CommandDemoPlugin extends PluginBase {
@Override
@SuppressWarnings("unchecked")
public void onEnable() {
PluginCommand<CommandDemoPlugin> hello = (PluginCommand<CommandDemoPlugin>) this.getCommand("hello");
if (hello == null) {
throw new IllegalStateException("Command 'hello' is missing from plugin.yml");
}
hello.setExecutor(new HelloCommandExecutor());
}
}
package com.example.commanddemo;
import cn.nukkit.command.Command;
import cn.nukkit.command.CommandExecutor;
import cn.nukkit.command.CommandSender;
public final class HelloCommandExecutor implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length != 1) {
return false;
}
sender.sendMessage("Hello, " + args[0] + "!");
return true;
}
}
This route still uses the plugin.yml metadata for description, aliases, permission, and usage.
3. Annotation-based Simple Commands
SimpleCommandMap.registerSimpleCommands(Object object) scans methods annotated with @cn.nukkit.command.simple.Command and registers them as SimpleCommand instances. This path does not depend on plugin.yml.
package com.example.commanddemo;
import cn.nukkit.Player;
import cn.nukkit.command.CommandSender;
import cn.nukkit.command.data.CommandParamType;
import cn.nukkit.command.simple.Arguments;
import cn.nukkit.command.simple.CommandParameters;
import cn.nukkit.command.simple.CommandPermission;
import cn.nukkit.command.simple.ForbidConsole;
import cn.nukkit.command.simple.Parameter;
import cn.nukkit.command.simple.Parameters;
import cn.nukkit.plugin.PluginBase;
public final class CommandDemoPlugin extends PluginBase {
@Override
public void onEnable() {
this.getServer().getCommandMap().registerSimpleCommands(this);
}
@cn.nukkit.command.simple.Command(
name = "whereis",
description = "Show a player's position",
usageMessage = "/whereis <player>",
aliases = {"locateplayer"}
)
@Arguments(min = 1, max = 1)
@CommandPermission("example.command.whereis")
@ForbidConsole
@CommandParameters(parameters = {
@Parameters(name = "default", parameters = {
@Parameter(name = "target", type = CommandParamType.TARGET)
})
})
public boolean whereIsCommand(CommandSender sender, String commandLabel, String[] args) {
Player target = this.getServer().getPlayerExact(args[0]);
if (target == null) {
sender.sendMessage("Player not found.");
return true;
}
sender.sendMessage(
target.getName() + " is at "
+ target.getFloorX() + ", "
+ target.getFloorY() + ", "
+ target.getFloorZ()
);
return true;
}
}
Notes:
- Method signature must be
boolean method(CommandSender sender, String commandLabel, String[] args). @Arguments,@CommandPermission, and@ForbidConsoleare enforced bySimpleCommand.@CommandParametersonly fills command metadata. The method still receives rawString[] args; it does not automatically enableParamTreeor typed server-side parsing.- Avoid reusing the same command name in
plugin.ymlandregisterSimpleCommands(...)unless you intentionally want a collision.
4. Custom Command with ParamTree
Use a real Command subclass when you need:
- typed parameters
- multiple overloads
- target selectors such as
@aand@p - command output through
CommandLogger
plugin.yml and a manually registered custom CommandCommands from plugin.yml are turned into PluginCommand instances by PluginManager.parseYamlCommands() during plugin loading, then registered through SimpleCommandMap.registerAll(...). A custom Command that you register later in onEnable() still goes into the same SimpleCommandMap.knownCommands.
The source logic in SimpleCommandMap.registerAlias(...) is not a simple "last registration wins" rule:
- If your manually registered primary label collides with an existing primary label, the new command does not get the bare command name and falls back to
fallbackPrefix:command - If your manually registered primary label collides with an existing alias, the bare alias entry is rewritten by
knownCommands.put(label, command). Also,registerAlias(...)writesfallbackPrefix:labelfirst, so when the same fallback prefix is involved, the prefixed alias entry can be taken over too
So the issue is not just "overriding". What usually happens is that command entries split: when a primary label collides with another primary label, the bare command name usually still points to the old PluginCommand; when a primary label collides with an alias, that alias entry may be rebound to the new command object. If you are writing a real custom Command, especially one with ParamTree, do not also declare the same entry in plugin.yml.
Register the command manually in onEnable():
package com.example.commanddemo;
import cn.nukkit.plugin.PluginBase;
public final class CommandDemoPlugin extends PluginBase {
@Override
public void onEnable() {
this.getServer().getCommandMap().register("commanddemo", new TargetInfoCommand());
}
}
Then define overloads with commandParameters and call enableParamTree() after the map is ready:
package com.example.commanddemo;
import cn.nukkit.command.Command;
import cn.nukkit.command.CommandSender;
import cn.nukkit.command.data.CommandParamType;
import cn.nukkit.command.data.CommandParameter;
import cn.nukkit.command.tree.ParamList;
import cn.nukkit.command.utils.CommandLogger;
import cn.nukkit.entity.Entity;
import java.util.List;
import java.util.Map;
public final class TargetInfoCommand extends Command {
public TargetInfoCommand() {
super("targetinfo", "Inspect the target selector result", "/targetinfo <target>");
this.setPermission("example.command.targetinfo");
this.commandParameters.clear();
this.commandParameters.put("default", new CommandParameter[]{
CommandParameter.newType("target", CommandParamType.TARGET)
});
this.enableParamTree();
}
@Override
public int execute(CommandSender sender, String commandLabel, Map.Entry<String, ParamList> result, CommandLogger log) {
List<Entity> targets = result.getValue().getResult(0);
if (targets.isEmpty()) {
log.addNoTargetMatch().output();
return 0;
}
Entity first = targets.get(0);
log.addSuccess(
"Matched " + targets.size() + " target(s). First: "
+ first.getName() + " @ "
+ first.getFloorX() + ", "
+ first.getFloorY() + ", "
+ first.getFloorZ()
).output();
return targets.size();
}
}
Important rules:
- Call
enableParamTree()only aftercommandParametersis fully configured. - When a command has a param tree, dispatch no longer calls
execute(CommandSender, String, String[]). Overrideexecute(CommandSender, String, Map.Entry<String, ParamList>, CommandLogger)instead. - If you enable a param tree but do not override the new
execute(...), the command fails and the server logs an error.
Selectors Supported by CommandParamType.TARGET
When an overload uses target-related parameter nodes, the current EntitySelectorAPI accepts:
- selectors:
@a,@e,@p,@r,@s,@initiator - common arguments:
x,y,z,dx,dy,dz,c,r,rm,name,tag,l,lm,m,type,rx,rxm,ry,rym,scores
That means a command like /targetinfo @a[tag=builder,c=3] can be parsed through the same selector system used by built-in commands.
Common Pitfalls
commands:inplugin.ymlonly registers metadata andPluginCommandobjects. It does not implement your command logic by itself.this.getCommand("name") == nullusually means the command is missing from your plugin'splugin.yml, or the command belongs to another plugin.plugin.ymlcommands,registerSimpleCommands(...), and manualregister(...)calls all end up in the sameSimpleCommandMap. If a primary label or alias collides, the bare command name may stay occupied or an alias entry may get rewritten, so the command that runs may not be the one you expected.- Returning
falseis not a generic error-handling strategy. It mainly means "show usage if available". - Annotation-based simple commands are convenient, but they are still raw-argument commands. Use a custom
CommandplusParamTreefor real typed parsing.