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.

Paper’s Brigadier API enables client-side command validation, argument type hints in the chat HUD, and rich tab completion that is displayed before the player even presses Tab. CraftCommand’s annotation processor generates complete Brigadier LiteralCommandNode trees at compile time — zero reflection, zero runtime code generation. When the plugin enables, PaperCommandManager hooks into Paper’s LifecycleEvents.COMMANDS event and registers each tree through the official lifecycle API, ensuring correct ordering with other plugins and avoiding the fragile CommandMap manipulation required on older platforms.
Use craftcommand-paper-processor (Brigadier) for the best player experience — players see live argument type hints and structured tab completion in the chat HUD. If you need a simpler setup or are targeting a mixed Bukkit/Paper environment, craftcommand-paper-processor-basic generates a _PaperBasic wrapper that implements Paper’s BasicCommand interface instead, with no Brigadier argument nodes.

Maven Setup

1

Add the dependencies

craftcommand-annotations supplies the compile-time annotations. craftcommand-paper-runtime provides PaperCommandManager, the PaperCommand interface (Brigadier), and the PaperBasicCommand interface (BasicCommand fallback). Shade craftcommand-paper-runtime into your plugin JAR.
<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>

    <!-- Paper runtime: PaperCommandManager, PaperCommand, PaperBasicCommand -->
    <dependency>
        <groupId>io.github.projectunified</groupId>
        <artifactId>craftcommand-paper-runtime</artifactId>
        <version>0.5.1</version>
    </dependency>

    <!-- Paper API (provided by the server) -->
    <dependency>
        <groupId>io.papermc.paper</groupId>
        <artifactId>paper-api</artifactId>
        <version>1.21-R0.1-SNAPSHOT</version>
        <scope>provided</scope>
    </dependency>
</dependencies>
2

Configure the annotation processor

Add craftcommand-paper-processor to <annotationProcessorPaths>. This generates the ClassName_Paper Brigadier wrapper. If you want the BasicCommand fallback wrapper instead, replace it with craftcommand-paper-processor-basic, which generates ClassName_PaperBasic. Both can coexist in the same <annotationProcessorPaths> block.
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.15.0</version>
            <configuration>
                <annotationProcessorPaths>
                    <!-- Brigadier wrapper (_Paper) -->
                    <path>
                        <groupId>io.github.projectunified</groupId>
                        <artifactId>craftcommand-paper-processor</artifactId>
                        <version>0.5.1</version>
                    </path>
                    <!-- Optional: BasicCommand fallback wrapper (_PaperBasic) -->
                    <!--
                    <path>
                        <groupId>io.github.projectunified</groupId>
                        <artifactId>craftcommand-paper-processor-basic</artifactId>
                        <version>0.5.1</version>
                    </path>
                    -->
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

paper-plugin.yml

Paper plugins use paper-plugin.yml instead of plugin.yml. Unlike the Bukkit platform, Paper’s lifecycle-based command registration does not require individual command entries in the YAML — the plugin just needs to declare itself as a Paper plugin.
name: ExamplePlugin
version: '${project.version}'
main: io.github.projectunified.craftcommand.example.paper.ExamplePlugin
api-version: '1.21'
description: Example Paper plugin using CraftCommand Paper

Writing Paper Commands

TeleportCommand

The TeleportCommand below is the real example from the CraftCommand Paper example module. The annotations and parameter types are identical to the Bukkit version — the difference is that the annotation processor generates a Brigadier LiteralCommandNode tree rather than a plain Command subclass.
package io.github.projectunified.craftcommand.example.paper;

import io.github.projectunified.craftcommand.CommandInfo;
import io.github.projectunified.craftcommand.annotation.*;
import io.github.projectunified.craftcommand.bukkit.annotation.Permission;
import io.github.projectunified.craftcommand.paper.PaperCommandManager;
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 for Paper")
@Permission("example.tp")
public class TeleportCommand {
    // 1. Suggestion provider from a Field
    public final List<String> modes = Arrays.asList("normal", "silent", "instant");
    private final PaperCommandManager commandManager;

    public TeleportCommand(PaperCommandManager 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.");
        }
    }
}

BroadcastCommand

BroadcastCommand shows how a subcommand method can accept CommandSourceStack directly as the sender type — useful when you need to access Paper-specific source properties.
package io.github.projectunified.craftcommand.example.paper;

import io.github.projectunified.craftcommand.annotation.Command;
import io.github.projectunified.craftcommand.annotation.Default;
import io.github.projectunified.craftcommand.annotation.Greedy;
import io.github.projectunified.craftcommand.annotation.Subcommand;
import io.github.projectunified.craftcommand.bukkit.annotation.Permission;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;

