Unreleased version v6.3.0-SNAPSHOT. This page describes the alpha branch and may change at any time; it is not part of any released version.

commit 49619c6 · 2026-09-13 12:23 UTC

Skip to content

Command Executor

In traditional Bukkit plugin development, we usually use the CommandExecutor interface of Bukkit to handle commands.

However, in some cases, we need to determine whether the sender of the command is a player, whether it has certain permissions, and determine the parameters, etc.

If a plugin has multiple commands, then these judgment logic will be repeated in the processing method of each command, such code is very redundant.

In addition, we may also need to handle command errors, output help information, etc.

UltiTools-API offers a more concise way to handle commands by encapsulating the native CommandExecutor interface.

Create a command executor

Starting with v6.2.0, you should inherit the BaseCommandExecutor class and override the handleHelp method. The @CmdTarget and @CmdExecutor annotations here represent the target type and executor information of the command.

Removed in v6.3.0

AbstractCommandExecutor (and the empty AbstractCommendExecutor shim) was deprecated since v6.2.0 and deleted outright in v6.3.0. Use BaseCommandExecutor instead — same annotation-driven features, plus a pluggable validation chain and custom type parsers. See the migration guide in COMPATIBILITY.md.

/cmd help is gated the same as any other invocation

Before v6.3.0, the help subcommand ran ahead of the validator chain, so handleHelp could still run for a console sender on a @CmdTarget(PLAYER) command, or for a sender missing the required permission. As of v6.3.0, handleGatedHelp runs SenderTypeValidator and PermissionValidator first, so a wrong sender type or a missing permission is refused before your handleHelp implementation ever runs.

java
package com.ultikits.docs.command;

import com.ultikits.ultitools.abstracts.command.BaseCommandExecutor;
import com.ultikits.ultitools.annotations.command.CmdExecutor;
import com.ultikits.ultitools.annotations.command.CmdTarget;
import org.bukkit.command.CommandSender;

// Command limits executor
@CmdTarget(CmdTarget.CmdTargetType.BOTH)
@CmdExecutor(
        // Command permission (optional)
        permission = "ultikits.example.all",
        // Command description (optional)
        description = "Test command",
        // Command alias
        alias = {"test", "ts"},
        // Whether to register manually (optional)
        manualRegister = false,
        // Whether to require OP permission (optional)
        requireOp = false
)
public class ExampleCommand extends BaseCommandExecutor {

    @Override
    protected void handleHelp(CommandSender sender) {
        // Send help message to command sender
    }
}

You have completed an empty command executor that does nothing! The @CmdTarget and @CmdExecutor annotations here represent the sender type and executor information of the command. We will introduce these two annotations in detail in the next section.

Register command

The six-parameter connector constructor is marked for removal

The example below calls the six-parameter UltiToolsPlugin constructor, which carries @Deprecated(since = "6.0.8", forRemoval = true) and hardcodes the resource folder path, so javac reports a removal warning on every build. Move the integration to the External Plugin API and call UltiToolsAPI.connect from your own JavaPlugin, or keep the connector and call the seven-parameter constructor passing resourceFolderPath yourself: both are supported on v6.2.5. The replacement signature for connectors is still being decided in issue #217, and the removal itself is tracked in issue #213.

The same as spigot development, with the executor, you need to register it. We can use the getCommandManager().register() method to register the command in the registerSelf method.

If your module has a large number of command executors and you don't want to register them manually, you can also use the automatic registration provided by UltiTools, for details, please refer to this article.

java
package com.ultikits.docs.command;

import com.ultikits.ultitools.abstracts.UltiToolsPlugin;

import java.io.IOException;
import java.util.List;

public class UltiToolsConnector extends UltiToolsPlugin {

    public UltiToolsConnector(String pluginName, String version, List<String> authors, List<String> loadAfter, int minUltiToolsVersion, String mainClass) {
        super(pluginName, version, authors, loadAfter, minUltiToolsVersion, mainClass);
    }

