Home›Java›Mods›Prism Commands
Prism Commands
ModsJava

Prism Commands

Java mod listed for Minecraft 1.8–26.x. Downloads from the MC Java Mods app on Android.

by Gurwi · on Modrinth

A code-first command framework that let's you build commands directly in Java with a fluent, type-safe API. No annotations required inspired by CommandAPI…

⬇ Download on Modrinth

🌟 Prism - THE Command Framework

A code-first command framework that lets you build commands directly in Java with a fluent, type-safe API. No annotations required.

Prism Hero Image

Maven Central Build Java License

Stars Issues Last commit Paper Spigot Velocity

Documentation · Issues


Table of contents


Why Prism?

Most command frameworks lean on annotations and reflection: you describe a command with a pile of annotations, and the framework figures out the rest at runtime. Prism takes the opposite approach. A command is plain Java code built with a fluent builder, so you get:

new PrismBukkitCommand("heal")
        .withDescription("Heal a player")
        .withPermission("myplugin.heal")
        .withArguments(new PlayerArgument("target"))
        .executes((sender, args) -> {
            Player target = args.get("target");
            target.setHealth(20);
            sender.sendMessage("Healed " + target.getName());
        })
        .register(platform);

Features

Supported platforms

Platform Module Status
Spigot (and forks) prism-spigot Supported
Paper (and forks) prism-paper Supported
Brigadier (Minecraft 1.13 → 26.x) bundled in the Spigot and Paper builds Supported
Velocity platform/velocity 🧪 Experimental
JDA (Discord) platform/jda 🚧 In development

Prism compiles against the Spigot 1.13.2 API, and the Brigadier layer has per-version adapters for Minecraft 1.13 through 1.21 and 26.x. If Brigadier can't be initialized on your server version, Prism logs a warning and falls back to legacy registration, so commands keep working.

The Velocity module targets Java 25 and the Velocity 4.x API. Velocity has no legacy fallback: commands are always registered through Brigadier.

Installation

Prism is currently in beta (1.0.0-beta.4). APIs may still change between beta releases.

Group ID: dev.gurwi

Server software Artifact ID
Paper prism-paper
Spigot prism-spigot

Each artifact is a self-contained bundle that already includes the core, Bukkit layer and the matching Brigadier adapters, so you only add one dependency.

Gradle (Kotlin DSL)

repositories {
    mavenCentral()
}

dependencies {
    implementation("dev.gurwi:prism-paper:1.0.0-beta.4")
}

Gradle (Groovy DSL)

repositories {
    mavenCentral()
}

dependencies {
    implementation 'dev.gurwi:prism-paper:1.0.0-beta.4'
}

Maven

<dependency>
    <groupId>dev.gurwi</groupId>
    <artifactId>prism-paper</artifactId>
    <version>1.0.0-beta.4</version>
</dependency>

Swap prism-paper for prism-spigot if you target Spigot. The individual modules (prism-core, prism-bukkit, prism-brigadier and prism-brigadier-minecraft) are published too, but most projects should use a bundle.

Prism is a library, not a standalone plugin, so shade it into your own plugin jar (for example with the Shadow plugin) and relocate it to avoid conflicts with other plugins that bundle Prism.

Don't want to shade? Install Prism as a standalone plugin instead, see Standalone plugin.

Standalone plugin

Instead of shading Prism into every plugin, you can install it once on the server as a plugin named prism. Your plugins then depend on it, use one shared copy of the library and get their commands registered through the Prism API. This is the Bukkit plugin in plugin/bukkit-plugin, built on the Paper platform.

Install the plugin

  1. Drop prism-<version>.jar into the server's plugins folder.
  2. Start the server once. The plugin creates plugins/prism/config.yml.
  3. Add Prism as a dependency of every plugin that uses it.

The plugin also adds a /prism command that shows basic info and links to this repository. It can be turned off in the config.

Use it from your plugin

Declare the dependency in your plugin.yml, so Prism always loads first:

name: MyPlugin
main: com.example.myplugin.MyPlugin
version: 1.0.0
depend: [prism]

Compile against Prism without shading it. The API classes come from the api module, which builds to prism-<version>-API.jar, and the command classes come from the matching Prism module:

dependencies {
    compileOnly("dev.gurwi:prism-paper:1.0.0-beta.4")
    compileOnly(files("libs/prism-1.0.0-beta.4-API.jar"))
}

