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):
After (typed parameters, generated parsing/permission/completion):
Registration Models
There are three common ways to obtain a registry:
- Bootstrap helper creates it for you.
CommandRegistry.create(...)returns an instance you keep explicitly.- 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:
Manual registry:
Fabric
Bootstrap-first:
Manual registry:
Velocity
Bootstrap-first:
Manual registry:
NeoForge
NeoForge currently uses the manual path:
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(aConsumer<TypeParserRegistry<S>>) registers your own type parsers so annotation and builder commands can accept custom types.brigadierRegistrar(aConsumer<BrigadierArgumentRegistry<S>>) registersBrigadierArgumentResolver<S>instances that map aCommandArgumentonto a native BrigadierArgumentType, 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):
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.
Nested subcommands are supported via path:
Common Annotations
@ParamNameoverrides argument names for help output.@OptionalArgumentor@DefaultValue("...")marks a parameter optional.@Greedycaptures the rest of the input.@Suggest("source")adds completion hints.@Senderinjects the sender and hides it from help output.@Option(shortNames = {"a"}, longNames = {"amount"})enables-a 5and--amount 5. Setflag = truefor toggles such as-s/--silent.
@Sender supports sender filtering via AllowedSender:
ANY,PLAYER,CONSOLEBLOCK,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()orList<String> getItems()getItems(Player player)getItems(ServerCommandSource sender)getItems(CommandSource sender)
@Suggest has two extra members:
permission = truefilters 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.
Method arity with contextArgs
A no-argument suggestion method is tried
first; only if none exists does the resolver call a method built from the
context. When you use contextArgs, the method receives each context value in
order, followed by the current partial input as a trailing String. So a
suggestion for an argument declared with contextArgs = {"server"} must take two
parameters, not one:
A one-parameter suggestWorlds(String server) silently returns nothing here,
because the resolver looks for a method matching (server, currentInput) and
finds no match. The optional sender/player parameter, when present, comes before
the context values.
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:
Permissions
Permissions can be defined at three levels:
@CommandInfo.permission@SubCommand.permission@Permissionon 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:
TRUEOPNOT_OPFALSE
For manual checks outside annotation processing, use MagicSender:
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 issendMessage, sofalsemeans "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:
Builder equivalents:
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:
You can rename it at runtime:
Help As A Subcommand
Use HelpCommandSupport when you want help inside another command tree or on
platforms that do not ship a dedicated wrapper:
MagicSender
MagicSender is the platform-neutral sender wrapper used throughout the
command system:
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:
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 flatcopy()— an unfrozen duplicate you can mutate and re-register
Nested builder subcommands are supported as well:
Composing Existing Commands
Already-authored annotation commands can be mounted under another command tree
without rewriting them into SubCommandSpec form:
This is intentionally different from withName("punish"):
withName(...)mutates the authored command instance itselfmount("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):
The difference from mount:
mount("greet", cmd)→/root greet hellomountSubCommands(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:
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:
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.