Skip to content

HanielCota/CommandFramework

Repository files navigation

CommandFramework

Build CodeQL Javadoc JitPack

A lightweight, type-safe command framework for Minecraft server plugins, supporting both Paper (Bukkit) and Velocity proxy platforms.

Features

  • Platform Abstraction: Core module is platform-agnostic — adapters for Paper and Velocity provided
  • Annotation-Based Commands: Declare commands with @Command, @Subcommand, @Permission, @Cooldown, @Async
  • Type-Safe Parameters: Automatic argument parsing with ParameterResolver registry (String, int, long, double, boolean, enums, greedy strings, and Paper types)
  • Low-Boilerplate UX: @Arg, @PlayerOnly, @ConsoleOnly, @OnlinePlayer, @GreedyString, @OptionalArg, @Syntax, batch registration, and message presets
  • Declarative Validation: @Range, @Min, @Max, @Length, @Regex annotations for automatic parameter validation
  • CLI-Style Flags: @Flag("silent") boolean silent and @Flag("reason") String reason for flag-based arguments
  • Pipeline Architecture: Dispatch pipeline with guard stage (permission, sender, cooldown) and execution stage (parse + invoke + interceptors)
  • Rich Interceptors: RichCommandInterceptor receives resolved parameters in before(ctx, params) for advanced middleware
  • Async Support: Mark routes with @Async to run the command body off-thread while guards/parsing stay on the platform thread
  • Rate Limiting & Throttling: Built-in token-bucket rate limiting and input sanitization
  • Tab Completion: Automatic suggestion engine based on route tree, parameter types, and flags
  • Adventure Components: CommandActor.sendMessage(Component) supported in core with platform-native delivery
  • Type-Safe Actor: actor.unwrap(Player.class) returns the native platform entity without casting
  • Subcommand Groups: @CommandGroup("moderator") on inner classes for type-safe command hierarchies
  • Advanced Help: PaginatedHelpCommand with pagination, grouping, and syntax highlighting
  • Parse Caching: Optional CachedCommandParameterParser for high-traffic commands
  • Metrics: InMemoryCommandMetrics tracks dispatch counts, error rates, and duration statistics per route
  • Production Ready: Thread-safe actor caches, debounced messages, safe logging with truncation, input sanitization, configurable overlays

Modules

Module Description
command-core Core framework — dispatcher, routing, parsing, pipeline, rate limiting, cooldowns
command-annotations Annotation scanning (@Command, @Subcommand, etc.) and method binding
command-paper Paper/Bukkit adapter — Brigadier lifecycle + legacy Bukkit command map
command-velocity Velocity proxy adapter — SimpleCommand + RawCommand bridges
examples Sample plugins for Paper and Velocity
benchmarks JMH benchmarks

Installation (JitPack)

Current stable release: v1.3.0.

Add the JitPack repository to your build.gradle.kts:

repositories {
    mavenCentral()
    maven("https://jitpack.io")
    maven("https://repo.papermc.io/repository/maven-public/")
}

Add the dependencies you need:

dependencies {
    // Paper plugin
    implementation("com.github.HanielCota.CommandFramework:command-paper:v1.3.0")
    
    // Velocity plugin
    implementation("com.github.HanielCota.CommandFramework:command-velocity:v1.3.0")
    
    // Or individual modules
    implementation("com.github.HanielCota.CommandFramework:command-core:v1.3.0")
    implementation("com.github.HanielCota.CommandFramework:command-annotations:v1.3.0")
}

For snapshots from main, replace v1.3.0 with main-SNAPSHOT. JitPack builds Git tags on demand, so a newly published tag can take a few minutes on its first dependency resolution.

Requirements

  • Java: 21+
  • Paper API: 1.21.11+
  • Velocity API: 3.5.0-SNAPSHOT+

Quick Start

Paper Plugin

import org.bukkit.entity.Player;

public final class MyPlugin extends JavaPlugin {
    private PaperCommandFramework commands;

    @Override
    public void onEnable() {
        commands = PaperCommandFramework.builder(this)
                .messageProvider(CommandMessages.portugueseBrazil())
                .build();
        commands.registerAnnotated(new MyCommands(), new AdminCommands());
        // Or, for no-arg command classes:
        // commands.registerPackage("com.example.commands");
    }