Everything is compileOnly: at runtime the classes come from the installed Prism plugin, so there is nothing to shade or relocate.

There are two ways to register commands: let Prism pick them up from a command provider, or use the API and a registry yourself. The registry route comes first.

Get the API through PrismPluginAPI.getAPI(). It returns null when Prism is not installed or has not registered its service yet.

public class MyPlugin extends JavaPlugin {

    @Override
    public void onEnable() {
        PrismPluginAPI api = PrismPluginAPI.getAPI();

        if (api == null) {
            getLogger().severe("Prism is not installed");
            getServer().getPluginManager().disablePlugin(this);
            return;
        }

        BukkitCommandsRegistry<?> registry = api.createRegistry(this);

        registry.register(
                new PrismBukkitCommand("hello")
                        .executes((sender, args) -> sender.sendMessage("Hello!"))
                        .build()
        );
    }
}

Commands are built with the same PrismBukkitCommand API as in the Guide. Instead of .register(platform) you pass the built definition to a registry.

Method Description
PrismPluginAPI.getAPI() The API instance, or null if Prism is missing.
createRegistry(plugin) Creates a registry for your plugin, configured from Prism's config.yml.
getDefaultRegistry() The registry of Prism's own platform, shared by all plugins.
registerProvider(plugin, provider, registry) Registers a command provider. registry may be null.
getRegistriesManager() createRegistry, getRegistry(plugin) and unloadRegistry(plugin).

Prefer createRegistry(this) so your commands are tracked per plugin.

Command providers

If you only want commands registered, implement PrismCommandProvider on your main class and return them from getCommands(). Prism detects the provider when your plugin enables, builds every command and registers it. There is no registry code in your plugin.

public class MyPlugin extends JavaPlugin implements PrismCommandProvider {

    @Override
    public Collection<? extends PrismBukkitCommand> getCommands() {
        return List.of(
                new PrismBukkitCommand("hello")
                        .executes((sender, args) -> sender.sendMessage("Hello!")),

                new PrismBukkitCommand("ping")
                        .executes((sender, args) -> sender.sendMessage("Pong!"))
        );
    }
}

Return the builders, not built definitions: Prism calls build() itself.

By default each plugin gets its own registry, created from Prism's config.yml, and its commands are unregistered when the plugin disables. To choose the registry yourself, override getRegistry():

@Override
public BukkitCommandsRegistry<?> getRegistry() {
    return PrismPluginAPI.getAPI().getDefaultRegistry();
}

A provider does not have to be your main class. Register any provider by hand with registerProvider:

PrismPluginAPI.getAPI().registerProvider(this, () -> List.of(
        new PrismBukkitCommand("hello")
                .executes((sender, args) -> sender.sendMessage("Hello!"))
), null);

Passing null as the registry uses the provider's getRegistry(), then falls back to a per-plugin registry. Your plugin must still declare depend: [prism]; Prism logs a warning for providers that don't.

Plugin configuration

plugins/prism/config.yml controls how Prism creates its platform and the registries it hands out:

Option Default Description
disableInfoCommand false Don't register the /prism info command.
disableBrigadierRegistrar false Skip Brigadier and register through the legacy system only.
disableLegacyRegistrar false Disable the legacy registrar.
disableAsyncSuggestions false Don't register the async tab-completion listener.
registerPermissions false Register command permissions with the server.

These map to the options in Platform configuration and apply to every plugin using the standalone Prism.

Quick start

1. Create the platform

Do this once, in onEnable.

Paper

public class MyPlugin extends JavaPlugin {

    private PaperPlatform platform;

    @Override
    public void onEnable() {
        this.platform = PaperPlatform.configure(this).init();
    }
}

Spigot

public class MyPlugin extends JavaPlugin {

    private SpigotPlatform platform;

    @Override
    public void onEnable() {
        this.platform = SpigotPlatform.configure(this).init();
    }
}

Velocity is set up differently, see Velocity.

2. Build and register a command

new PrismBukkitCommand("teleport", "tp")
        .withDescription("Teleport yourself to a location")
        .withPermission("myplugin.teleport")
        .withArguments(new LocationArgument("location"))
        .executesPlayer((player, args) -> {
            Location location = args.get("location");
            player.teleport(location);
            player.sendMessage("Teleported!");
        })
        .register(platform);

That's it. The first argument is the command name; any further strings are aliases. Tab completion, argument parsing and error messages come for free.

