Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ProjectUnified/CraftCommand/llms.txt

Use this file to discover all available pages before exploring further.

The Bukkit platform integrates CraftCommand directly with Bukkit’s CommandMap, enabling you to register commands programmatically without the verbose boilerplate of implementing CommandExecutor and TabCompleter by hand. The sender type on this platform is org.bukkit.command.CommandSender, which covers players, the console, and command blocks. The annotation processor generates parsing helpers for common Bukkit types — Player, OfflinePlayer, World, and Location — directly into the wrapper class, so no manual resolver registration is needed.
Commands registered through CommandMap must still have a corresponding entry in your plugin.yml file. The server reads plugin.yml during start-up to build its internal command table. Entries missing from plugin.yml may not be recognised by some server implementations or tab-completion clients.

Maven Setup

1

Add the dependencies

craftcommand-annotations provides the compile-time annotations (@Command, @Subcommand, etc.). craftcommand-bukkit-runtime provides BukkitCommandManager. craftcommand-bukkit-annotations is optional but required if you want to use @Permission.
<dependencies>
    <!-- Core annotations (compile-time only) -->
    <dependency>
        <groupId>io.github.projectunified</groupId>
        <artifactId>craftcommand-annotations</artifactId>
        <version>0.5.1</version>
        <scope>provided</scope>
    </dependency>

    <!-- Bukkit runtime: BukkitCommandManager and built-in type resolvers -->
    <dependency>
        <groupId>io.github.projectunified</groupId>
        <artifactId>craftcommand-bukkit-runtime</artifactId>
        <version>0.5.1</version>
    </dependency>

    <!-- Optional: @Permission annotation support -->
    <dependency>
        <groupId>io.github.projectunified</groupId>
        <artifactId>craftcommand-bukkit-annotations</artifactId>
        <version>0.5.1</version>
        <scope>provided</scope>
    </dependency>
</dependencies>
2

Configure the annotation processor

Add craftcommand-bukkit-processor to <annotationProcessorPaths>. At compile time it generates a ClassName_Executor class that extends Bukkit’s Command, wiring all your annotated methods to the right dispatch and tab-completion paths.
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.15.0</version>
            <configuration>
                <annotationProcessorPaths>
                    <path>
                        <groupId>io.github.projectunified</groupId>
                        <artifactId>craftcommand-bukkit-processor</artifactId>
                        <version>0.5.1</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

Writing a Bukkit Command

The example below is the real TeleportCommand from the CraftCommand Bukkit example module. It demonstrates @Default with an @Optional argument, multiple @Subcommand methods, @Permission on both the class and individual methods, @Suggest for custom completions, @Greedy string capture, nested inner-class subcommands, and a local resolver for Location.
package io.github.projectunified.craftcommand.example.bukkit;

import io.github.projectunified.craftcommand.CommandInfo;
import io.github.projectunified.craftcommand.annotation.*;
import io.github.projectunified.craftcommand.bukkit.BukkitCommandManager;
import io.github.projectunified.craftcommand.bukkit.annotation.Permission;
import io.github.projectunified.craftcommand.validation.annotation.Max;
import io.github.projectunified.craftcommand.validation.annotation.Min;
import io.github.projectunified.craftcommand.validation.annotation.ValidateWith;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;

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

@Command(value = "tp", aliases = {"teleport"}, description = "Comprehensive Teleport Commands")
@Permission("example.tp")
public class TeleportCommand {
    // 1. Suggestion provider from a Field
    public final List<String> modes = Arrays.asList("normal", "silent", "instant");
    private final BukkitCommandManager commandManager;

    public TeleportCommand(BukkitCommandManager commandManager) {
        this.commandManager = commandManager;
    }

    // 2. Suggestion provider from a Method (context-aware, filters players within 50 blocks)
    public List<String> getNearPlayers(Player sender, String[] args, String current) {
        List<String> suggestions = new ArrayList<>();
        String lower = current.toLowerCase();
        for (Player p : Bukkit.getOnlinePlayers()) {
            if (p.getWorld().equals(sender.getWorld()) && p.getLocation().distance(sender.getLocation()) < 50) {
                if (p.getName().toLowerCase().startsWith(lower)) {
                    suggestions.add(p.getName());
                }
            }
        }
        return suggestions;
    }

    // Use Case: @Default command with @Optional object argument
    @Default
    public void execute(Player sender, @Optional Player target) {
        if (target == null) {
            sender.teleport(sender.getWorld().getSpawnLocation());
            sender.sendMessage("Teleported to spawn.");
        } else {
            sender.teleport(target);
            sender.sendMessage("Teleported to " + target.getName());
        }
    }