    @Override
    public boolean registerSelf() throws IOException {
        // register command
        getCommandManager().register(this, ExampleCommand.class);
        return true;
    }

    @Override
    public void unregisterSelf() {

    }

    @Override
    public void reloadSelf() {
        super.reloadSelf();
    }
}

Mapping-based command executor

Quick start

Assuming that your plugin has a function to set the teleport point, you want the player to enter a command with the teleport point name, so as to set up a teleport point.

Then this command should look like this: /point add name

If you use the traditional method, you need to judge the legality of the parameter input, the sender and permissions, etc. If there are other functions, you also need to write a lot of switch ... case and if ... else statements, crazy nesting.

However, with UltiTools, you only need to write the main logic, and the rest will be handled by UltiTools.

First, you need to create an executor class that inherits BaseCommandExecutor.

Then create a method named addPoint and add the parameters you want:

java
public void addPoint(@CmdSender Player player, String name) {
  ...
}

Yes, each of your functions uses a separate function without extra judgment.

If you want to get the Player object instead of the CommandSender object, then you get the Player, you don't need to judge and convert at all. You only need to add the @CmdSender annotation in front of the parameter you want to get the sender object.

Then, you only need to add the @CmdMapping annotation so that UltiTools can match your method according to the input command:

java

@CmdMapping(format = "add <name>")
public void addPoint(@CmdSender Player player, String name) {
  ...
}

Finally, use @CmdParam to bind command parameters:

java

@CmdMapping(format = "add <name>")
public void addPoint(@CmdSender Player player, @CmdParam("name") String name) {
  ...
}

Till now, you only need to register the command executor to complete all the work.

Tab completion

Tab completion is wired into BaseCommandExecutor

As of v6.3.0, suggest(Player, Command, String[]) resolves the first-argument literal from the @CmdMapping format list and the suggest attribute of @CmdParam for parameter slots, through the shared commands/tabcomplete/ dispatch — the sections below describe the full resolution order. You can still override suggest(...) yourself if you want different behaviour; the method remains protected.

Need Tab suggestion for each command parameter, but don't want to write a lot of code?

It is a disaster to generate a completion list by judging the length of each command and the previous parameters.

Now you only need to write a method for each parameter to return a completion list! This method can be reused, and all the complicated parameter quantity judgments are left to UltiTools to complete.

What you need to do is just add the suggest attribute in the @CmdParam annotation and specify a method name.

java

@CmdMapping(format = "add <name>")
public void addPoint(@CmdSender Player player, @CmdParam(value = "name", suggest = "listName") String name) {
  ...
}

public List<String> listName(Player player, Command command, String[] args) {
  ...
}

UltiTools will first search for matching method names in the current class and try to call this method.

Your method can contain up to three parameters, corresponding to the types Player, Command and String[]. You can choose any amount or order of parameters, but the type can only be these three types, one parameter for each type.

Player represents the player who sent the command, Command represents the current command, and String[] represents the current parameters of the current command.

Your method needs to return a value of type List<String>, and UltiTools will return this value as a completion list to the player.

Built-in completers (@key notation)

As of v6.3.0, a suggest value starting with @ resolves through a registered completer instead of a method name — no method to write at all:

java
@CmdMapping(format = "tp <target>")
public void tp(@CmdSender Player player, @CmdParam(value = "target", suggest = "@players") String target) {
  ...
}

Four built-in keys are available: @players (online players), @worlds (loaded worlds), @materials (also @blocks/@items), and @boolean (also @toggle). A module can also register its own key at runtime via TabCompletionManager.register(String, TabCompleter).

@ is not a legal Java identifier start, so this notation can never collide with a method-name value — every example on this page using a plain method name needs no change. An unknown @key refuses the declaring module to load, naming the class, the method and the key; it does not fall through to the plain-string prompt behaviour described next.

TIP

If you just want to return a simple prompt string, then you only need to write the string you want in the suggest field. The string here also supports internationalization.

