-
Notifications
You must be signed in to change notification settings - Fork 1
Commands
PowerLib ships an optional command framework that lets you declare a command once and register it on Bukkit/Spigot/Paper, BungeeCord, or Velocity without rewriting the platform glue each time. You build the command with a fluent CommandBuilder, define typed arguments, and provide an execution handler that receives a platform-agnostic CommandSender and a parsed CommandContext. The per-platform module handles the actual registration against the running server or proxy.
This module is independent of the rest of PowerLib — it lives in its own artifacts and does not require powerlib-common, powerlib-bukkit, etc. to be present (though it can sit alongside them). If you only want the command API, you only pull in the command modules.
For sending feedback from inside a handler, see Messages. For project setup, repositories and bootstrapping, see Getting Started.
The framework is split into a platform-agnostic API plus one thin binding per platform. You always depend on the API, plus the binding for the platform you target.
| Artifact | Package | Contents |
|---|---|---|
powerlib-commands-api |
it.mycraft.powerlib.commands |
CommandBuilder, Argument<T>, Args, CommandContext, CommandSender
|
powerlib-commands-bukkit |
it.mycraft.powerlib.commands.bukkit |
BukkitCommandBuilder, BukkitCommandSender (Paper Brigadier detection + Bukkit fallback) |
powerlib-commands-bungee |
it.mycraft.powerlib.commands.bungee |
BungeeCommandBuilder, BungeeCommandSender
|
powerlib-commands-velocity |
it.mycraft.powerlib.commands.velocity |
VelocityCommandBuilder, VelocityCommandSender
|
The <platform> module pulls in powerlib-commands-api transitively, so you only declare the binding.
PowerLib is published to the CodeMC repository.
<repositories>
<repository>
<id>codemc</id>
<url>https://repo.codemc.io/repository/maven-public/</url>
</repository>
</repositories>Declare the binding for one platform — Bukkit shown; swap the artifactId for powerlib-commands-bungee or powerlib-commands-velocity:
<dependency>
<groupId>it.mycraft</groupId>
<artifactId>powerlib-commands-bukkit</artifactId>
<version>1.3.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>repositories {
maven { url 'https://repo.codemc.io/repository/maven-public/' }
mavenCentral()
}
dependencies {
compileOnly 'it.mycraft:powerlib-commands-bukkit:1.3.1-SNAPSHOT'
}Use
provided/compileOnlywhen a standalone PowerLib plugin supplies the classes at runtime. If you instead shade the command modules into your own jar, switch tocompile/implementationand relocateit.mycraft.powerlibexactly as described in Getting Started. The command API is built at Java 17 bytecode level.
CommandBuilder ── name, description, permission
│
├── argument(Argument<?>) ← typed, ordered positional arguments
├── executor(BiConsumer<…>) ← runs on execution
├── tabComplete(BiFunction<…>) ← optional suggestion provider
└── register(pluginOrProxy) ← platform-specific binding
Handler receives:
CommandSender ── getName / hasPermission / sendMessage / raw
CommandContext ── typed, name-keyed values parsed from the arguments
Every platform builder (BukkitCommandBuilder, BungeeCommandBuilder, VelocityCommandBuilder) extends the same abstract CommandBuilder, so the command definition is identical across platforms — only the create(...) factory and the object passed to register(...) differ.
CommandBuilder (in powerlib-commands-api) is the abstract, fluent definition. You never instantiate it directly; you create a platform builder via its static create(String name) factory and then chain the fluent setters, which all return CommandBuilder for chaining.
import it.mycraft.powerlib.commands.Args;
import it.mycraft.powerlib.commands.bukkit.BukkitCommandBuilder;
BukkitCommandBuilder.create("heal")
.description("Heal a player")
.permission("myplugin.heal")
.argument(Args.string("target"))
.executor((sender, ctx) -> {
String target = ctx.get("target", String.class).orElse(sender.getName());
sender.sendMessage("§aHealing " + target);
})
.register(this); // 'this' is your org.bukkit.plugin.PluginThe command name is fixed at construction (create(name)); it is the label players type. It is exposed via getName() and cannot be changed afterwards.
.description("Heal a player") // stored; surfaced to Bukkit via setDescription
.permission("myplugin.heal") // checked automatically before the executor runsIf a permission is set, the platform adapter checks it before invoking your executor (and before tab completion through the legacy resolver). When the sender lacks the permission, the adapter sends §cNo permission. and stops — your executor is not called. A null permission (the default) means the command is open to everyone.
The denial message is currently fixed (
§cNo permission.). If you need custom permission feedback, leavepermissionunset and checksender.hasPermission(...)yourself inside the executor, then send your own Message.
Arguments are ordered, positional, and typed. Add them with argument(Argument<?>) in the order they appear on the command line. They are parsed for you and made available through the CommandContext. See Arguments & parsing for the full reference.
.argument(Args.string("target"))
.argument(Args.integer("amount"))executor(BiConsumer<CommandSender, CommandContext>) registers the code that runs when the command is executed. The first parameter is the cross-platform sender; the second is the context holding the parsed argument values.
.executor((sender, ctx) -> {
int amount = ctx.get("amount", Integer.class).orElse(1);
sender.sendMessage("§eAmount: " + amount);
})tabComplete(BiFunction<CommandSender, CommandContext, List<String>>) is optional. When set, it supplies suggestion strings; the CommandContext passed in reflects the arguments parsed so far. If you never set it, the command simply offers no PowerLib-provided suggestions.
.tabComplete((sender, ctx) -> Arrays.asList("Alice", "Bob", "Console"))On Bukkit/Paper this provider is invoked through the legacy (
BukkitCommand) tab-completion path — see Bukkit registration for why.
subcommand(String name, Consumer<CommandBuilder> config) declares a same-platform child builder you configure inline:
BukkitCommandBuilder.create("party")
.subcommand("create", sub -> sub
.permission("myplugin.party.create")
.executor((sender, ctx) -> sender.sendMessage("§aParty created")))
.subcommand("invite", sub -> sub
.argument(Args.string("player"))
.executor((sender, ctx) -> sender.sendMessage("§aInvited "
+ ctx.get("player", String.class).orElse("?"))));Children are full CommandBuilder instances stored under their name and retrievable via getSubcommands(). The platform adapters dispatch the root command's executor and parse the root's declared arguments; routing to a subcommand's executor based on the first argument is left to you. The lazy, reliable pattern is to declare a leading argument on the root and branch on it:
BukkitCommandBuilder root = BukkitCommandBuilder.create("party");
root.argument(Args.string("action"))
.executor((sender, ctx) -> {
String action = ctx.get("action", String.class).orElse("");
CommandBuilder sub = root.getSubcommands().get(action);
if (sub != null && sub.getExecutor() != null) {
sub.getExecutor().accept(sender, ctx);
} else {
sender.sendMessage("§cUnknown subcommand: " + action);
}
});CommandSender is the platform-agnostic abstraction over whoever ran the command (a player or the console). Your executor and tab-completion handlers receive it, so the same handler body works on every platform.
public interface CommandSender {
String getName();
boolean hasPermission(String permission);
void sendMessage(String message);
Object raw(); // platform-native sender for advanced use
}| Method | Behaviour |
|---|---|
getName() |
Sender's name. Players: their username. Console on Velocity reports "CONSOLE". |
hasPermission(String) |
true if the sender holds the permission. A null permission returns true (treated as "no permission required"). |
sendMessage(String) |
Sends a message, translating legacy §/&-style colour codes to the platform's native chat (Bungee builds a TextComponent; Velocity an Adventure Component). |
raw() |
The underlying platform sender, for APIs PowerLib does not wrap. Cast it to the concrete type for your platform. |
Use raw() only when you need something platform-specific. It returns the native object:
// Bukkit
org.bukkit.command.CommandSender bukkit = (org.bukkit.command.CommandSender) sender.raw();
// Bungee
net.md_5.bungee.api.CommandSender bungee = (net.md_5.bungee.api.CommandSender) sender.raw();
// Velocity
com.velocitypowered.api.command.CommandSource source =
(com.velocitypowered.api.command.CommandSource) sender.raw();For most feedback you do not need raw() — sender.sendMessage(...) already handles colour translation per platform. To build richer multi-line / placeholder messages, use the Message API and send through the platform sender.
CommandContext carries the parsed argument values into your handler. Values are stored by argument name and retrieved by name and expected type; retrieval is null- and type-safe and returns an Optional.
public final class CommandContext {
public CommandContext put(String name, Object value);
public <T> Optional<T> get(String name, Class<T> type);
public Map<String, Object> all(); // unmodifiable view
}-
get(name, type)returnsOptional.empty()if the name is absent or the stored value is not an instance oftype. It never throws and never returnsnull. - Arguments that fail to parse are not put into the context, so
get(...)is empty for them — pair it withorElse(...)/ifPresent(...)to supply defaults. -
all()exposes an unmodifiable, insertion-ordered map of everything parsed, useful for logging or debugging.
.executor((sender, ctx) -> {
String target = ctx.get("target", String.class).orElse(sender.getName());
int amount = ctx.get("amount", Integer.class).orElse(1);
boolean silent = ctx.get("silent", Boolean.class).orElse(false);
if (!silent) {
sender.sendMessage("§aGiving §e" + amount + "§a to §e" + target);
}
})You normally never call
put(...)yourself — the platform adapter populates the context from your declared arguments before the executor runs.put(...)is public mainly for testing and advanced custom flows.
An Argument<T> is a named, typed token with a parser that turns the raw string into an Optional<T>. PowerLib parses each positional token against the argument declared at that index; a successful parse is stored in the context under the argument's name.
public final class Argument<T> {
public Argument(String name, Class<T> type, Function<String, Optional<T>> parser);
public Argument<T> optional(); // mark as optional (fluent)
public String getName();
public Class<T> getType();
public boolean isOptional();
public Optional<T> parse(String raw); // run the parser
}You rarely construct Argument by hand. The Args factory provides the common types and a hook for custom ones:
| Factory | Returns | Parses |
|---|---|---|
Args.string(name) |
Argument<String> |
any token (passes through; null → empty) |
Args.integer(name) |
Argument<Integer> |
base-10 integer; non-numeric → empty (no exception) |
Args.bool(name) |
Argument<Boolean> |
true / false, case-insensitive; anything else → empty |
Args.custom(name, type, parser) |
Argument<T> |
whatever your Function<String, Optional<T>> returns |
import it.mycraft.powerlib.commands.Args;
import it.mycraft.powerlib.commands.Argument;
Argument<String> target = Args.string("target");
Argument<Integer> amount = Args.integer("amount");
Argument<Boolean> silent = Args.bool("silent");Args.custom(...) lets you parse into any type. The parser returns Optional.empty() to signal a parse failure (the value is then simply not added to the context).
import java.util.Optional;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
Argument<Player> playerArg = Args.custom("player", Player.class, raw -> {
Player p = Bukkit.getPlayerExact(raw);
return Optional.ofNullable(p);
});
BukkitCommandBuilder.create("tphere")
.permission("myplugin.tphere")
.argument(playerArg)
.executor((sender, ctx) -> {
Optional<Player> p = ctx.get("player", Player.class);
if (p.isEmpty()) {
sender.sendMessage("§cPlayer not found.");
return;
}
sender.sendMessage("§aTeleporting " + p.get().getName());
})
.register(this);Parsing is positional and lenient:
- Token
iis parsed against argumenti. Extra tokens beyond the declared arguments are ignored; missing trailing tokens simply leave those arguments unset. - A token that fails to parse is skipped (not stored), so the corresponding
ctx.get(...)is empty. Validate withOptionalrather than expecting an exception. -
optional()/isOptional()is metadata you can read (e.g. to build usage strings); the built-in adapters do not reject a command for missing optional arguments — handle required-vs-optional in your executor viaorElse/isEmpty.
The command definition is identical everywhere. Only two things change per platform: the create(...) factory you call, and the object you hand to register(...). register(Object) validates that object's type and throws IllegalArgumentException if it is wrong for the platform.
Use BukkitCommandBuilder.create(...) and pass your org.bukkit.plugin.Plugin (usually this inside your plugin's onEnable). The command does not need to be declared in plugin.yml — PowerLib registers it dynamically.
package it.mycraft.example;
import it.mycraft.powerlib.commands.Args;
import it.mycraft.powerlib.commands.bukkit.BukkitCommandBuilder;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
public final class ExamplePlugin extends JavaPlugin {
@Override
public void onEnable() {
BukkitCommandBuilder.create("feed")
.description("Feed yourself or another player")
.permission("example.feed")
.argument(Args.string("target"))
.executor((sender, ctx) -> {
String name = ctx.get("target", String.class).orElse(sender.getName());
sender.sendMessage("§aFed §e" + name);
// Reach the native sender when you need Bukkit-specific APIs:
if (sender.raw() instanceof Player player) {
player.setFoodLevel(20);
}
})
.tabComplete((sender, ctx) ->
getServer().getOnlinePlayers().stream()
.map(Player::getName)
.toList())
.register(this);
}
}Brigadier and the fallback. On registration the Bukkit builder probes for Paper's modern Brigadier API by checking whether io.papermc.paper.command.brigadier.Commands is on the classpath (PaperBrigadierAdapter.isAvailable()):
-
Detected (Paper 1.20.4+): PowerLib logs once that the Brigadier API is present, then — in this release — delegates to the Bukkit
CommandMapfallback. Native Brigadier-node registration is a planned follow-up; until then your command still works, and tab completion uses the legacy resolver (yourtabCompleteprovider). -
Not detected (Spigot, older Paper): PowerLib registers a
BukkitCommanddirectly into the server'sCommandMapvia reflection.
Either way you call exactly the same code; the behaviour you depend on (executor, permission, tabComplete) is identical. The detection exists so the fallback is explicit and forward-compatible — you do not configure anything.
Because registration goes through the live
CommandMap, register your commands inonEnable(). There is noplugin.ymlentry to maintain.
Use BungeeCommandBuilder.create(...) and pass your net.md_5.bungee.api.plugin.Plugin. The permission you set on the builder is also handed to Bungee's Command constructor, and the adapter implements TabExecutor so your tabComplete provider is wired in.
package it.mycraft.example;
import it.mycraft.powerlib.commands.Args;
import it.mycraft.powerlib.commands.bungee.BungeeCommandBuilder;
import net.md_5.bungee.api.plugin.Plugin;
public final class ExampleBungeePlugin extends Plugin {
@Override
public void onEnable() {
BungeeCommandBuilder.create("hub")
.description("Send a player to the hub")
.permission("example.hub")
.argument(Args.string("server"))
.executor((sender, ctx) -> {
String server = ctx.get("server", String.class).orElse("lobby");
sender.sendMessage("§aConnecting to §e" + server);
})
.tabComplete((sender, ctx) ->
getProxy().getServers().keySet().stream().toList())
.register(this);
}
}Use VelocityCommandBuilder.create(...) and pass the com.velocitypowered.api.proxy.ProxyServer. On Velocity you register from a listener method subscribed to ProxyInitializeEvent. The adapter is a SimpleCommand, so tabComplete is exposed through Velocity's suggest(...).
package it.mycraft.example;
import com.google.inject.Inject;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.proxy.ProxyServer;
import it.mycraft.powerlib.commands.Args;
import it.mycraft.powerlib.commands.velocity.VelocityCommandBuilder;
@Plugin(id = "example", name = "Example", version = "1.0.0")
public final class ExampleVelocityPlugin {
private final ProxyServer proxy;
@Inject
public ExampleVelocityPlugin(ProxyServer proxy) {
this.proxy = proxy;
}
@Subscribe
public void onInit(ProxyInitializeEvent event) {
VelocityCommandBuilder.create("send")
.description("Send yourself to a server")
.permission("example.send")
.argument(Args.string("server"))
.executor((sender, ctx) -> {
String server = ctx.get("server", String.class).orElse("lobby");
sender.sendMessage("§aConnecting to §e" + server);
})
.tabComplete((sender, ctx) ->
proxy.getAllServers().stream()
.map(s -> s.getServerInfo().getName())
.toList())
.register(proxy);
}
}| Platform | Builder factory | Pass to register(...)
|
Registered as |
|---|---|---|---|
| Bukkit / Spigot / Paper | BukkitCommandBuilder.create(name) |
org.bukkit.plugin.Plugin |
BukkitCommand into the server CommandMap (Brigadier detected, fallback used) |
| BungeeCord | BungeeCommandBuilder.create(name) |
net.md_5.bungee.api.plugin.Plugin |
Command + TabExecutor via the PluginManager
|
| Velocity | VelocityCommandBuilder.create(name) |
com.velocitypowered.api.proxy.ProxyServer |
SimpleCommand via the CommandManager
|
Passing the wrong handle type to register(...) throws IllegalArgumentException with a message naming the expected type.
| Method | Returns | Purpose |
|---|---|---|
description(String) |
CommandBuilder |
Set the command description. |
permission(String) |
CommandBuilder |
Set the required permission (auto-checked before the executor; null = open). |
argument(Argument<?>) |
CommandBuilder |
Append a positional, typed argument. |
executor(BiConsumer<CommandSender, CommandContext>) |
CommandBuilder |
Set the execution handler. |
tabComplete(BiFunction<CommandSender, CommandContext, List<String>>) |
CommandBuilder |
Set the optional suggestion provider. |
subcommand(String, Consumer<CommandBuilder>) |
CommandBuilder |
Declare and configure a same-platform child builder. |
register(Object) |
void |
Register on the platform; validates the handle type. (abstract) |
getName() |
String |
The command label (set at create). |
getDescription() |
String |
Current description. |
getPermission() |
String |
Current permission (may be null). |
getArguments() |
List<Argument<?>> |
Declared arguments, in order. |
getExecutor() |
BiConsumer<…> |
The execution handler (may be null). |
getTabComplete() |
BiFunction<…> |
The suggestion provider (may be null). |
getSubcommands() |
Map<String, CommandBuilder> |
Declared subcommands by name (insertion-ordered). |
| Type | Factory | Notes |
|---|---|---|
BukkitCommandBuilder |
create(String) |
register expects org.bukkit.plugin.Plugin. |
BungeeCommandBuilder |
create(String) |
register expects net.md_5.bungee.api.plugin.Plugin. |
VelocityCommandBuilder |
create(String) |
register expects com.velocitypowered.api.proxy.ProxyServer. |
| Method | Returns |
|---|---|
Args.string(String name) |
Argument<String> |
Args.integer(String name) |
Argument<Integer> |
Args.bool(String name) |
Argument<Boolean> |
Args.custom(String name, Class<T> type, Function<String, Optional<T>> parser) |
Argument<T> |
| Method | Returns | Purpose |
|---|---|---|
new Argument<>(name, type, parser) |
— | Construct directly (prefer Args). |
optional() |
Argument<T> |
Mark optional (metadata; fluent). |
getName() |
String |
Argument name (context key). |
getType() |
Class<T> |
Declared type. |
isOptional() |
boolean |
Whether marked optional. |
parse(String raw) |
Optional<T> |
Run the parser; empty on failure. |
| Method | Returns | Purpose |
|---|---|---|
put(String, Object) |
CommandContext |
Store a value (normally done by the adapter). |
get(String, Class<T>) |
Optional<T> |
Type-safe lookup; empty if absent or wrong type. |
all() |
Map<String, Object> |
Unmodifiable, insertion-ordered view of all values. |
| Method | Returns | Purpose |
|---|---|---|
getName() |
String |
Sender name ("CONSOLE" for console on Velocity). |
hasPermission(String) |
boolean |
Permission check; null permission ⇒ true. |
sendMessage(String) |
void |
Send a colour-translated message. |
raw() |
Object |
Underlying platform sender for advanced use. |
- Getting Started — repositories, dependency scopes, shading and bootstrapping.
- Messages — build coloured, multi-line, placeholder-aware feedback to send from your handlers.
Core (all platforms)
Bukkit / Paper
Commands