Guide

The guide uses the Bukkit API (PrismBukkitCommand). The builder methods (withArguments, withOptionalArguments, withSubCommands, then, withPermission, withRequirement, register) are inherited from the platform-agnostic core and work the same on every platform. Only the executors, senders, arguments and permission helpers differ.

Arguments

Required arguments are added with withArguments(...). They are parsed in order, and values are read back by name from the CommandArguments object passed to your executor.

new PrismBukkitCommand("give")
        .withArguments(
                new PlayerArgument("target"),
                new MaterialArgument("material"),
                new IntegerArgument("amount", 1, 64)
        )
        .executes((sender, args) -> {
            Player target = args.get("target");
            Material material = args.get("material");
            int amount = args.get("amount");

            target.getInventory().addItem(new ItemStack(material, amount));
        })
        .register(platform);

args.get(...) is generic, so the target type is inferred from the variable you assign it to. You can also pass the argument object itself instead of its name:

IntegerArgument amount = new IntegerArgument("amount", 1, 64);
// ...
int value = args.get(amount);

Optional arguments

Optional arguments are declared with withOptionalArguments(...) and always come after the required ones.

new PrismBukkitCommand("number")
        .withArguments(new IntegerArgument("number", 0, 10))
        .withOptionalArguments(new FloatArgument("float", 0.1f, 9.9f))
        .executes((sender, args) -> {
            int number = args.get("number");
            float scale = args.getOptional("float", 1.0f); // with a default
            sender.sendMessage(number + " x " + scale);
        })
        .register(platform);

getOptional(key) returns an Optional<T>, and getOptional(key, defaultValue) returns the value or your default.

Subcommands

Nest commands with withSubCommands(...). A subcommand is just another PrismBukkitCommand.

new PrismBukkitCommand("team", "t")
        .withDescription("Team management")
        .withSubCommands(
                new PrismBukkitCommand("create")
                        .withArguments(new StringArgument("name"))
                        .executes((sender, args) -> {
                            sender.sendMessage("Created team " + args.get("name"));
                        }),

                new PrismBukkitCommand("join")
                        .withArguments(new StringArgument("name"))
                        .executesPlayer((player, args) -> {
                            player.sendMessage("Joined team " + args.get("name"));
                        })
        )
        .register(platform);

A subcommand can read arguments from its parents by path, using getAt with a ../-style path:

new PrismBukkitCommand("paint")
        .withArguments(new ColorArgument("color"))
        .withSubCommands(
                new PrismBukkitCommand("call")
                        .executes((sender, args) -> {
                            // Read "color" from the parent command
                            sender.sendMessage("Color: " + args.getAt("../", "color"));
                        })
        )
        .register(platform);

CommandArguments also exposes the definition chain (getParent(), getRoot(), getChain(), getChildDefinitions()) if you need to inspect where you are in the tree.

Argument trees

When a command needs different follow-up arguments depending on the path taken, use then(...). Each branch can carry its own executor.

new PrismBukkitCommand("prism", "pri")
        .withArguments(new ItemStackPredicateArgument("predicate"))
        .then(
                new MaterialArgument("material")
                        .executes((sender, args) -> {
                            Material material = args.get("material");
                            sender.sendMessage("Selected material " + material);
                        })
        )
        .then(
                new GameModeArgument("gamemode")
                        .executes((sender, args) -> {
                            GameMode gameMode = args.get("gamemode");
                            sender.sendMessage("Selected game mode " + gameMode);
                        })
        )
        .register(platform);

Branches can be nested further by calling .then(...) on an argument. Argument names must be unique among siblings, and Prism throws an IllegalArgumentException if you reuse one or try to add an argument beneath itself.

Arguments that are not listed (setListed(false), as LiteralArgument is) are matched but never stored in CommandArguments.

Executors and senders

Pick the executor that matches who may run the command. Prism only invokes the executor that fits the sender, so you don't need instanceof checks.

Method Sender type
executes(...) Any CommandSender
executesPlayer(...) Player
executesConsole(...) ConsoleCommandSender
executesRemoteConsole(...) RemoteConsoleCommandSender
executesProxy(...) ProxiedCommandSender
executesBlock(...) BlockCommandSender

You can register several executors on the same command, one per sender type:

new PrismBukkitCommand("whoami")
        .executesPlayer((player, args) -> player.sendMessage("You are " + player.getName()))
        .executesConsole((console, args) -> console.sendMessage("You are the console"))
        .register(platform);