@Command(value = "broadcast", aliases = {"bc"}, description = "Broadcasts a message to all players")
@Permission("example.broadcast")
public class BroadcastCommand {

    @Default
    public void execute(CommandSender sender, @Greedy String message) {
        Bukkit.broadcastMessage("[Broadcast] " + message);
    }

    // Uses CommandSourceStack directly for Paper-specific source access
    @Subcommand("stack")
    public void executeStack(CommandSourceStack sender, @Greedy String message) {
        Bukkit.broadcastMessage("[Stack Broadcast] " + message);
    }

    @Subcommand("type")
    public void executeType(CommandSender sender, BroadcastType type, @Greedy String message) {

    }

    public enum BroadcastType {
        MESSAGE,
        ACTION_BAR
    }
}

Registering in Your Plugin

Create a PaperCommandManager in onEnable. The constructor immediately hooks into LifecycleEvents.COMMANDS — so you must call register(...) before the lifecycle event fires, which means registering in onEnable (not in a scheduler task). There is no syncCommand() call required; the lifecycle manager handles that automatically.
package io.github.projectunified.craftcommand.example.paper;

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

import java.util.Locale;

public class ExamplePlugin extends JavaPlugin {
    private PaperCommandManager commandManager;

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

        commandManager.registerResolver(BroadcastCommand.BroadcastType.class, (sender, args, current) -> BroadcastCommand.BroadcastType.valueOf(current.toUpperCase(Locale.ROOT)));

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

        getLogger().info("ExamplePlugin enabled and Paper commands registered!");
    }
}
PaperCommandManager registers a LifecycleEvents.COMMANDS handler in its constructor. All calls to register(...) queue up command entries; they are submitted to the Paper registrar when that lifecycle event fires. This means you must call register(...) during or before onEnable — not inside an async task or a delayed scheduler.

Brigadier vs BasicCommand

PaperCommandManager.register(Object) applies the following resolution order at runtime:
1

Try the Brigadier wrapper (_Paper)

The manager attempts to load ClassName_Paper — the class generated by craftcommand-paper-processor. If found, it casts to PaperCommand and registers the LiteralCommandNode tree via event.registrar().register(node, description, aliases).
2

Fall back to BasicCommand wrapper (_PaperBasic)

If ClassName_Paper is not on the classpath (i.e. you used craftcommand-paper-processor-basic instead), the manager loads ClassName_PaperBasic. This implements Paper’s BasicCommand interface and is registered via event.registrar().register(name, description, aliases, command).
3

Throw if neither wrapper exists

If neither class is found, register(Object) throws IllegalArgumentException explaining which class it tried to instantiate.
Generated by craftcommand-paper-processor. Produces a full LiteralCommandNode<CommandSourceStack> tree with typed argument nodes. Clients see argument type hints (e.g. <player>, <world>) in the chat HUD and benefit from server-side argument validation before the command body runs.
<path>
    <groupId>io.github.projectunified</groupId>
    <artifactId>craftcommand-paper-processor</artifactId>
    <version>0.5.1</version>
</path>

PaperCommandManager API

register(Object commandInstance)

Tries ClassName_Paper (Brigadier) first, then ClassName_PaperBasic (BasicCommand). Queues the wrapper to be submitted when LifecycleEvents.COMMANDS fires.

register(PaperCommand command)

Registers a pre-built PaperCommand Brigadier wrapper directly. The wrapper’s getCommandNode(), getDescription(), and getAliases() are forwarded to Paper’s command registrar.

register(PaperBasicCommand command)

Registers a pre-built PaperBasicCommand BasicCommand wrapper directly. The wrapper’s getName(), getDescription(), and getAliases() are forwarded to Paper’s command registrar.

Sender Type

On the Paper platform the internal sender type used by CommandManager is CommandSourceStack (io.papermc.paper.command.brigadier.CommandSourceStack). PaperCommandManager extends CommandManager<CommandSourceStack>, so the default error handler and any custom ErrorHandler you supply receive a CommandSourceStack as the first argument. To reach the underlying CommandSender (or cast to Player) from a CommandSourceStack, call source.getSender():
import io.papermc.paper.command.brigadier.CommandSourceStack;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;

// In a custom ErrorHandler:
ErrorHandler<CommandSourceStack> handler = (source, exception) -> {
    CommandSender sender = source.getSender();
    sender.sendMessage("Error: " + exception.getMessage());
};

PaperCommandManager manager = new PaperCommandManager(plugin, handler);
In your command methods you can declare CommandSender or Player directly as the sender parameter type — the generated wrapper resolves the correct object from the CommandSourceStack automatically. Use CommandSourceStack only when you need Paper-specific source properties (e.g. source.getExecutor() for the entity that actually triggered the command, as opposed to the sender).

Build docs developers (and LLMs) love