
Prism Commands
Java mod listed for Minecraft 1.8–26.x. Downloads from the MC Java Mods app on Android.
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.

Table of contents
- Why Prism?
- Features
- Supported platforms
- Installation
- Standalone plugin
- Quick start
- Guide
- Velocity
- Platform configuration
- Built-in argument types
- Implementing your own platform
- Project structure
- Building from source
- Trying it out on a server
- Contributing
- License
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:
- Compile-time safety. Arguments, executors and requirements are real typed objects, not strings buried in annotations.
- Composability. Commands are values. Build them in loops, share argument definitions, and assemble trees programmatically.
- Discoverability. Your IDE's autocomplete is the documentation. Everything hangs off one builder.
- One API, many platforms. The core is platform-agnostic; Spigot, Paper (with native Brigadier support), Velocity and others plug in on top.
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
- 🧱 Fluent, type-safe builder API with no annotations
- 🌳 Subcommands and argument trees for arbitrarily nested command structures
- 🎯 Rich built-in arguments for players, worlds, materials, locations, entity selectors, ranges, colors, enchantments, potion effects and more
- 💡 Sync and async tab completion with optional tooltips
- 🚩 Flag support (
-c red -e) viaFlagArgument - 🔐 Permissions and usage requirements at the command and argument level
- 🧭 Per-sender-type executors for players, console, remote console, command blocks and proxied senders
- ⚡ Native Brigadier integration (client-side validation, colored syntax, native suggestions) with an automatic legacy fallback
- 🛰️ Velocity support built on the same Brigadier tree builder
- 📖 Built-in help generator that renders clickable usage for a command tree
- ♻️ Runtime registration and unregistration of commands
- 🧩 Shaded bundles so you ship a single dependency
- 🔌 Standalone plugin if you would rather not shade Prism into your jar
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
- Drop
prism-<version>.jarinto the server'spluginsfolder. - Start the server once. The plugin creates
plugins/prism/config.yml. - 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);
}
}
VelocityPlatform.configure(plugin, server)takes your plugin instance as a plainObject. Velocity has no plugin handle:@Pluginis an annotation, and the instance of your main class is the plugin.- Prism does not register any Velocity event listener.
register(...)talks to Velocity'sCommandManagerimmediately, so calling it beforeProxyInitializeEventis your responsibility to avoid. - Velocity injects the SLF4J
Logger, notjava.util.logging.Logger.
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:
- Walk the definition tree, matching subcommand names with
getChild(...). - Check
definition.canExecute(source)and report a "no permission" error if it fails. - Parse the raw tokens with each argument's
parse(source, token), honouringcanUse(source), and collect the values into two maps keyed by argument name (required and optional). CatchArgumentParseExceptionand turn it into a user-facing message. - Wrap the values in a
CommandArguments, passing the parent'sCommandArgumentsso thatgetAt("../", "key")and friends work. - Check
definition.hasExecutor(source), then calltryInvoke(source, args)on each executor fromdefinition.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,LegacyCommandRegistrarandLegacyArgumentsParserinplatform/bukkitto 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
- A JDK that Gradle can use for toolchains. The library itself targets Java 17, tests run on Java 21, and the Paper test plugin, the Velocity module and CI use Java 25. Gradle's toolchain resolver can download missing JDKs automatically.
- Gradle itself must run on JDK 25 or newer while the Velocity module is part of the build. Set the Gradle JVM in your IDE accordingly.
- No separate Gradle install is needed. Use the included wrapper.
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.
- Open an issue to discuss larger changes before starting.
- Fork the repository and create a branch from
main. - Make your change and add or update tests where it makes sense.
- Run
./gradlew testand make sure it passes. The same check runs in CI on every push and pull request. - Open a pull request describing what changed and why.
License
Prism is released under the MIT License.
Made by Gurwi · Athlas Studios
- Edition: Minecraft Java
- File type: .jar
- Minecraft versions listed: 1.8.8, 1.8.9, 1.9
- How to install: Install the matching mod loader (Forge, Fabric or NeoForge) for your Minecraft version. → Download the .jar. → Put it in the .minecraft/mods folder and launch that loader profile.
- Where to get it: Opens on Modrinth — not every file is mirrored on our own servers.
Install steps are the general flow for this file type — How to install Minecraft Java mods & modpacks walks through it step by step.
- Ships as prism-1.0.0-beta.4.jar — drop this file into the mods folder
- Download size: 902 KB
- Download link checked 25 Oct 2026 — working
These come from our own check of the pack file, not from the source page.