Use clearExecutors() to remove all executors from a builder.

A sender that has no matching executor gets "This command cannot be run by this sender" on Brigadier platforms, and the command is hidden from its suggestions.

Permissions and requirements

Permissions are checked against the sender (operators always pass on Bukkit platforms):

new PrismBukkitCommand("admin")
        .withPermission("myplugin.admin", "myplugin.staff")
        // ...

For wildcard-style checks, BukkitPermission.startsWith("myplugin.") matches any effective permission with that prefix. Velocity has no equivalent, see Velocity permissions.

Requirements are arbitrary predicates on the sender. Sender-specific shortcuts are available:

new PrismBukkitCommand("fly")
        .withPlayerRequirement(player -> player.getWorld().getName().equals("lobby"))
        .executesPlayer((player, args) -> player.setAllowFlight(true))
        .register(platform);

Available helpers: withRequirement, withPlayerRequirement, withConsoleRequirement, withRemoteConsoleRequirement, withProxyRequirement and withBlockRequirement. Requirements can also be attached to individual arguments with withRequirement(...), and arguments can be hidden from listings with setListed(false).

Suggestions and tab completion

Every built-in argument ships with sensible default suggestions. Replace them with replaceSuggestions(...).

Static strings

new IntegerArgument("number", 0, 10).replaceSuggestions("5", "10");

Synchronous, computed suggestions (with tooltips)

new StringArgument("kit").replaceSuggestions(
        BukkitSuggestionsSupplier.sync(ctx -> List.of(
                ArgumentSuggestion.of("starter", "The default starter kit"),
                ArgumentSuggestion.of("vip", "Perks for VIP players")
        ))
);

Asynchronous suggestions (for database or network lookups) return a CompletableFuture:

new StringArgument("player").replaceSuggestions(
        BukkitSuggestionsSupplier.async(ctx -> CompletableFuture.supplyAsync(() ->
                List.of(ArgumentSuggestion.of("alpha", "first async value"))
        ))
);

The SuggestionContext passed in gives you access to the current raw input (ctx.getCurrentRawArg()) so you can filter results. Helpers BukkitSuggestionsSupplier.strings(...) and stringsAsync(...) cover the plain-string cases.

On Brigadier platforms Prism also filters suggestions by the text already typed (case-insensitive prefix match), so suppliers can return their full list.

On Paper, async suggestions are handled through a dedicated listener. You can turn this off with disableAsyncSuggestions() (see Platform configuration).

Flags

FlagArgument parses -x-style flags in any order. Flags can carry a value (using any other argument type) or act as simple on/off switches.

new PrismBukkitCommand("flagger")
        .withArguments(
                new FlagArgument("flags").flags(
                        Flag.valued(new ColorArgument("color"), 'c'),
                        Flag.presence("enable", 'e')
                )
        )
        .executesPlayer((player, args) -> {
            ParsedFlags flags = args.get("flags");
            Optional<ChatColor> color = flags.get("color");

            color.ifPresent(c -> player.sendMessage("Color: " + c.name()));
        })
        .register(platform);

Built-in help

CommandHelp renders a clickable usage list for the current command, including its subcommands and argument-tree branches. It only shows entries the sender is actually allowed to run.

new PrismBukkitCommand("myplugin")
        .withSubCommands(/* ... */)
        .executesPlayer((player, args) -> CommandHelp.help(player, args))
        .register(platform);

Pass two extra ChatColor values to CommandHelp.help(sender, args, primary, secondary) to restyle it.

Registering and unregistering

A command can be registered in two equivalent ways:

// 1. Directly from the builder
BukkitCommandDefinition definition = new PrismBukkitCommand("hello")
        .executes((sender, args) -> sender.sendMessage("Hello!"))
        .register(platform);

// 2. Build first, register later
BukkitCommandDefinition built = new PrismBukkitCommand("hello")
        .executes((sender, args) -> sender.sendMessage("Hello!"))
        .build();

platform.getCommandsRegistry().register(built);
platform.getCommandsRegistry().unregister(built);

getRegisteredCommands() returns an unmodifiable view of everything registered on the platform.

Velocity

The Velocity platform (platform/velocity) reuses the core builder and the Brigadier tree builder, so commands, subcommands, argument trees, optional arguments and suggestions work as described in the guide. The command class is VelocityCommand, and the sender is Velocity's CommandSource.