    @Override
    public void onDisable() {
        if (commands != null) commands.shutdown();
    }
}

@Command("kit")
public class MyCommands {
    @Default
    @PlayerOnly
    public void onDefault(CommandActor actor) {
        actor.sendWarning("Use /kit give <player>");
    }

    @Subcommand("give")
    @Permission("kit.give")
    @Cooldown(value = 3, unit = TimeUnit.SECONDS)
    @Syntax("/kit give <player> <item> [amount]")
    public void onGive(CommandActor actor,
                       @OnlinePlayer @Arg("player") Player target,
                       Material item,
                       @Arg(value = "amount", defaultValue = "1") int amount) {
        actor.sendSuccess("Sent " + amount + "x " + item.name() + " to " + target.getName());
        // void methods automatically return CommandResult.success()
    }

    @Subcommand("heal")
    @Permission("kit.heal")
    @PlayerOnly
    public void onHeal(Player player) {
        // Receive Player directly — no casting needed!
        player.setHealth(player.getMaxHealth());
        player.sendMessage("§aYou have been healed!");
    }

    @Subcommand("delete")
    @Permission("kit.admin")
    public CommandResult onDelete(CommandActor actor, String target) {
        // Return CommandResult explicitly when you need error handling
        if (target.isBlank()) {
            return CommandResult.failure(CommandStatus.INVALID_USAGE, "Player name required");
        }
        actor.sendMessage("Kit deleted for " + target);
        return CommandResult.success();
    }
}

Cognitive-Load Helpers

Use the semantic aliases when they make command intent clearer:

@Subcommand("broadcast")
@Permission("admin.broadcast")
public void broadcast(CommandActor actor, @GreedyString String message) {
    actor.sendSuccess("Broadcast sent.");
}

@Subcommand("page")
public void page(CommandActor actor, @OptionalArg("1") int page) {
    actor.sendMessage("Page " + page);
}

@Subcommand("give")
@Syntax("/kit give <player> <item> [amount]")
public void give(CommandActor actor,
                 @OnlinePlayer @Arg("player") Player target,
                 Material item,
                 @Arg(value = "amount", defaultValue = "1") int amount) {
    actor.sendSuccess("Kit sent.");
}

Platform adapters also register annotated player resolvers:

@Subcommand("tp")
@PlayerOnly
public void tp(Player sender, @OnlinePlayer Player target) {
    sender.teleport(target);
}

Player without @OnlinePlayer injects the command sender. @OnlinePlayer Player consumes a player-name argument and provides player-name tab completion. On Paper, built-in resolvers also cover World, cached OfflinePlayer, Material, NamespacedKey, and Enchantment.

@Syntax overrides the generated usage shown after parse failures and in help. Use @Arg when the Java parameter name is not available at runtime or when a default value should live beside the parameter.

Registration Helpers

Platform adapters support explicit instances, batches, no-arg command classes, and package scanning:

commands.registerAnnotated(new KitCommands());
commands.registerAnnotated(new KitCommands(), new AdminCommands());
commands.registerClasses(KitCommands.class, AdminCommands.class); // no-arg constructors required
commands.registerPackage("com.example.commands");                // scans @Command classes

Prefer explicit instances when commands need constructor dependencies. registerPackage is intended for plugin startup and rejects blank package names.

Velocity Plugin

@Plugin(id = "my-plugin", name = "My Plugin", version = "1.0.0")
public class MyVelocityPlugin {
    private final VelocityCommandFramework<MyVelocityPlugin> commands;

    @Inject
    public MyVelocityPlugin(ProxyServer server, Logger logger) {
        this.commands = VelocityCommandFramework.builder(server, this).build();
        commands.registerAnnotated(new MyCommands());
    }
}

Declarative Suggestions

The framework provides type-safe tab-completion with @Suggestions:

@Subcommand("msg")
public void onMsg(CommandActor actor, @Suggestions("players") String target, String message) {
    // 'target' suggests online player names
}

@Subcommand("warp")
public void onWarp(CommandActor actor, @Suggestions("worlds") String world) {
    // 'world' suggests world names (Paper)
}

Built-in providers: players, worlds (Paper), servers (Velocity). Enums suggest their constants automatically without annotation.

Register custom providers:

PaperCommandFramework.builder(this)
    .suggestionProvider("kits", ctx -> List.of("starter", "vip", "admin"))
    .build();

CLI-Style Flags

Use @Flag for toggle-style and value-style flags:

@Subcommand("ban")
@Permission("moderator.ban")
public void ban(
    CommandActor actor,
    String player,
    @Flag("silent") boolean silent,
    @Flag("reason") String reason
) {
    // /ban player --silent --reason "spam"
    // boolean flags default to false
    // String flags consume the next token as value
}

Flag parameters are declared after positional parameters in Java method signatures, but users may type flags before or after positional arguments. Flags automatically suggest themselves in tab-completion (--silent, -s).

Declarative Validation

Validate parameters without manual checks:

@Subcommand("give")
public void give(
    CommandActor actor,
    @Range(min = 1, max = 64) int amount,
    @Length(min = 3, max = 16) String item
) {
    // Validation runs automatically before method invocation
    // Failed validation returns CommandStatus.INVALID_USAGE
}

@Subcommand("warp")
public void warp(
    @Min(0) @Max(100) int x,
    @Min(0) @Max(100) int y,
    @Min(0) @Max(100) int z
) {
    // Numeric bounds enforced automatically
}

Supported validators:

Annotation Types Description
@Range(min, max) Numeric Inclusive range check
@Min(value) Numeric Minimum value (inclusive)
@Max(value) Numeric Maximum value (inclusive)
@Length(min, max) String String length bounds
@Regex("pattern") String Regex pattern matching

Type-Safe Actor Access

Access the underlying platform entity without casting:

@Subcommand("heal")
@OnlyPlayer
public void heal(CommandActor actor) {
    // Paper: get native Player without casting
    Player player = actor.unwrap(Player.class);
    player.setHealth(player.getMaxHealth());
}

unwrap(Class) delegates to platform adapters. as(Class) is also available for casting the actor wrapper itself.

Subcommand Groups

Create type-safe command hierarchies with @CommandGroup:

@Command("admin")
public class AdminCommands {

    @Subcommand("ban")
    public void ban(CommandActor actor, String player) { }

    @CommandGroup("moderator")
    public class ModeratorGroup {
        @Subcommand("kick")
        public void kick(CommandActor actor, String player) { }
        // Resolves to: /admin moderator kick <player>
    }
}

Groups can be nested arbitrarily deep. Each inner class annotated with @CommandGroup prefixes its methods with the group path.

Advanced Help

Paginated help with automatic grouping:

// Show 8 routes per page
CommandRoute helpRoute = PaginatedHelpCommand.create(
    "help", dispatcher, messageProvider, 8
);
dispatcher.register(helpRoute);

PaginatedHelpCommand filters by permission, groups by category, and supports pagination via /help <page>.

Rich Interceptors

Access resolved parameters in interceptors:

public class AuditInterceptor implements RichCommandInterceptor {
    @Override
    public CommandResult before(CommandContext context, List<ParsedParameter<?>> parameters) {
        // Log or validate based on parsed parameter values
        for (ParsedParameter<?> param : parameters) {
            logger.info("Param " + param.parameter().name() + " = " + param.value());
        }
        return CommandResult.success();
    }
}

RichCommandInterceptor extends CommandInterceptor and receives parsed parameters before execution. Regular CommandInterceptor continues to work unchanged.

CommandContext Access

Retrieve parsed parameters by name and type:

public CommandResult execute(CommandContext context, List<ParsedParameter<?>> parameters) {
    // Type-safe parameter retrieval
    Optional<Integer> amount = context.get("amount", Integer.class);
    String player = context.require("player", String.class);
    
    // Alternative: access via parsedParameter()
    Optional<ParsedParameter<?>> param = context.parsedParameter("amount");
}

get() returns Optional<T> when present and type-compatible. require() throws IllegalArgumentException if absent or incompatible.

Adventure Component Support

Send Adventure Component directly from the core API:

Component component = MiniMessage.miniMessage().deserialize("<green>Hello!</green>");
actor.sendMessage(component); // Works on both Paper and Velocity

The default implementation serializes to plain text for platforms without native Adventure support. Paper and Velocity adapters deliver components natively.

Metrics

Track command usage with built-in metrics:

InMemoryCommandMetrics metrics = new InMemoryCommandMetrics();
CommandDispatcher dispatcher = CommandDispatcher.builder()
    .metrics(metrics)
    .build();

// After dispatching commands:
InMemoryCommandMetrics.CommandStats stats = metrics.stats("kit give");
long total = stats.totalDispatches();
double errorRate = stats.errorRate();
Duration avg = stats.averageDuration();

Thread Safety

  • Paper: Most Bukkit API calls must run on the main thread. When using @Async, route resolution, guards, parsing, and before-interceptors stay on the command thread; only the command executor body is scheduled on the configured async executor. The Paper adapter automatically schedules sendMessage and after-interceptors back to the main thread. Other API calls (Player, World, Inventory) must be scheduled manually by the command executor.
  • Velocity: The proxy API is generally thread-safe. @Async is safe for most operations.

Note on Registration: Route registration (register(), unregister()) is synchronized but should happen during plugin startup, not concurrently with command dispatch.

Safety Features

  • Input Sanitization: Automatically strips control characters and null bytes (\0). Limits max arguments (32) and argument length (128) by default.
  • Safe Logging: Log text is cleaned of control characters and truncated to 1024 characters to prevent log injection.
  • Locale Safety: All string normalizations use Locale.ROOT to avoid Turkish locale bugs (i vs ı).

Auto-Generated Help

The framework provides two help command implementations:

Simple Help

CommandRoute helpRoute = AutoHelpCommand.create("help", dispatcher, messageProvider);
dispatcher.register(helpRoute);

Lists all available routes filtered by permission and sender requirements.

Paginated Help

CommandRoute helpRoute = PaginatedHelpCommand.create(
    "help", dispatcher, messageProvider, 8 /* routes per page */
);
dispatcher.register(helpRoute);

Features pagination, permission filtering, and syntax display. Users navigate with /help <page>.

The CommandMessageProvider interface includes noDescription() for internationalized fallback text when a route has no description.

Advanced Features

Builder.copyFrom()

Clone an existing route to create a modified variant:

CommandRoute original = CommandRoute.builder("warp", executor).build();
CommandRoute modified = CommandRoute.Builder.copyFrom(original)
    .permission("warp.use")
    .build();

Configuration Overlay

Override route settings at runtime via CommandConfiguration:

CommandDispatcher dispatcher = CommandDispatcher.builder()
    .configuration(myYamlConfig)
    .build();

Supported overrides: permission, cooldown, aliases, description, syntax, async.

Parse Caching

Enable caching for high-traffic commands to avoid re-parsing identical arguments:

CommandDispatcher dispatcher = CommandDispatcher.builder()
    .parameterCache(true)
    .build();

Uses an internal bounded cache with 10k entries and 5-minute TTL. The cache only stores scalar/enum parse results; routes with platform objects or other unsafe parameter types bypass caching automatically.

Metrics

Track command usage with built-in metrics:

InMemoryCommandMetrics metrics = new InMemoryCommandMetrics();
CommandDispatcher dispatcher = CommandDispatcher.builder()
    .metrics(metrics)
    .build();

// Query statistics
InMemoryCommandMetrics.CommandStats stats = metrics.stats("kit give");
System.out.println("Total: " + stats.totalDispatches());
System.out.println("Error rate: " + stats.errorRate());
System.out.println("Avg duration: " + stats.averageDuration());

MiniMessage / Adventure Formatting

Both Paper and Velocity adapters support MiniMessage out of the box:

actor.sendMessage("<green>Hello <yellow>%s</yellow>!".formatted(playerName));

MiniMessage tags are parsed automatically. If MiniMessage parsing fails, the adapter falls back to legacy color codes (§a, &a).

For advanced use (hover events, click events, translatable components), use Component directly on platform adapters.

All default framework messages use MiniMessage tags for rich formatting.

Building

./gradlew build

The build runs tests, Javadoc, shadow JAR tasks, JaCoCo reports, and Spotless format checks. Spotless is pinned to UNIX line endings; .editorconfig and .gitattributes also enforce LF so Windows and Linux checkouts behave the same.

Documentation

License

MIT License — see LICENSE for details.

About

Lightweight type-safe command framework for Paper and Velocity Minecraft servers

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages