-
Notifications
You must be signed in to change notification settings - Fork 0
Commands
PrisonCore commands are described declaratively. You build a CommandDescriptor, hand it to the CommandService, and the platform takes care of registering it with Bukkit, dispatching, permission checks, and tab completion.
You don't write a plugin.yml commands: block, you don't extend BukkitCommand, and you don't touch the command map yourself.
com.github.frosxt.prisoncore.command.api.CommandService
public interface CommandService {
void register(CommandDescriptor descriptor);
void unregister(CommandKey key);
}Resolved through the service container:
final CommandService commandService = context.services().resolve(CommandService.class);register adds your command. The descriptor's subcommands come along with it. unregister removes the command and all its subcommands. Always unregister in your onDisable.
com.github.frosxt.prisoncore.command.api.CommandDescriptor
Built with CommandDescriptor.builder(namespace, name). The namespace is your module id by convention. The name is the command word the player types.
final CommandDescriptor descriptor = CommandDescriptor.builder("hello", "greet")
.aliases("hi", "wave")
.description("Greet the world")
.permission(PermissionPolicy.of("hello.greet"))
.completionProvider((ctx, argIndex) -> argIndex == 0
? Bukkit.getOnlinePlayers().stream().map(Player::getName).toList()
: List.of())
.executor(ctx -> {
final CommandSender sender = (CommandSender) ctx.sender();
sender.sendMessage("Hello!");
return new CommandResult.Success(null);
})
.build();
commandService.register(descriptor);Builder methods:
-
aliases(String...)— alternative names players can type. -
description(String)— human-readable description shown in help. -
permission(PermissionPolicy)— the permission gate. See below. -
completionProvider(CompletionProvider)— tab completion. -
executor(CommandExecutor)— what runs when the command fires. -
subcommand(CommandDescriptor)— nest another descriptor under this one.
The descriptor is immutable once built. You can re-register the same descriptor instance after unregistering.
com.github.frosxt.prisoncore.command.api.PermissionPolicy
Three factory methods:
PermissionPolicy.none(); // anyone can run it
PermissionPolicy.of("module.command"); // permission required, any sender
PermissionPolicy.playerOnly("module.command"); // permission required and player onlyA playerOnly policy fails the dispatch with the platform's "player only" message before your executor is called, so you don't have to instanceof-check inside.
com.github.frosxt.prisoncore.command.api.CommandExecutor
A functional interface:
@FunctionalInterface
public interface CommandExecutor {
CommandResult execute(CommandContext context);
}Return one of the CommandResult variants. The platform turns it into the appropriate Bukkit return value and message delivery.
com.github.frosxt.prisoncore.command.api.CommandContext
Passed to your executor and your completion provider.
public Object sender(); // cast to CommandSender on Bukkit
public String label(); // the command word as typed
public String[] args(); // raw arguments
public UUID senderId(); // player UUID, or null for console
public String arg(int index);
public int argCount();sender() is typed as Object so the api package stays Bukkit-free. On Bukkit you cast to CommandSender. The cast is safe — the runtime layer always passes a CommandSender.
senderId() is null for the console sender. Use it for null-safe checks rather than instanceof Player.
arg(int) returns null if the index is out of bounds, so you can write if (ctx.arg(0) == null) return new CommandResult.Usage(...) without a manual length check.
com.github.frosxt.prisoncore.command.api.CommandResult
A sealed interface with three variants:
public sealed interface CommandResult {
record Success(String message) implements CommandResult {}
record Error(String message) implements CommandResult {}
record Usage(String usage) implements CommandResult {}
}Success — your command did what it was asked. The optional message gets delivered to the sender. Pass null if you don't need feedback (you may have already sent a message yourself).
Error — your command failed for a reason that isn't an argument problem. The message is shown to the sender.
Usage — the caller's arguments were wrong. The message is shown as usage help.
You don't return success or failure as a boolean. Pattern-match on the result variant in tests if you need to assert behavior.
com.github.frosxt.prisoncore.command.api.CompletionProvider
@FunctionalInterface
public interface CompletionProvider {
List<String> complete(CommandContext context, int argIndex);
}argIndex is the zero-based index of the argument currently being typed. Return the candidates for that position. Return an empty list to suppress completion for that slot.
For most commands a switch on argIndex is the right shape:
.completionProvider((ctx, argIndex) -> switch (argIndex) {
case 0 -> Bukkit.getOnlinePlayers().stream().map(Player::getName).toList();
case 1 -> List.of("10", "100", "1000");
default -> List.of();
})Subcommands are just nested CommandDescriptors. The parent's executor runs only when no subcommand matches.
final CommandDescriptor parent = CommandDescriptor.builder("hello", "greet")
.description("Greeting commands")
.permission(PermissionPolicy.of("hello.greet"))
.subcommand(CommandDescriptor.builder("hello", "loud")
.permission(PermissionPolicy.of("hello.greet.loud"))
.executor(ctx -> {
final CommandSender sender = (CommandSender) ctx.sender();
sender.sendMessage("HELLO!");
return new CommandResult.Success(null);
})
.build())
.executor(ctx -> {
final CommandSender sender = (CommandSender) ctx.sender();
sender.sendMessage("Hello.");
return new CommandResult.Success(null);
})
.build();Permissions on subcommands are checked independently. A player who has hello.greet but not hello.greet.loud can run /greet but not /greet loud.
This is the structure most modules end up with:
public final class HelloModule extends AbstractPlatformModule {
private final List<CommandKey> registered = new ArrayList<>();
private CommandService commandService;
@Override
protected void onPrepare(final ModuleContext context) {
commandService = context.services().resolve(CommandService.class);
}
@Override
protected void onEnable(final ModuleContext context) {
final CommandDescriptor greet = buildGreetCommand();
commandService.register(greet);
registered.add(greet.key());
}
@Override
protected void onDisable(final ModuleContext context) {
for (final CommandKey key : registered) {
commandService.unregister(key);
}
registered.clear();
}
private CommandDescriptor buildGreetCommand() { ... }
}Hold the keys you registered, unregister them on disable, and the platform will clean up the Bukkit side for you.
This wiki documents the public API surface module authors are expected to touch. Kernel internals are intentionally omitted. Spot something wrong? Open an issue.
Start here
Architecture
Subsystems
- Commands
- Menus
- Messages and Placeholders
- Scheduler
- Storage
- Events and Listeners
- Configuration
- Player Profiles
Utilities