Setup

Create the platform in your plugin constructor and register commands from your ProxyInitializeEvent handler or later.

@Plugin(id = "my-plugin")
public class MyPlugin {

    private final ProxyServer server;
    private final VelocityPlatform platform;

    @Inject
    public MyPlugin(ProxyServer server) {
        this.server = server;
        this.platform = VelocityPlatform.configure(this, server).init();
    }

    @Subscribe
    public void onProxyInit(ProxyInitializeEvent event) {
        new VelocityCommand("hello")
                .executes((source, args) -> {
                    source.sendPlainMessage("Hello World!");
                })
                .register(platform);
    }
}

Permissions

Velocity has no operator system, so there is no OP permission. Permissions go through CommandSource#hasPermission, and ConsoleCommandSource always passes.

VelocityPermission.of("myplugin.admin");
VelocityPermission.of("myplugin.admin", "myplugin.staff");

Without a permission plugin such as LuckPerms, Velocity's default provider returns true for every hasPermission call, so any player passes any permission check. Don't rely on permissions for anything sensitive unless a permission plugin is installed.

Velocity cannot enumerate the permissions a source holds, so a prefix check works against a known set of nodes instead of the sender's effective permissions:

VelocityPermission.startsWith("myplugin.admin.", knownPermissions);

knownPermissions is the collection of nodes your commands declare. Only nodes in that collection are tested, and each one is checked with hasPermission. Use the LuckPerms API directly if you need to match arbitrary nodes at runtime.

Differences from Bukkit

Topic Bukkit platforms Velocity
Command class PrismBukkitCommand VelocityCommand
Sender CommandSender CommandSource
Plugin handle JavaPlugin Object (your plugin instance)
Registration timing onEnable ProxyInitializeEvent or later
Operators Always pass permission checks No operator concept
startsWith permission Matches effective permissions Matches a supplied set of known nodes
Brigadier fallback Legacy registrar None, Brigadier only
Suggestion tooltips LiteralMessage VelocityBrigadierMessage

Platform configuration

Both PaperPlatform.configure(plugin) and SpigotPlatform.configure(plugin) return a builder. Finish with .init(). VelocityPlatform.configure(plugin, server) works the same way.

Option Platform Description
disableBrigadier() Paper, Spigot Skip Brigadier and register through the legacy command system only.
disableLegacyRegistrar() Paper, Spigot Disable the legacy registrar.
disableAsyncSuggestions() Paper Don't register the async tab-completion listener.
registerPermissions(boolean) Paper, Spigot Toggle command permission registration. Defaults to true.
withGlobalLegacyCommandErrorHandler(handler) Paper, Spigot Set the default handler for argument-parse errors on legacy-registered commands.
PaperPlatform platform = PaperPlatform.configure(this)
        .disableAsyncSuggestions()
        .registerPermissions(false)
        .init();

Individual commands can override the error handler with replaceErrorHandler(...). Prism ships LegacyCommandExecutionErrorHandler.defaultHandler() and consoleHandler(), and you can implement the interface yourself.

Built-in argument types

All of these live in dev.gurwi.prism.bukkit.argument.

Category Arguments
Primitives StringArgument, GreedyStringArgument, MultiStringArgument, CharacterArgument, BooleanArgument, ByteArgument, ShortArgument, IntegerArgument, LongArgument, FloatArgument, DoubleArgument, UUIDArgument
Ranges ByteRangeArgument, ShortRangeArgument, IntegerRangeArgument, LongRangeArgument, FloatRangeArgument, DoubleRangeArgument, NumberRangeArgument
Players and entities PlayerArgument, OfflinePlayerArgument, EntityTypeArgument, EntitySelectorArgument (e.g. ManyEntities)
World and position WorldArgument, LocationArgument, ColumnLocationArgument, BlockPositionArgument, ColumnBlockPositionArgument, VectorArgument, RotationArgument, AngleArgument, AxesArgument
Blocks and items MaterialArgument, BlockStateArgument, BlockPredicateArgument, ItemStackPredicateArgument, EnchantmentArgument
Game enums and registries GameModeArgument, DifficultyArgument, WeatherArgument, BiomeArgument, PotionEffectTypeArgument, ParticleArgument, SoundArgument, DisplaySlotArgument, NamespacedKeyArgument, MirrorArgument, StructureRotationArgument, EnumArgument
Text and color ColorArgument, HexColorArgument, MessageArgument, TimeArgument
Structure LiteralArgument, FlagArgument

