Skip to main content

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.

Source-backed scope

This page is written against the current Nukkit-MOT source, especially PluginManager, PluginCommand, PluginBase, SimpleCommandMap, Command, ParamTree, and EntitySelectorAPI.

Choose a Registration Path

PathGood forRegistration point
plugin.yml + onCommand(...)Standard plugin commandsplugin.yml
plugin.yml + setExecutor(...)Keeping command logic out of the main plugin classplugin.yml + onEnable()
registerSimpleCommands(this)Small raw-string utility or admin commandsonEnable()
Custom Command + enableParamTree()Typed parsing, overloads, and target selectorsonEnable()

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.

plugin.yml
commands:
hello:
description: Send a hello message
usage: "/hello <name>"
aliases: ["hi"]
permission: example.command.hello
permission-message: "You need <permission> to use /hello"
CommandDemoPlugin.java
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.

CommandDemoPlugin.java
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());
}
}
HelloCommandExecutor.java
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.

CommandDemoPlugin.java
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 @ForbidConsole are enforced by SimpleCommand.
  • @CommandParameters only fills command metadata. The method still receives raw String[] args; it does not automatically enable ParamTree or typed server-side parsing.
  • Avoid reusing the same command name in plugin.yml and registerSimpleCommands(...) 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 @a and @p
  • command output through CommandLogger
Do not declare the same command name in both plugin.yml and a manually registered custom Command

Commands 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(...) writes fallbackPrefix:label first, 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():

CommandDemoPlugin.java
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:

TargetInfoCommand.java
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 after commandParameters is fully configured.
  • When a command has a param tree, dispatch no longer calls execute(CommandSender, String, String[]). Override execute(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: in plugin.yml only registers metadata and PluginCommand objects. It does not implement your command logic by itself.
  • this.getCommand("name") == null usually means the command is missing from your plugin's plugin.yml, or the command belongs to another plugin.
  • plugin.yml commands, registerSimpleCommands(...), and manual register(...) calls all end up in the same SimpleCommandMap. 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 false is 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 Command plus ParamTree for real typed parsing.