CommandsAPI is a modular, extensible Java library for building robust, typed command systems across multiple
platforms such as Spigot and Velocity.
As of version 4.0.0, all core logic has been extracted into a dedicated core module, enabling seamless
multi-platform support.
- β Multi-Platform Support (Spigot, Velocity, etc.)
- β Typed Argument Parsing with validation
- β Custom Argument Converters
- β Subcommands & Hierarchical Command Trees
- β Tab Completion Support
- β Permission & Context Requirements
- β Optional and Infinite Arguments
- β Auto-Generated Usage Help
- β Overriding of Existing Labels (vanilla or another plugin)
- β Message Formatter (prefix, MiniMessage, placeholders, per-sender translation)
- β Lightweight, Fast, and Fully Extensible
A label already registered on the platform β a vanilla command such as /gamemode, /tp, /ban,
or one owned by another plugin β is left alone by default: the command is not bound and the
existing one keeps answering. This is now logged as a warning, instead of failing silently.
To take the label over, mark the command as an override:
@Command(name = "gamemode", permission = "admin.gamemode", override = true)
public void gamemode(Player sender, @Arg("mode") GameMode mode) { ... }
// or, with the builder
manager.command("gamemode").override().executor(...).register();The previous command stays reachable through its namespace (/minecraft:gamemode,
/otherplugin:home) β only the plain label changes hands.
The messages the library sends β no permission, only in-game, command disabled, unmet
requirement, usage, unrecognised argument β come from the MessageHandler. A message
formatter is the last step before one of them leaves, and unlike the handler it knows
who the message is being sent to:
public interface MessageFormatter<S> {
String format(S sender, String raw);
}Register one on the manager:
// A plugin prefix, without repeating it in every message of the handler
manager.setMessageFormatter((sender, raw) -> "&8[&bMyPlugin&8] " + raw);
// MiniMessage, serialized back to the legacy format
manager.setMessageFormatter((sender, raw) -> LegacyComponentSerializer.legacySection()
.serialize(MiniMessage.miniMessage().deserialize(raw)));
// External placeholders, resolved for the sender the message is about to reach
manager.setMessageFormatter((sender, raw) -> sender instanceof Player player
? PlaceholderAPI.setPlaceholders(player, raw)
: raw);The formatter is called on every sending path, including the direct interaction replies of
the JDA platform, and it receives the message after the internal placeholders (%arg%,
%requirement%) have been substituted, so it always sees the final text. Setting it back to
null restores the default behaviour: with no formatter registered, messages are sent
exactly as the handler produced them.
βΉοΈ The text formatter works on plain
String, because thecoremodule also serves platforms without Adventure, such as JDA. To keepclickandhoverevents, use a component formatter (below) on Paper and Velocity.
A component formatter builds each message as an Adventure Component, which is then sent
as a component β click and hover events reach the player intact:
public interface ComponentMessageFormatter<S> {
Component format(S receiver, MessageKey key, Map<String, String> placeholders);
}The message comes by its MessageKey (NO_PERMISSION, ONLY_IN_GAME, ARG_NOT_RECOGNIZED,
REQUIREMENT, DISABLED, TOO_LONG, INVALID_FORMAT, USAGE, INTERNAL_ERROR), and its values
come apart, never substituted in the text: arg (the input), requirement (the requirement's
errorMessage(), or its name() when empty) and usage.
// MiniMessage templates from the server's configuration; values become <arg>, <requirement>, <usage>
manager.setComponentFormatter(ComponentMessageFormatter.miniMessage(
key -> getConfig().getString("messages." + key.name().toLowerCase())));
// Or keep the texts in a MessageHandler written in MiniMessage: %arg% is read as <arg>
manager.setComponentFormatter(ComponentMessageFormatter.miniMessage(myHandler));Values are inserted as plain text: an argument typed as <red>abc is shown as typed. A formatter
returning null for a key sends that message as text, through the MessageHandler and the text
formatter, as before. With a component formatter set, the text formatter only sees those messages.
The formatter lives in the adventure-addon module, pulled in by the Paper and Velocity
platforms. On another Adventure platform, install it with ComponentMessages.install(manager, formatter).
platform-paper registers commands through Paper's Brigadier API (LifecycleEvents.COMMANDS)
instead of the Bukkit command map, so the client knows the shape of each command: arguments are
coloured, a wrong value is flagged before it is sent, and suggestions come from the tab completers.
public final class MyPlugin extends JavaPlugin {
@Override
public void onEnable() {
CommandManager<MyPlugin> manager = new fr.traqueur.commands.paper.CommandManager<>(this);
manager.command("give").arg("amount", Integer.class).executor((sender, args) -> { ... }).register();
}
}| CommandsAPI argument | Brigadier node |
|---|---|
int/Integer, long, double, float, byte (library converter) |
the matching number |
boolean/Boolean (library converter) |
boolean |
Infinite |
greedy string, to the end of the line |
String, a type with a custom converter, a number with your own converter |
word, with the tab completer's suggestions |
The permission and the requirements of a command become the requires of its node: a player who
cannot run a command does not see it. A command is executable once its required arguments are
given; running it goes through the usual invoker, so conversion, checks and messages are unchanged.
setComponentFormatter is available as on Velocity.
βΉοΈ A word only accepts
0-9 A-Z a-z _ - . +, as in vanilla: the client refuses aStringargument holding other characters (accents,<,&). Declare itInfinitewhen it must accept any text.
Paper asks for the commands when it builds its command tree: once at startup, after the plugins
are enabled, then on every /minecraft:reload. The nodes are built from what the manager holds
at that moment. Tested on Paper 1.21.4 and 26.3:
| What happens | Result |
|---|---|
Command registered in onEnable |
Available at startup. |
| New label registered after startup | Unknown command until the next /minecraft:reload (a warning is logged), then available. |
| Command unregistered after startup | Its node stays, hidden by its requires: unknown command, and the clients' command lists are refreshed. |
| Same label registered again | Visible again, with the arguments the node had when it was built; a changed shape needs a reload. |
unregisterAll() |
Every command of the manager becomes unknown; other plugins' commands stay. |
platform-spigot stays available for Spigot, or for a Paper server that prefers the command map.
manager.describe() returns a public, read-only tree of the commands registered through the
manager, to generate a plugin's documentation without touching the internal structures:
for (CommandDescription command : manager.describe()) {
System.out.println(command.usage() + " - " + command.description()
+ (command.permission().isEmpty() ? "" : " (" + command.permission() + ")"));
command.requiredArguments(); // List<ArgumentDescription>: name, type, optional, infinite
command.optionalArguments();
command.subcommands(); // List<CommandDescription>, with their full path ("home set")
}The usage is the one set on the command, or the one the library generates, listing every subcommand.
When a plugin stops, unregisterAll() removes every command its manager registered, with their
aliases and subcommands:
@Override
public void onDisable() {
manager.unregisterAll();
}Only the commands of this manager go: those of other managers, other plugins and the server stay in place, including a label another plugin held and this manager did not get.
A requirement that fails sends its errorMessage() as-is. When it is empty, the generic
MessageHandler#getRequirementMessage() is sent instead, and its %requirement% placeholder
is filled with Requirement#name() β the simple name of the requirement's class by default.
Override name() to show something readable:
@Override
public String name() {
return "Γͺtre dans le Nether";
}Creating a CommandManager makes no network call. To be told about a newer version of the
library, ask for it:
Updater.checkUpdates(); // asynchronous, logs onceA failed check, or a version that cannot be compared (a build named after a commit), announces
nothing. A server can turn the check off for every plugin with -Dcommands.updater=false.
traqueur-dev-commandsapi/
βββ core/ # Platform-agnostic command logic
βββ spigot/ # Spigot implementation
βββ paper/ # Paper implementation, on Brigadier
βββ adventure-addon/ # Component messages, for Adventure platforms
βββ <platform>-test-plugin/ # The test plugin for the specified platform
βββ velocity/ # Velocity implementation
- Java 21+
- A supported Minecraft platform (e.g., Spigot or Velocity)
- Build tool (Gradle/Maven) with JitPack
repositories {
maven { url 'https://repo.groupez.dev/<repository>' } // snapshots or releases
}
dependencies {
implementation 'fr.traqueur.commands:platform-spigot:[version]' // or platform-velocity or platform-<your-platform>
}<repositories>
<repository>
<id>groupez-releases</id>
<url>https://repo.groupez.dev/releases</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>fr.traqueur.commands</groupId>
<artifactId>platform-spigot</artifactId> <!-- or platform-velocity or platform-<your-platform> -->
<version>[version]</version>
</dependency>
</dependencies>
β οΈ Relocate the library when shading it into your plugin to avoid version conflicts with other plugins.
The annotations-addon module provides annotation-based command registration. When using @Arg implicitly (without annotation), parameter names are used as argument names. This requires the -parameters compiler flag.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>tasks.withType(JavaCompile).configureEach {
options.compilerArgs.add('-parameters')
}tasks.withType<JavaCompile>().configureEach {
options.compilerArgs.add("-parameters")
}π‘ Without this flag, parameter names default to
arg0,arg1, etc. You can always use@Arg("name")explicitly to avoid this requirement.
| Parameter | Left out by the sender |
|---|---|
@Arg("amount") int amount |
required: the usage is sent |
@Arg("amount") Optional<Integer> amount |
Optional.empty() |
@Arg("amount") @Default("1") int amount |
1 |
@Arg("amount") @Default("1") Optional<Integer> amount |
Optional.of(1) |
@Arg("text") @Infinite @Default("hello") String text |
"hello" |
The @Default value is written as the sender would type it and goes through the converter of the
parameter's type when the command runs, custom converters included. On a primitive parameter, an
invalid value (@Default("lots") int) is refused when the commands are registered. Optional
parameters must come after the required ones: required arguments are always read first.
Be sure to extends all the classes from the platform you are using (Spigot, Velocity, etc.):
fr.traqueur.commandsapi.spigot.CommandManager for Spigot, fr.traqueur.commandsapi.velocity.CommandManager for
Velocity, etc.
public class HelloWorldCommand extends Command<MyPlugin> {
public HelloWorldCommand(MyPlugin plugin) {
super(plugin, "helloworld");
setDescription("A simple hello world command");
setUsage("/helloworld");
}
@Override
public void execute(CommandSender sender, Arguments args) {
sender.sendMessage("Hello, world!");
}
}Register the command:
@Override
public void onEnable() {
CommandManager<MyPlugin> manager = new CommandManager<>(/*args depending of the platform*/);
manager.registerCommand(new HelloWorldCommand(this));
}You can create your own adapter by implementing:
public interface CommandPlatform<T, S> {
T getPlugin();
void injectManager(CommandManager<T, S> manager);
Logger getLogger();
boolean hasPermission(S sender, String permission);
void addCommand(Command<T, S> command, String label);
void removeCommand(String label, boolean subcommand);
}This allows support for new platforms like Fabric, Minestom, or BungeeCord.
To publish locally for development:
./gradlew core:publishToMavenLocal spigot:publishToMavenLocal velocity:publishToMavenLocalVisit the Wiki for:
- Tutorials
- Examples
- API Reference
- Extending with custom types and logic
We welcome contributions!
- Fork this repository
- Create a new branch
- Implement your feature or fix
- Open a pull request with a clear description
CommandsAPI is licensed under the MIT License.
Need help or want to report a bug? Open an issue on GitHub