Velocity arguments live in dev.gurwi.prism.velocity.argument and extend VelocityArgument. Each one wraps a Brigadier ArgumentType.

Need something custom? Extend BukkitArgument (or VelocityArgument on Velocity, or one of the base argument classes) and implement parse(...), and optionally provide default suggestions.

Greedy arguments must be the last argument of a command and cannot have children.

Implementing your own platform

core knows nothing about Minecraft, Discord or any other host. A platform is a thin layer that supplies six things on top of it, and everything else (the fluent builder, argument trees, subcommands, requirements, CommandArguments) comes for free. The platform/bukkit module is the full-featured reference, platform/velocity is a compact Brigadier-only reference, and platform/jda is a minimal skeleton that follows the same layout.

Add the core module (it is published on its own):

dependencies {
    api("dev.gurwi:prism-core:1.0.0-beta.4")
}

The examples below use an imaginary chat service where the command source is an AcmeUser.

1. Executor

An executor is a functional interface over your source type and your definition type.

@FunctionalInterface
public interface AcmeExecutor extends CommandExecutor<AcmeUser, AcmeCommandDefinition> {
}

If your host has several kinds of senders (like Bukkit's player, console and command block), declare one executor interface per kind and register them under their own class, as BukkitExecutorContainerBuilder does.

2. Permissions

Implement PermissionNode<Permission, Source>. permission() returns your host's native permission value, and hasPermission(...) answers for a given source.

public record AcmePermission(String node) implements PermissionNode<String, AcmeUser> {

    @Override
    public String permission() {
        return node;
    }

    @Override
    public boolean hasPermission(AcmeUser user) {
        return user.hasPermission(node);
    }
}

3. Arguments and suggestions

Extend AbstractArgument with your source type and a SuggestionsSupplier. The one method you must implement is parse. Throw ArgumentParseException for invalid input.

@FunctionalInterface
public interface AcmeSuggestions<T> extends SuggestionsSupplier<AcmeUser, Collection<T>> {
}

public abstract class AcmeArgument<Impl extends AcmeArgument<Impl, T>, T>
        extends AbstractArgument<Impl, T, AcmeUser, AcmeSuggestions<T>> {

    public AcmeArgument(String name, AcmeSuggestions<T> defaultSuggestions) {
        super(name, defaultSuggestions);
    }
}

public class AcmeIntegerArgument extends AcmeArgument<AcmeIntegerArgument, Integer> {

    public AcmeIntegerArgument(String name) {
        super(name, ctx -> List.of());
    }

    @Override
    public Integer parse(AcmeUser source, String value) throws ArgumentParseException {
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            throw new ArgumentParseException(this, value, "'" + value + "' is not a number");
        }
    }
}

If you want then(...) argument trees on your platform, extend AbstractTreeArgument instead and AbstractTreeCommandBuilder / AbstractTreeCommandDefinition in the next steps (this is what the Bukkit module does).

Arguments also expose canUse(source), which a platform must honour, and isListed(), which decides whether a parsed value is stored in CommandArguments.

4. Command definition

The definition is the immutable, built form of a command. Subclass AbstractCommandDefinition and bind its type parameters (self type, source, executor, argument, permission). The builder is the only thing that constructs it.

public class AcmeCommandDefinition extends AbstractCommandDefinition<
        AcmeCommandDefinition, AcmeUser, AcmeExecutor, AcmeArgument<?, ?>, AcmePermission> {

    protected AcmeCommandDefinition(@Nullable AcmeCommandDefinition parent, String name, List<String> aliases,
                                    String description, UsageRequirement<AcmeUser> requirement,
                                    Set<AcmePermission> permissions,
                                    List<AcmeArgument<?, ?>> requiredArguments,
                                    List<AcmeArgument<?, ?>> optionalArguments,
                                    Map<Class<? extends AcmeExecutor>, AcmeExecutor> executors,
                                    CommandChildrenFactory<AcmeCommandDefinition> childrenFactory) {
        super(parent, name, aliases, description, requirement, permissions,
                requiredArguments, optionalArguments, executors, childrenFactory);
    }
}

The base class already gives you getName(), getAliases(), getChildren(), getChild(name), getParent(), getExecutors(), hasExecutor(source) and canExecute(source) (requirement plus all permissions).

5. Builder

Extend AbstractCommandBuilder, implement build(parent), and add whatever convenience methods your executors need.

public class AcmeCommandBuilder extends AbstractCommandBuilder<
        AcmeCommandBuilder, AcmeCommandDefinition, AcmeUser, AcmeExecutor, AcmeArgument<?, ?>, AcmePermission> {

    public AcmeCommandBuilder(String name, String... aliases) {
        super(name, aliases);
    }

    public AcmeCommandBuilder executes(AcmeExecutor executor) {
        return executes(AcmeExecutor.class, executor);
    }

    @Override
    public AcmeCommandDefinition build(@Nullable AcmeCommandDefinition parent) {
        return new AcmeCommandDefinition(parent, name, aliases, description, requirement, permissions,
                requiredArguments, optionalArguments, executors, CommandChildrenFactory.create(children));
    }
}

withDescription, withArguments, withOptionalArguments, withPermission, withRequirement, withSubCommands and register(...) are all inherited. register(...) passes the built definition to your registry, so the definition must never be null.

6. Platform and registry

The platform identifies itself and exposes its registry. The registry is where the work happens: it stores definitions and connects them to your host.

public class AcmePlatform implements Platform<AcmeCommandsRegistry> {

    private final AcmeCommandsRegistry registry = new AcmeCommandsRegistry(this);

    @Override
    public String getIdentifier() {
        return "acme";
    }

    @Override
    public AcmeCommandsRegistry getCommandsRegistry() {
        return registry;
    }
}

public class AcmeCommandsRegistry implements CommandsRegistry<AcmePlatform, AcmeCommandDefinition> {

    private final AcmePlatform platform;
    private final Set<AcmeCommandDefinition> commands = new HashSet<>();

    public AcmeCommandsRegistry(AcmePlatform platform) {
        this.platform = platform;
    }

    @Override
    public void register(AcmeCommandDefinition definition) {
        commands.add(definition);
        // Tell your host about the command (name, aliases, description, ...)
    }

    @Override
    public void unregister(AcmeCommandDefinition definition) {
        commands.remove(definition);
        // Remove it from your host
    }

    @Override
    public @Unmodifiable Set<AcmeCommandDefinition> getRegisteredCommands() {
        return Set.copyOf(commands);
    }

    @Override
    public AcmePlatform getPlatform() {
        return platform;
    }
}

A registry should not register host event listeners on its own. Leave that to the user of your platform, as the Velocity registry does.

7. Dispatching input

If your host has no command system of its own, Prism does not parse or run anything by itself. When your host receives input for a registered command, your registry (or a helper it owns) does the following:

  1. Walk the definition tree, matching subcommand names with getChild(...).
  2. Check definition.canExecute(source) and report a "no permission" error if it fails.
  3. Parse the raw tokens with each argument's parse(source, token), honouring canUse(source), and collect the values into two maps keyed by argument name (required and optional). Catch ArgumentParseException and turn it into a user-facing message.
  4. Wrap the values in a CommandArguments, passing the parent's CommandArguments so that getAt("../", "key") and friends work.
  5. Check definition.hasExecutor(source), then call tryInvoke(source, args) on each executor from definition.getExecutors().
CommandArguments<AcmeCommandDefinition, AcmeUser> args =
        new CommandArguments<>(definition, requiredValues, optionalValues, parentArgs);

if (!definition.hasExecutor(user)) {
    user.sendMessage("This command cannot be run here.");
    return;
}

definition.getExecutors().forEach(executor -> executor.tryInvoke(user, args));

For suggestions, call each argument's getSuggestionsSupplier().getSuggestions(new SuggestionContext<>(source, currentInput, currentRawArg)) and translate the result into whatever your host expects.

8. Brigadier hosts (optional)

If your host already speaks Brigadier (Minecraft servers, Velocity), skip step 7. Implement BrigadierAdapter<Source, Definition> and let BrigadierTreeBuilder build the command tree, parse the hierarchy and wire up suggestions.

Method Purpose
visible(definition, source) Whether the source may see the command at all.
available(argument, source) Whether the source may use an argument node.
forcedValues(argument) Fixed values an argument expands to. Defaults to none.
accepts(argument, source, word) Whether a single word parses for the argument.
nativeType(argument) The Brigadier ArgumentType backing the argument, or null for literals.
tokens(argument) How many input words the argument consumes.
greedy(argument) / literal(argument) Node kind.
branches(definition) / children(argument) The then(...) tree of a command or an argument.
command(...) The Brigadier Command for a definition or a path inside its argument tree.
suggestions(argument, nativeType) The SuggestionProvider for an argument, falling back to nativeType.

Your registry then turns each definition into Brigadier nodes:

for (String label : adapter.getBuilder().labels(definition)) {
    LiteralCommandNode<CommandSource> node = adapter.getBuilder().build(definition, label);
    BrigadierCommand command = new BrigadierCommand(node);

    CommandMeta meta = manager.metaBuilder(command)
            .plugin(platform.getPlugin())
            .build();

    manager.register(meta, command);
}

Read BukkitCommandDefinitionAdapter (platform/brigadier/minecraft) and VelocityCommandDefinitionAdapter (platform/velocity) for two complete adapters.

Using it

Once those pieces exist, users of your platform get the same fluent API as everyone else:

AcmePlatform platform = new AcmePlatform();

new AcmeCommandBuilder("greet")
        .withArguments(new AcmeIntegerArgument("times"))
        .executes((user, args) -> {
            int times = args.get("times");
            user.sendMessage("Hello! ".repeat(times));
        })
        .register(platform);

Tip: read BukkitCommandsRegistry, LegacyCommandRegistrar and LegacyArgumentsParser in platform/bukkit to see a complete implementation of steps 6 and 7, including tab completion, flags and greedy arguments.

Project structure

Prism is a multi-module Gradle build:

prism
├── core/                  Platform-agnostic command model: builders, arguments, executors, permissions
├── reflection/            Small reflection helpers shared across platform modules
├── platform/
│   ├── bukkit/            Shared Bukkit API layer: PrismBukkitCommand, arguments, executors, legacy registrar
│   ├── spigot/            Spigot platform and registry
│   ├── paper/             Paper platform and registry (async suggestions, Paper-native Brigadier)
│   ├── velocity/          Velocity platform and registry (VelocityCommand, VelocityPermission, Brigadier adapter)
│   ├── brigadier/         Platform-independent Brigadier tree builder and adapter
│   │   └── minecraft/     Minecraft-facing Brigadier bridge
│   │       ├── V1_13 … V1_21, V26   Per-version adapters
│   │       └── brigadier-paper      Paper-native adapter
│   └── jda/               Discord (JDA) platform, in development
├── publish/
│   ├── paper/             Aggregates modules into the `prism-paper` bundle
│   └── spigot/            Aggregates modules into the `prism-spigot` bundle
├── gradle-plugin/
│   └── spigot-nms/        Gradle plugin for Spigot NMS reobfuscation
├── buildSrc/              Shared publishing conventions
├── plugin/
│   └── bukkit-plugin/     Standalone Prism plugin (`api/` for dependents, `plugin/` for the jar)
├── examples/
│   ├── paper-plugin/      Paper example plugin
│   └── velocity-plugin/   Velocity example plugin
└── test/plugin/           Paper test plugin used to exercise the API on a live server

Building from source

Requirements

git clone https://github.com/AthlasStudios/prism.git
cd prism

# Compile everything and build the jars
./gradlew build

# Run the test suite
./gradlew test

On Windows, use gradlew.bat instead of ./gradlew.

Tests use JUnit with MockBukkit.

Trying it out on a server

The examples module is a sample plugin that exercises most of the API (subcommands, argument trees, flags, async and sync suggestions, and many built-in argument types). Start a local Paper server with it installed:

./gradlew :test:plugin:runServer

To produce the plugin jar instead:

./gradlew :test:plugin:shadowJar

Its source, TestPlugin.java, doubles as a handy set of usage examples.

For Velocity, examples/velocity-plugin starts a local Velocity proxy with a minimal plugin:

./gradlew :examples:velocity-plugin:runVelocity

Contributing

Contributions, bug reports and ideas are welcome.

  1. Open an issue to discuss larger changes before starting.
  2. Fork the repository and create a branch from main.
  3. Make your change and add or update tests where it makes sense.
  4. Run ./gradlew test and make sure it passes. The same check runs in CI on every push and pull request.
  5. Open a pull request describing what changed and why.

License

Prism is released under the MIT License.


Made by Gurwi · Athlas Studios

Quick facts

Install steps are the general flow for this file type — How to install Minecraft Java mods & modpacks walks through it step by step.

File checked by MCModsHub

These come from our own check of the pack file, not from the source page.

Explore more