Commands

Commands

MagicUtils commands are annotation-first, but the runtime model is registry based. Each platform exposes a CommandRegistry that owns parsers, permissions, and command registration for that plugin or mod.

See Commands Cheat Sheet for a quick reference and Permissions for node generation details.

Why an annotation-first command framework

Registering a command by hand means parsing raw String[] args yourself, validating and converting each argument, writing tab-completion separately, checking permissions, and repeating all of it in a different shape for every platform's dispatcher. A single "give a player an amount" command turns into dozens of lines of boilerplate that has nothing to do with your feature.

MagicUtils lets you describe the command as a method: parameter types drive parsing and completion, annotations add options and permissions, and the same command class registers on Bukkit, BungeeCord, Velocity, Fabric, and NeoForge.

Before (raw Bukkit onCommand, manual parsing and checks):

java
public boolean onCommand(CommandSender s, Command c, String label, String[] args) {    if (!s.hasPermission("donate.give")) { s.sendMessage("No permission"); return true; }    if (args.length < 2) { s.sendMessage("Usage: /donate give <player> <amount>"); return true; }    Player target = Bukkit.getPlayer(args[0]);    if (target == null) { s.sendMessage("Unknown player"); return true; }    int amount;    try { amount = Integer.parseInt(args[1]); } catch (NumberFormatException e) {        s.sendMessage("Amount must be a number"); return true;    }    // ... finally, the actual logic    return true;}

After (typed parameters, generated parsing/permission/completion):

java
@SubCommand(name = "give", permission = "donate.give")public CommandResult give(        @Sender MagicSender sender,        @ParamName("player") Player target,        int amount) {    // just the logic; parsing, validation, and permission are handled    return CommandResult.success("Gave " + amount + " to " + target.getName());}

Registration Models

There are three common ways to obtain a registry:

  1. Bootstrap helper creates it for you.
  2. CommandRegistry.create(...) returns an instance you keep explicitly.
  3. Legacy CommandRegistry.initialize(...) / createDefault(...) creates the default registry for the current platform.

For multi-plugin or multi-mod setups, prefer an explicit registry instance or the scoped static overloads. The no-arg register(...) methods operate on the default registry.

Platform Registration

Bukkit/Paper

Bootstrap-first:

java
BukkitBootstrap.RuntimeResult magic = BukkitBootstrap.forPlugin(plugin)        .permissionPrefix("myplugin")        .enableCommands()        .configureCommands(registry -> registry.registerCommand(new DonateCommand()))        .buildRuntime();

Manual registry:

java
CommandRegistry registry = CommandRegistry.create(plugin, "myplugin", logger);registry.registerCommand(new DonateCommand());registry.registerCommand(new AdminCommand());

Fabric

Bootstrap-first:

java
FabricBootstrap.RuntimeResult magic = FabricBootstrap.forMod("mymod", () -> server)        .permissionPrefix("mymod")        .enableCommands()        .buildRuntime();CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {    if (magic.commandRegistry() != null) {        magic.commandRegistry().registerCommand(dispatcher, new DonateCommand());    }});

Manual registry:

java
CommandRegistry registry = CommandRegistry.create("mymod", "mymod", logger, 2);CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {    registry.registerCommand(dispatcher, new DonateCommand());});

Velocity

Bootstrap-first:

java
VelocityBootstrap.RuntimeResult magic = VelocityBootstrap.forPlugin(proxy, plugin, "MyPlugin", dataDirectory)        .permissionPrefix("myplugin")        .enableCommands()        .configureCommands(registry -> registry.registerCommand(new DonateCommand()))        .buildRuntime();

Manual registry:

java
CommandRegistry registry = CommandRegistry.create(proxy, plugin, "myplugin", loggerCore);registry.registerCommand(new DonateCommand());

NeoForge

NeoForge currently uses the manual path:

java
CommandRegistry registry = CommandRegistry.create("mymod", "mymod", loggerCore, 2);@SubscribeEventpublic void onRegisterCommands(RegisterCommandsEvent event) {    registry.registerCommand(event.getDispatcher(), new DonateCommand());}

The second argument is always the permission prefix used when generating nodes.

Brigadier Integration

On Brigadier platforms (Fabric and NeoForge) the CommandRegistry is backed by BrigadierCommandRegistry<S> from magicutils-commands-brigadier. You rarely touch it directly, but two hooks let you customise how MagicUtils maps arguments onto Brigadier:

  • parserRegistrar (a Consumer<TypeParserRegistry<S>>) registers your own type parsers so annotation and builder commands can accept custom types.
  • brigadierRegistrar (a Consumer<BrigadierArgumentRegistry<S>>) registers BrigadierArgumentResolver<S> instances that map a CommandArgument onto a native Brigadier ArgumentType, so a parameter can use real Brigadier parsing and suggestions instead of MagicUtils' string-based parsing.

A resolver returns a BrigadierArgumentShape (or null to skip):

java
registry.register(argument -> {    if (argument.getType() == GameProfile.class) {        // use a native Brigadier argument type with its own suggestions        return BrigadierArgumentShape.nativeSuggestions(GameProfileArgument.gameProfile());    }    return null; // fall back to MagicUtils parsing});

BrigadierArgumentShape.of(type) wraps a Brigadier ArgumentType without native suggestions; nativeSuggestions(type) keeps Brigadier's own completion. Higher priority() resolvers run first. Most commands never need this: the built-in type parsers already cover players, worlds, enums, numbers, and booleans.

Annotation-Based Commands

Use @CommandInfo on the class and @SubCommand on methods. A method named execute without @SubCommand is treated as the root handler.

java
@CommandInfo(        name = "donate",        description = "DonateMenu main command",        aliases = {"d"},        permission = "donate.use")public final class DonateCommand extends MagicCommand {    public CommandResult execute(@Sender MagicSender sender) {        return CommandResult.success("Opened menu");    }    @SubCommand(name = "give", description = "Give currency to a player")    public CommandResult give(            @Sender MagicSender sender,            @ParamName("player") Player target,            @Option(shortNames = {"a"}, longNames = {"amount"}) int amount,            @Option(shortNames = {"s"}, longNames = {"silent"}, flag = true) boolean silent    ) {        return CommandResult.success(silent ? "" : "Done");    }}

Nested subcommands are supported via path:

java
@SubCommand(path = {"npc", "commands"}, name = "add")public CommandResult addNpcCommand(...) { ... }

Common Annotations

  • @ParamName overrides argument names for help output.
  • @OptionalArgument or @DefaultValue("...") marks a parameter optional.
  • @Greedy captures the rest of the input.
  • @Suggest("source") adds completion hints.
  • @Sender injects the sender and hides it from help output.
  • @Option(shortNames = {"a"}, longNames = {"amount"}) enables -a 5 and --amount 5. Set flag = true for toggles such as -s / --silent.

@Sender supports sender filtering via AllowedSender:

  • ANY, PLAYER, CONSOLE
  • BLOCK, MINECART, PROXIED, REMOTE

Platform-specific sender types can also be injected directly:

  • Bukkit: CommandSender, Player
  • Fabric: ServerCommandSource, ServerPlayerEntity
  • Velocity: CommandSource, Player, ConsoleCommandSource
  • NeoForge: CommandSourceStack, ServerPlayer

Suggestions And Type Parsers

Suggestions can come from:

  • Built-in type parsers (players, worlds, enums, booleans).
  • Special sources such as @players, @worlds, @commands.
  • Inline lists: @Suggest("{on,off,reset}").
  • Methods on the command class: @Suggest("getItems").

Suggestion methods can be:

  • String[] getItems() or List<String> getItems()
  • getItems(Player player)
  • getItems(ServerCommandSource sender)
  • getItems(CommandSource sender)

@Suggest has two extra members:

  • permission = true filters the suggestions through the argument's permission, so a player only sees completions they are allowed to use.
  • contextArgs = {"world"} passes the current values of earlier arguments into the suggestion method, so later completions can depend on what was already typed.

Built-in sources:

  • @players, @player, @allplayers
  • @offlineplayers (Bukkit only)
  • @worlds, @world
  • @language_keys (Bukkit only)
  • @commands
  • {a,b,c} inline list syntax

Custom parsers are registered on the registry's parser registry:

java
registry.commandManager()        .getTypeParserRegistry()        .register(new MyTypeParser());

Permissions

Permissions can be defined at three levels:

  • @CommandInfo.permission
  • @SubCommand.permission
  • @Permission on parameters

Generated nodes use this shape when you do not provide explicit values:

  • Command: commands.<command>
  • Subcommand: commands.<command>.subcommand.<path>
  • Argument: commands.<command>.subcommand.<path>.argument.<name>

These nodes are prefixed by the registry permission prefix. See Permissions for the platform-specific behaviour.

MagicPermissionDefault controls the default access policy:

  • TRUE
  • OP
  • NOT_OP
  • FALSE

For manual checks outside annotation processing, use MagicSender:

java
MagicSender sender = MagicSender.wrap(rawSender);if (MagicSender.hasPermission(rawSender, "myplugin.admin")) {    // adapter-default fallback}if (sender != null && sender.hasPermission("myplugin.admin", 4)) {    // explicit fallback override for this check}

The overload with fallbackOpLevel is mainly useful on Fabric and NeoForge, where adapters may fall back to command-source permission levels.

CommandResult

CommandResult factories control whether MagicUtils sends feedback automatically, and whether the logger prefix is attached.

Success:

  • CommandResult.success() succeeds with no reply text.
  • CommandResult.success("Done") sends a success reply (with prefix).
  • CommandResult.success("Done", false) sends the reply without the prefix.
  • CommandResult.success(false, "Done") succeeds but suppresses the reply (the boolean is sendMessage, so false means "do not send").

Failure:

  • CommandResult.failure("No permission") sends a failure reply (with prefix).
  • CommandResult.failure("No permission", false) sends it without the prefix.
  • CommandResult.failure(false) fails silently, sending no reply.

Not found:

  • CommandResult.notFound() returns the built-in "command not found" failure.

Note the two two-argument success overloads differ by parameter order: success(String, boolean) is (message, sendPrefix), while success(boolean, String) is (sendMessage, message).

Threading

Commands run on the main thread by default. Use CommandThreading.ASYNC for IO-heavy or CPU-heavy work:

java
@CommandInfo(name = "donate", threading = CommandThreading.ASYNC)public final class DonateCommand extends MagicCommand {    public CommandResult execute(@Sender MagicSender sender) {        return CommandResult.success("done");    }    @SubCommand(name = "give", threading = CommandThreading.ASYNC)    public CommandResult give(@Sender MagicSender sender, Player target) {        return CommandResult.success("ok");    }}

Builder equivalents:

java
MagicCommand.<CommandSender>builder("donate")        .threading(CommandThreading.ASYNC)        .execute(ctx -> CommandResult.success("done"))        .build();SubCommandSpec.<CommandSender>builder("give")        .threading(CommandThreading.ASYNC)        .execute(ctx -> CommandResult.success("ok"))        .build();

Only mark commands as async when your code is thread-safe. When you need to touch platform APIs again, switch back to the main thread via Platform.runOnMain(...) or Tasks.runOnMain(...).

Help Output

The help renderer respects permissions and hides commands or arguments the sender cannot access. It is styled through logger.{ext} under the help section.

Standalone Help Command

Bukkit and Fabric ship a ready-to-register HelpCommand wrapper:

java
registry.registerCommand(new HelpCommand(logger, registry));

You can rename it at runtime:

java
registry.registerCommand(new HelpCommand(logger, registry)        .withName("donatehelp")        .addAlias("dhelp"));

Help As A Subcommand

Use HelpCommandSupport when you want help inside another command tree or on platforms that do not ship a dedicated wrapper:

java
registry.registerCommand(new DonateCommand()        .addSubCommand(HelpCommandSupport.createHelpSubCommand(                "help",                loggerCore,                registry::commandManager        )));

MagicSender

MagicSender is the platform-neutral sender wrapper used throughout the command system:

java
MagicSender sender = MagicSender.wrap(rawSender);if (MagicSender.hasPermission(rawSender, "my.permission")) {    // ...}

Use it when you want shared command logic across Bukkit, BungeeCord, Velocity, Fabric, and NeoForge without branching on raw sender types.

Builder API

Use the builder API when you need runtime composition but still want a real MagicCommand instance:

java
MagicCommand donateCommand = MagicCommand.<CommandSender>builder("donate")        .description("DonateMenu main command")        .aliases("d")        .execute(ctx -> CommandResult.success("Opened menu"))        .subCommand(SubCommandSpec.<CommandSender>builder("give")                .description("Give currency")                .argument(CommandArgument.builder("player", Player.class).build())                .argument(CommandArgument.builder("amount", Integer.class).build())                .execute(ctx -> CommandResult.success("ok"))                .build())        .build();registry.registerCommand(donateCommand);

You can mix annotations with runtime overrides:

  • withName(...), addAlias(...), removeAlias(...)
  • addSubCommand(SubCommandSpec<?>)
  • setExecute(...)
  • mount(MagicCommand) / mount("route", existingCommand)
  • mountSubCommands(MagicCommand) — graft a carrier's sub-commands in flat
  • copy() — an unfrozen duplicate you can mutate and re-register

Nested builder subcommands are supported as well:

java
SubCommandSpec<CommandSender> npcAdd = SubCommandSpec.<CommandSender>builder("add")        .path("npc", "commands")        .description("Add NPC command")        .execute(ctx -> CommandResult.success("ok"))        .build();

Composing Existing Commands

Already-authored annotation commands can be mounted under another command tree without rewriting them into SubCommandSpec form:

java
MagicCommand adminCommand = MagicCommand.<CommandSender>builder("admin")        .mount("punish", new BanCommand())        .build();registry.registerCommand(adminCommand);

This is intentionally different from withName("punish"):

  • withName(...) mutates the authored command instance itself
  • mount("punish", command) keeps the source command identity intact and only changes the route segment inside the parent tree

Mounting snapshots the child command structure at mount time. That means:

  • changing the child name or aliases later does not rewrite the already-mounted parent tree
  • the mounted command still executes against the original child instance
  • when you override the mounted route, root aliases from the child are not exposed automatically

Flat-mounting sub-commands with mountSubCommands

mount("route", cmd) nests the source command under a route segment, producing /root <route> <sub>. Sometimes you instead want another command's sub-commands to appear directly under the root, at the same level — /root <sub> — without the source command's own name becoming a nesting level. That is mountSubCommands(cmd):

java
// GreetCommand is a carrier: an @CommandInfo(name = "greet") class whose only// job is to hold @SubCommand methods (e.g. `hello`). mountSubCommands grafts// those methods straight under `root`, so `/root hello` works — `greet` never// appears as a command level.MagicCommand root = manager.getCommand("root").copy();root.mountSubCommands(new GreetCommand());manager.register(root, root.resolveInfo());

The difference from mount:

  • mount("greet", cmd)/root greet hello
  • mountSubCommands(cmd)/root hello

Only the source's sub-commands are carried over. Its own bare execute (the action bound to the source's plain name) is intentionally not grafted — use mount(...) if you want the source's top-level action too. This lets a plugin contribute top-level sub-commands to a shared root command it does not own.

Extending a registered command with copy()

Registration freezes a command, after which mount, mountSubCommands, and the other mutators throw IllegalStateException. To add sub-commands to a command tree that is already registered, copy() returns an unfrozen duplicate you can mutate and re-register under the same name — registration replaces the previous owner by name:

java
// First contributorMagicCommand first = manager.getCommand("root").copy();first.mount("punish", new BanCommand());manager.register(first, first.resolveInfo());// Second contributor — its copy already sees the first mount, and its own// additions accumulate on top.MagicCommand second = manager.getCommand("root").copy();second.mount("greet", new GreetCommand());manager.register(second, second.resolveInfo());// /root now has both `punish` and `greet`; the root's own execute survives too.

Because a copy preserves the source's mounted sub-commands, several plugins can each copy → mount → register in turn and their additions stack. The copy carries the source's info, name/alias overrides, mounted trees, and dynamic execute. Sub-commands declared as @SubCommand methods on a concrete subclass belong to that subclass, not to the copied declarative state; if such a subclass must preserve those methods across a copy, override copy() to return an instance of its own type. Commands built from a spec/builder or assembled purely by mount (such as an aggregate root) copy losslessly with the default implementation.

Mutation Lifecycle

MagicCommand is mutable only during definition and composition:

java
MagicCommand command = MagicCommand.<CommandSender>builder("donate")        .execute(ctx -> CommandResult.success("ok"))        .build()        .addAlias("d");

After registry.registerCommand(...) or commandManager.register(...), the command is frozen. Later calls to:

  • withName(...)
  • addAlias(...)
  • removeAlias(...)
  • addSubCommand(...)
  • setExecute(...)
  • mount(...) / mountSubCommands(...)

throw IllegalStateException. To extend a command after it is registered, take a copy() of it (see Extending a registered command with copy()), mutate the copy, and re-register it under the same name. If you still use registerSpec(...), it remains supported as a compatibility path and is internally converted into the same MagicCommand runtime model.

For runtime diagnostics mounted the same way, see Diagnostics.