    // Use Case: @Subcommand with platform-specific @Permission
    @Subcommand(value = "here", aliases = {"h"})
    @Permission("example.tp.here")
    public void teleportHere(Player sender, Player target) {
        target.teleport(sender);
        sender.sendMessage("Teleported " + target.getName() + " to you");
    }

    // Use Case: Subcommand with no arguments
    @Subcommand(value = "all")
    @Permission("example.tp.all")
    public void teleportAll(Player sender) {
        for (Player player : Bukkit.getOnlinePlayers()) {
            if (!player.equals(sender)) {
                player.teleport(sender);
            }
        }
        sender.sendMessage("Teleported all online players to you");
    }

    // Use Case: Resolving complex Location parameter via default global resolver (width = 4)
    @Subcommand("loc")
    public void teleportLocation(Player sender, Location location) {
        sender.teleport(location);
        sender.sendMessage(String.format("Teleported to %.2f, %.2f, %.2f in %s", location.getX(), location.getY(), location.getZ(), location.getWorld().getName()));
    }

    // Local resolver method for Location with optional World parameter
    public Location resolveLocationLocal(Player sender, double x, double y, double z, @Optional World world) {
        World targetWorld = world != null ? world : sender.getWorld();
        return new Location(targetWorld, x, y, z);
    }

    // Use Case: Local parameter resolver with optional arguments (min_width = 3, max_width = 4)
    @Subcommand("loc-local")
    public void teleportLocationLocal(Player sender, @Resolve("resolveLocationLocal") Location location) {
        sender.teleport(location);
        sender.sendMessage(String.format("Teleported (local) to %.2f, %.2f, %.2f in %s", location.getX(), location.getY(), location.getZ(), location.getWorld().getName()));
    }

    // Use Case: Subcommand with @Optional primitive defaults (boolean) and validations (@Min, @Max)
    @Subcommand(value = "player")
    public void tpPlayer(Player sender, Player target, @Min(0) @Max(1) @Optional("0") int level) {
        sender.teleport(target);
        sender.sendMessage("Teleported to " + target.getName() + " with priority level " + level);
    }

    // Use Case: Subcommand with @Greedy and @Name override
    @Subcommand(value = "msg")
    public void sendMessage(Player sender, Player target, @Name("text") @Greedy String message) {
        target.sendMessage(sender.getName() + " says: " + message);
        sender.sendMessage("Message sent to " + target.getName());
    }

    // Use Case: @Suggest referencing a class Field
    @Subcommand(value = "mode")
    public void setMode(CommandSender sender, @Suggest("modes") String mode) {
        sender.sendMessage("Teleport mode set to: " + mode);
    }

    // Use Case: @Suggest referencing a class Method
    @Subcommand(value = "near")
    public void tpNear(Player sender, @Suggest("getNearPlayers") Player target) {
        sender.teleport(target);
        sender.sendMessage("Teleported to nearby player: " + target.getName());
    }

    // Use Case: No @Sender annotation; index 0 parameter acts as the sender resolved/cast automatically
    @Subcommand(value = "info")
    public void getInfo(Player player) {
        player.sendMessage("Your location is: " + player.getLocation());
    }

    // Use Case: @Subcommand with a custom permission message
    @Subcommand("secret")
    @Permission(value = "example.tp.secret", message = "Access denied! The secret TP area is off-limits.")
    public void secretTp(Player sender) {
        sender.sendMessage("Welcome to the secret teleport area.");
    }

    // Custom validation method for coordinate
    public void validateCoordinate(double coord) {
        if (Math.abs(coord) > 30000000) {
            throw new IllegalArgumentException("Coordinate cannot exceed 30,000,000!");
        }
    }

    // Use Case: Custom validation annotation Showcase
    @Subcommand("tp-coord")
    public void tpCoordinate(Player sender, @ValidateWith("validateCoordinate") double x, double y, double z) {
        sender.teleport(new Location(sender.getWorld(), x, y, z));
        sender.sendMessage(String.format("Teleported to custom validated coordinates: %.2f, %.2f, %.2f", x, y, z));
    }

    @Subcommand("help")
    public void getHelp(CommandSender sender) {
        sender.sendMessage("--- Teleport Command Help ---");
        for (CommandInfo info : commandManager.getCommandInfo(this)) {
            String path = String.join(" ", info.getPath());
            String usage = info.getUsage();
            String desc = info.getDescription();
            sender.sendMessage("/" + path + (usage.isEmpty() ? "" : " " + usage) + (desc.isEmpty() ? "" : " - " + desc));
        }
    }

    // Use Case: Nested subcommand class (inner class)
    @Subcommand("admin")
    @Permission("example.tp.admin")
    public class AdminCommands {
        @Default
        public void execute(Player sender) {
            sender.sendMessage("Teleport admin console.");
        }

        @Subcommand("spawn")
        public void setSpawn(Player sender, String worldName) {
            sender.sendMessage("Spawn for world " + worldName + " has been set.");
        }
    }
}

plugin.yml Declaration

Even though CraftCommand registers commands via CommandMap at runtime, Bukkit reads plugin.yml before the plugin enables. At minimum, declare each top-level command label. Aliases listed here also appear in /help output and other server-side tooling.
name: ExamplePlugin
version: 1.0
main: io.github.projectunified.craftcommand.example.bukkit.ExamplePlugin
api-version: 1.20
For richer server-side metadata (description, usage hint, permission), you can expand the commands block:
commands:
  tp:
    description: Comprehensive Teleport Commands
    aliases:
      - teleport
    permission: example.tp
    usage: /<command> [player]
If a command label is absent from plugin.yml, Bukkit may not include it in the global command index built during start-up. Always declare every top-level command name (and its aliases) in plugin.yml, even when using BukkitCommandManager.

Registering in Your Plugin

Create a BukkitCommandManager in onEnable, register your command instances, and call syncCommand() to push the new entries to connected clients. On onDisable, call unregisterAll() to cleanly remove every command this manager registered.
package io.github.projectunified.craftcommand.example.bukkit;

import io.github.projectunified.craftcommand.bukkit.BukkitCommandManager;
import org.bukkit.plugin.java.JavaPlugin;

public class ExamplePlugin extends JavaPlugin {
    private BukkitCommandManager commandManager;

    @Override
    public void onEnable() {
        this.commandManager = new BukkitCommandManager(this);

        // Register the command
        commandManager.register(new TeleportCommand(commandManager));
        commandManager.syncCommand();

        getLogger().info("ExamplePlugin enabled and commands registered!");
    }

    @Override
    public void onDisable() {
        this.commandManager.unregisterAll();
    }
}

Built-in Bukkit Type Resolvers

The craftcommand-bukkit-processor annotation processor generates parsing helper methods directly into the _Executor wrapper class for each command. You do not need to register anything manually — just declare the parameter type and the generated wrapper handles parsing.
Parameter typeResolution logic
PlayerLooks up an online player by name using Bukkit.getPlayer(name). Throws IllegalArgumentException if no online player matches.
OfflinePlayerLooks up an offline player by name using Bukkit.getOfflinePlayer(name). Throws IllegalArgumentException if the player has not played before.
WorldLooks up a loaded world by name using Bukkit.getWorld(name). Throws IllegalArgumentException if no loaded world matches.
LocationParses four consecutive arguments as world x y z. The world argument is resolved the same way as the World resolver.

@Permission Annotation

@Permission is provided by craftcommand-bukkit-annotations and can be placed on a class or an individual method. When the annotation processor generates the _Executor wrapper, it inserts a permission check before the annotated handler is called.
Applying @Permission to the class guards every subcommand in that class under the same permission node.
@Command(value = "tp", aliases = {"teleport"}, description = "Comprehensive Teleport Commands")
@Permission("example.tp")
public class TeleportCommand {
    // All subcommands require example.tp
}
Annotation fields:
FieldTypeDefaultDescription
valueString(required)The Bukkit permission node (e.g. "myplugin.admin").
messageString""Custom message sent when permission is denied. When empty, a default message is used.

BukkitCommandManager API

register(Object commandInstance)

Locates the generated ClassName_Executor wrapper via class name lookup, instantiates it, and registers it to the server’s CommandMap. Throws IllegalArgumentException if no wrapper class is found.

register(Command command)

Registers a raw Bukkit Command object directly to the CommandMap. Useful for hand-crafted commands or commands built by other libraries. Logs a warning and skips if the label is already registered by this manager.

syncCommand()

Calls CraftServer#syncCommands() (if available) to synchronise the server’s command tree with connected Brigadier clients. Call this once after all commands are registered in onEnable.

unregisterAll()

Removes every command registered by this manager from the server’s CommandMap known-commands table and calls Command#unregister. Call in onDisable to avoid stale entries if the plugin is reloaded.

Build docs developers (and LLMs) love