Skip to content

Commands

frosxt edited this page Apr 14, 2026 · 1 revision

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.

CommandService

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.

CommandDescriptor

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.

PermissionPolicy

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 only

A 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.

CommandExecutor

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.

CommandContext

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.

CommandResult

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.

CompletionProvider

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

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.

A complete pattern

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.

Clone this wiki locally