java

@CmdMapping(format = "add <name>")
public void addPoint(@CmdSender Player player,
                     @CmdParam(value = "name", suggest = "[name]") String name) {
  ...
}

TIP

If you are not satisfied with the completion list generated by UltiTools, you can override the suggest method to generate the completion list yourself.

java

@Override
protected List<String> suggest(Player player, Command command, String[] strings) {
    ...
}

@CmdSuggest

@CmdSuggest is read on BaseCommandExecutor

As of v6.3.0, the class in the example below is read through the shared commands/tabcomplete/ dispatch, so the methods in PointSuggest are looked up and called exactly as shown.

If you want a completion method to be shared with other command classes, you can create a class and write methods which you want to reuse in other class.

Add the @CmdSuggest annotation to the class which need to use suggestion method, and specify the suggestion class.

java

@CmdSuggest({PointSuggest.class})
public class PointCommand extends BaseCommandExecutor {

    @CmdMapping(format = "add <name>")
    public void addPoint(@CmdSender Player player, @CmdParam(value = "name", suggest = "listName") String name) {
        ...
    }
}
java
public class PointSuggest {
    public List<String> listName(Player player, Command command, String[] args) {
        ...
    }
}

Parameters

Command without Parameters

If a command does not require any parameters, simply leave the format value empty.

java
@CmdMapping(format="")

This type of command can have at most one occurrence.

Variable Parameters

For the last parameter in a method, you can use an array type by adding ... to the last parameter in the format. Here's an example:

java
@CmdMapping(format = "add <name...>")
public void addPoint(@CmdSender Player player, @CmdParam("name") String[] name) {
  ...
}

In this example, when a player enters /somecmd add aa bb cc, the name will be ['aa', 'bb', 'cc'].

Type Parsing

Before passing parameters to a method, UltiTools converts the command's variable parameters based on the types required by the method.

All parsers are stored in a map called parsers, and you can use getParser() to access it.

For some types, BaseCommandExecutor provides default parsers via TypeParserRegistry.getInstance() (including base types and arrays):

  • String (Java built-in)
  • Float (Java built-in)
  • Double (Java built-in)
  • Integer (Java built-in)
  • Short (Java built-in)
  • Byte (Java built-in)
  • Long (Java built-in)
  • OfflinePlayer (Bukkit API)
  • Player (Bukkit API)
  • Material (Bukkit API)
  • UUID (Java built-in)
  • Boolean (Java built-in)

If you want to use a custom parser, you need to create a method that can be used with the Function interface.

Supported parser types are <String, ?>, meaning the method has exactly one parameter of type String and returns a value of any type.

java
public static SomeType toSomeType(String s) {
  //do something...
  return result;
}

Permission

Method permission

If you need to specify permissions for a method, you need to add the permission attribute in the @CmdMapping annotation.

java
@CmdMapping(..., permission = "point.set.add")

TIP

The permissions from @CmdExecutor and @CmdMapping are additive — both are checked independently. The player must have both the class-level @CmdExecutor(permission=...) and the method-level @CmdMapping(permission=...) to execute the command.

OP Required

If you want all methods to be executed by OP only, you need to set the requireOp attribute in @CmdExecutor to true

java
@CmdExecutor(..., requireOp = true)

If you want a method to be executed by OP only, you need to set the requireOp attribute in @CmdMapping to true

java
@CmdMapping(..., requireOp = true)

Sender Limitation

If you want to specify the sender for all methods, you need to add the @CmdTarget annotation in front of your class.

If you want to specify the sender for a method, just add it in front of the method.

java
@CmdTarget(CmdTarget.CmdTargetType.BOTH)

The method-level annotation replaces the class-level one

A method-level @CmdTarget overrides the class-level one entirely, it does not require both to be satisfied. A class annotated PLAYER with a method annotated BOTH lets the console execute that method. Restoring the intersection semantics (narrowing, not overriding) is proposed in issue #320.

Asynchronous Execution

If a command needs to execute a task that takes a long time, you need to add @RunAsync in front of the corresponding

java

@CmdMapping(format = "list")
@RunAsync
public void listPoint(@CmdSender Player player) {
    //do query
}

This will create a new asynchronous thread to execute the method, avoiding blocking in the Bukkit main thread.

Since the Bukkit API does not allow asynchronous calls, if you need to call the Bukkit API, you need to create a synchronous task:

java

@CmdMapping(format = "list")
@RunAsync
public void listPoint(@CmdSender Player player) {
    //do query
    new BukkitRunnable() {
        @Override
        public void run() {
            //call bukkit api
        }
    }.runTask(PluginMain.getInstance());
}

An @RunAsync body must not touch world, entity, block or chunk state directly.

Asynchrony here is reserved for pure-CPU or I/O work. Any world, entity, block or chunk access an async body needs must be scheduled back onto the main thread as a synchronous task, BukkitRunnable#runTask(...) as in the example above or any other synchronous scheduler method such as runTaskLater(...); the annotation never grants safe access to those APIs by itself. A handler whose body consists only of such state access has no reason to carry the annotation at all.

An unannotated command body is not executed inline on the calling thread: the framework already defers it by one tick through runTask(). That deferral only changes when the body starts, not whether it can block the server: once the tick arrives, the whole body (including any expensive CPU or I/O work inside it) still runs synchronously on the main thread. Removing @RunAsync restores safe dispatch only for a handler that was never doing expensive work in the first place; a handler that mixes expensive work with Bukkit access keeps the expensive part off-thread and schedules only the Bukkit-touching part back onto the main thread through runTask(...), the pattern shown above, rather than simply removing the annotation. Paper also does not guard every world, entity, block or chunk access from an async body: its asynchronous-operation checks cover a limited set of unsafe operations, so breaking the rule above can leave a handler running on unsafe or inconsistent state instead of crashing outright, and the absence of an exception is not evidence the code is safe. @RunAsync remains the right tool for a body that genuinely belongs off-thread and needs nothing more; when that body also needs a processing message, a timeout, or configurable behaviour around the boundary, use @AsyncCommand instead.

Command cooldown

If you don't want a command to be executed in large quantities and consume server resources, then you can add @CmdCD in front of the corresponding method:

java
@CmdCD(60)

Parameter type is integer, in second.

If the command is executed before the cooldown ends, the message 操作频繁,请 %d 秒后再试 will be sent, with the remaining seconds substituted for %d. As of v6.3.0, en.json carries this parameterized key, so English-locale servers see the translated Frequent operations, please try again in %d seconds instead of the raw Chinese string.

This restriction only takes effect on players.

The cooldown applies after every invocation attempt, not only after a successful one. A mapped method that throws still starts the cooldown, exactly as if it had returned normally — the validator does not distinguish success from failure. This is intentional: a command that errors out is still a server-resource cost, and a caller retrying an erroring command in a tight loop is exactly the pattern the cooldown exists to prevent.

A @CmdCD your validator chain cannot enforce now refuses to load

As of v6.3.0, a class or method carrying @CmdCD whose validator chain has no CooldownValidator — most commonly a custom ValidatorChain that omits it, see Creating Custom Validators below — is refused at plugin load, naming the offending class and method. This closes the gap where the annotation looked declared but enforced nothing.

Execution lock

If you want a command to be executed only one by one, you can add @UsageLimit in front of the corresponding method:

java
@UsageLimit(ContainConsole = false, value = LimitType.SENDER)

ContainConsole is whether the restriction is applied to the console, and value is the restriction type. As of v6.3.0, ContainConsole defaults to true — a console sender is now subject to a limit unless a mapping opts out explicitly with ContainConsole = false as shown above.

Available types are:

  • LimitType.SENDER limits that each sender can only have one command of this type executed at a time
  • LimitType.ALL limits that only one command of this type can be executed in the whole server
  • LimitType.NONE no limit

Under the LimitType.SENDER strategy, the player will receive a prompt: Please wait for last Command Processing!

Under the LimitType.ALL strategy, the player will receive a prompt: Please wait for last Command Processing which sent by other players!

@UsageLimit now genuinely serialises

As of v6.3.0, acquisition is the gate: the lock is taken inside validation itself, so a blocked sender's invocation is rejected before the method runs, and an ALL-scope lock is released only by the sender who acquired it — a different sender's completion can no longer free it. Like @CmdCD above, a @UsageLimit(SENDER|ALL) whose chain has no UsageLockValidator refuses to load, naming the offending class and method; LimitType.NONE is exempt.

Command Context

The CommandContext is an immutable object that encapsulates all information about a command invocation. It is passed to validators and is useful for accessing command metadata during execution.

Accessing Context Information

java
// Check if sender is a player
boolean isPlayer = context.isPlayer();

// Get the player (returns null if sender is not a player)
Player player = context.getPlayer();

// Get the raw command sender
CommandSender sender = context.getSender();

// Get the command and its alias
Command command = context.getCommand();
String alias = context.getAlias();

// Get raw arguments
String[] args = context.getRawArgs();
int argCount = context.getArgCount();
String firstArg = context.getArg(0);

// Get parsed parameters by name
String[] nameValues = context.getParam("name");
String singleValue = context.getParamValue("name");

// Get the matched method and format
Method method = context.getMatchedMethod();
String format = context.getMatchedFormat();

// Get command invocation timestamp
long timestamp = context.getTimestamp();

Command Validation Chain

The validation chain implements the Chain of Responsibility pattern, allowing you to compose multiple validators that execute in order. Built-in validators handle common requirements like permissions, sender type, cooldowns, and execution locks.

Built-in Validators

SenderTypeValidator

Validates that the command sender matches the expected target type (player, console, or both):

java
package com.ultikits.docs.command;

import com.ultikits.ultitools.abstracts.command.BaseCommandExecutor;
import com.ultikits.ultitools.annotations.command.CmdExecutor;
import com.ultikits.ultitools.annotations.command.CmdTarget;
import org.bukkit.command.CommandSender;

@CmdTarget(CmdTarget.CmdTargetType.PLAYER)
@CmdExecutor(alias = {"mycmd"})
public class PlayerOnlyCommand extends BaseCommandExecutor {
    // Automatically rejects console users

    @Override
    protected void handleHelp(CommandSender sender) {
        sender.sendMessage("/mycmd");
    }
}

PermissionValidator

Validates that the sender has required permissions:

java
@CmdExecutor(
    alias = {"admin"},
    permission = "myadmin.use",  // Base permission for all commands
    requireOp = false
)
@CmdMapping(format = "reload", permission = "myadmin.reload")  // Method-specific permission
public void reload(@CmdSender CommandSender sender) {
    // Only users with "myadmin.reload" can execute this
}

CooldownValidator

Manages per-player command cooldowns using @CmdCD:

java
@CmdMapping(format = "expensive")
@CmdCD(30)  // 30 second cooldown
public void expensiveOperation(@CmdSender Player player) {
    // Performs expensive operation
    // Player must wait 30 seconds before executing again
}

Access cooldown state programmatically:

java
public void checkCooldown(UUID playerId, String methodKey) {
    long remaining = getCooldownValidator().getRemainingCooldown(playerId, methodKey);
    if (remaining > 0) {
        // Player is on cooldown
    }
}

The cooldown validator is obtained through getCooldownValidator(), an instance field on BaseCommandExecutor. Each command executor holds its own instance, so this call only reports cooldown state for the current executor.

UsageLockValidator

Prevents concurrent execution using @UsageLimit:

java
@CmdMapping(format = "backup")
@UsageLimit(value = UsageLimit.LimitType.ALL)  // Only one per server
public void backup(@CmdSender CommandSender sender) {
    // Only one player can run this at a time
}

@CmdMapping(format = "download")
@UsageLimit(value = UsageLimit.LimitType.SENDER)  // One per player
public void download(@CmdSender Player player) {
    // Each player can only run one at a time
}

Creating Custom Validators

Implement CommandValidator to create custom validation logic:

java
package com.ultikits.docs.command;

import com.ultikits.ultitools.abstracts.command.CommandContext;
import com.ultikits.ultitools.abstracts.command.validation.CommandValidator;
import org.bukkit.entity.Player;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class WorldRestrictionValidator implements CommandValidator {

    private final Set<String> allowedWorlds = new HashSet<>();

    public WorldRestrictionValidator(String... worlds) {
        allowedWorlds.addAll(Arrays.asList(worlds));
    }

    @Override
    public ValidationResult validate(CommandContext context) {
        if (!context.isPlayer()) {
            return ValidationResult.success();
        }

        Player player = context.getPlayer();
        if (!allowedWorlds.contains(player.getWorld().getName())) {
            return ValidationResult.failure(
                "You can only use this command in: " + String.join(", ", allowedWorlds),
                "command.error.wrong_world"
            );
        }

        return ValidationResult.success();
    }

    @Override
    public int getOrder() {
        return 400;  // Execute after permission validators
    }

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

Register the validator in your command executor:

java
package com.ultikits.docs.command;

import com.ultikits.ultitools.abstracts.command.BaseCommandExecutor;
import org.bukkit.command.CommandSender;

public class ValidatorCommand extends BaseCommandExecutor {

    public ValidatorCommand() {
        super();
        addValidator(new WorldRestrictionValidator("world", "world_nether"));
    }

    @Override
    protected void handleHelp(CommandSender sender) { }
}

A custom chain replaces all four default validators

Passing a ValidatorChain to super(...) skips createDefaultValidatorChain(), so SenderTypeValidator (the class-level @CmdTarget), PermissionValidator (requireOp and the method-level @CmdMapping permissions, though the class-level permission is still enforced by Bukkit), UsageLockValidator (@UsageLimit) and CooldownValidator (@CmdCD) are all absent from the chain unless you add them yourself. Use the form shown just above instead: call super() and register your validator with addValidator(...), which keeps the four defaults and orders yours by getOrder(). As of v6.3.0, a class or method whose @CmdCD/@UsageLimit has no matching validator in the custom chain is refused at plugin load rather than silently recording state without enforcing anything — see the tips under Command cooldown and Execution lock above.

Or use a custom validator chain:

java
package com.ultikits.docs.command;

import com.ultikits.ultitools.abstracts.command.BaseCommandExecutor;
import com.ultikits.ultitools.abstracts.command.validation.ValidatorChain;
import com.ultikits.ultitools.abstracts.command.validation.validators.PermissionValidator;
import com.ultikits.ultitools.abstracts.command.validation.validators.SenderTypeValidator;
import org.bukkit.command.CommandSender;

public class ChainCommand extends BaseCommandExecutor {

    // Build the chain inside the constructor. A chain assigned to a local
    // variable outside the class is not reachable from super(...).
    public ChainCommand() {
        super(ValidatorChain.builder()
            .add(SenderTypeValidator.fromAnnotation(null))
            .add(new PermissionValidator("myadmin.use", false))
            .add(new WorldRestrictionValidator("world"))
            .build());
    }

    @Override
    protected void handleHelp(CommandSender sender) { }
}

Validator Execution Order

Validators execute in order by their getOrder() value (lower values first):

  1. 100 - SenderTypeValidator (ensure right user type)
  2. 200 - PermissionValidator (check permissions)
  3. 250 - UsageLockValidator (prevent concurrent execution)
  4. 300 - CooldownValidator (check cooldown state)
  5. 400+ - Custom validators

Async Commands

Use @AsyncCommand to execute commands asynchronously without blocking the server thread. It offers more configuration options than @RunAsync:

java
@CmdMapping(format = "backup")
@AsyncCommand
public void backupWorld(@CmdSender Player player) {
    // Runs asynchronously - safe for I/O operations
    performBackupLogic();

    // Sync back to main thread for Bukkit operations
    Bukkit.getScheduler().runTask(UltiTools.getInstance(), () -> {
        player.sendMessage("Backup completed!");
    });
}

Async Command Options

timeout on @AsyncCommand is now honoured

As of v6.3.0, timeout() is a deadline on how long the framework waits, not a cancellation of the method body: when the configured duration elapses with the body still running, the framework stops waiting and sends the sender one timeout message, but the body itself is never interrupted and keeps running to completion. timeout = 0 disables the watcher entirely — the framework waits indefinitely and never reports a timeout.

java
@AsyncCommand(
    showProcessing = true,                      // Show "Processing..." message
    processingMessageKey = "command.backup.processing",  // Custom i18n message
    timeout = 60                                // 60 second timeout (0 = no timeout, defaults to 30 if omitted)
)
@CmdMapping(format = "backup")
public void backupWorld(@CmdSender Player player) {
    // Configuration above:
    // - Shows "处理中..." while executing
    // - Uses custom i18n key instead of default
}

Custom Type Parsers

Type parsers convert command argument strings into the types your methods require. UltiTools provides built-in parsers for primitive types, Bukkit entities, and arrays.

Built-in Parsers

  • Primitive types: String, Integer, Double, Float, Long, Short, Byte, Boolean
  • Bukkit entities: Player, OfflinePlayer, Material, World
  • Other types: UUID, Location, GameMode, Enchantment
  • Arrays: All types above support array syntax

Creating Custom Parsers

Implement TypeParser<T>:

java
package com.ultikits.docs.command;

import com.ultikits.ultitools.abstracts.command.parser.TypeParseException;
import com.ultikits.ultitools.abstracts.command.parser.TypeParser;
import org.bukkit.Color;

import java.util.Arrays;
import java.util.List;

public class ColorParser implements TypeParser<Color> {

    @Override
    public Class<Color> getPrimaryType() {
        return Color.class;
    }

    @Override
    public List<Class<?>> getSupportedTypes() {
        return Arrays.asList(Color.class, Color[].class);
    }

    @Override
    public Color parse(String value) throws TypeParseException {
        try {
            // Parse hex color like "FF0000"
            int rgb = Integer.parseInt(value, 16);
            return Color.fromRGB(rgb);
        } catch (IllegalArgumentException e) {
            // Catch IllegalArgumentException, not NumberFormatException. Two
            // different failures land here and only one of them is a format
            // error: Integer.parseInt throws NumberFormatException on bad hex,
            // while Color.fromRGB throws plain IllegalArgumentException for a
            // value outside the low 24 bits (e.g. "1000000" parses fine, then
            // gets rejected). NumberFormatException extends
            // IllegalArgumentException, so catching the supertype covers both.
            // Catching only the subtype lets the range error escape as an
            // undeclared runtime exception instead of a TypeParseException.
            throw new TypeParseException(value, Color.class,
                "Invalid color. Use 6-digit hexadecimal RGB (e.g., FF0000)", e);
        }
    }

    @Override
    public int getPriority() {
        return 0;
    }
}

Register the parser:

java
@Autowired
private UltiToolsPlugin plugin;

@PostConstruct
public void init() {
    TypeParserRegistry.getInstance().register(new ColorParser());
}

Use in your command:

java
@CmdMapping(format = "setcolor <color>")
public void setColor(@CmdSender Player player, @CmdParam("color") Color color) {
    // color is parsed automatically
}

Advanced parser with array support:

java
package com.ultikits.docs.command;

import com.ultikits.ultitools.abstracts.command.parser.TypeParseException;
import com.ultikits.ultitools.abstracts.command.parser.TypeParser;

import java.util.Arrays;
import java.util.List;

public class RangeParser implements TypeParser<IntRange> {

    @Override
    public Class<IntRange> getPrimaryType() {
        return IntRange.class;
    }

    @Override
    public List<Class<?>> getSupportedTypes() {
        return Arrays.asList(IntRange.class, IntRange[].class);
    }

    @Override
    public IntRange parse(String value) throws TypeParseException {
        String[] parts = value.split("-");
        if (parts.length != 2) {
            throw new TypeParseException(value, IntRange.class,
                "Range format: min-max (e.g., 1-100)");
        }

        try {
            int min = Integer.parseInt(parts[0]);
            int max = Integer.parseInt(parts[1]);
            return new IntRange(min, max);
        } catch (NumberFormatException e) {
            throw new TypeParseException(value, IntRange.class,
                "Range bounds must be integers", e);
        }
    }
}
java
// Usage
@CmdMapping(format = "random <range>")
public void randomNumber(@CmdSender Player player,
                         @CmdParam("range") IntRange range) {
    int value = ThreadLocalRandom.current().nextInt(range.min, range.max + 1);
    player.sendMessage("Random: " + value);
}

Vanilla Bukkit Command Executor Wrapper

Player Command

If you want a command to be executed only in the game (executed by the player), you can inherit the AbstractPlayerCommandExecutor class and override the onPlayerCommand method.

java
package com.ultikits.docs.command;

import com.ultikits.ultitools.abstracts.AbstractPlayerCommandExecutor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;

public class PlayerCommandExample extends AbstractPlayerCommandExecutor {
    @Override
    protected boolean onPlayerCommand(Command command, String[] strings, Player player) {
        // your code
        return true;
    }

    // Required: sendHelpMessage is abstract in AbstractCommand.
    @Override
    protected void sendHelpMessage(CommandSender sender) {
        // send help message
    }
}

Except for the Player type parameter, this method is the same as the CommandExecutor#onCommand method.

If you try to execute this command in the console, you will receive an error message: This command can only be performed in GAME!

If you want this command to use Tab completion, please see the next section.

Command Completion

From Minecraft 1.13, the Bukkit API provides a new TabCompleter interface for command completion.

UltiTools has encapsulated this interface to provide a more concise way of command completion.

You need to inherit the AbstractTabExecutor class and override the onPlayerTabComplete method.

java

@Override
protected List<String> onPlayerTabComplete(Command command, String[] strings, Player player) {
    // your code
    return null;
}

Except for the Player type parameter, this method is the same as the TabCompleter#onTabComplete method.

The rest of the usage is the same as the AbstractPlayerCommandExecutor class.

Console Command

If you want a command to be executed only in the console, you can inherit the AbstractConsoleCommandExecutor class and override the onConsoleCommand method.

java
package com.ultikits.docs.command;

import com.ultikits.ultitools.abstracts.AbstractConsoleCommandExecutor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;

public class ConsoleCommandExample extends AbstractConsoleCommandExecutor {
    @Override
    protected boolean onConsoleCommand(CommandSender commandSender, Command command, String[] strings) {
        // your code
        return true;
    }

    // Required: sendHelpMessage is abstract in AbstractCommand.
    @Override
    protected void sendHelpMessage(CommandSender sender) {
        // send help message
    }
}

This method is the same as the CommandExecutor#onCommand method.

If you try to execute this command in the game, you will receive an error message: This command can only be performed in CONSOLE!

Help Message

All three classes above inherit sendHelpMessage from AbstractCommand, where it is declared protected abstract. It is not provided for you — every subclass has to implement it, which is why both examples above override it:

java
@Override
protected void sendHelpMessage(CommandSender sender) {
    // send help message
}

When sending the /somecommand help command, this method will be called.

Error Message

You may find that the onCommand method of the three classes above returns a boolean type value.

It is the same as the native CommandExecutor interface, this value is used to indicate whether the command was executed successfully.

When the command execution returns false, the command sender will be automatically prompted with an error message.

Contributors

No contributors

Released under the MIT License.