MultiLoader 1.21+ · Part 27

Custom Commands (MultiLoader 1.21+)

INTERMEDIATE MULTILOADER 1.21-1.21.1 20 min read · Nov 9, 2026
MultiLoader 1.21+ · 28 parts
1 Getting Started with MultiLoader 1.21+ 2 Setting Up RegistrationUtils 3 Creating Items (MultiLoader 1.21+) 4 Creating Blocks (MultiLoader 1.21+) 5 Data Generation: Block & Item Models (MultiLoader 1.21+) 6 Data Generation: Block Loot Tables (MultiLoader 1.21+) 7 Data Generation: Crafting Recipes (MultiLoader 1.21+) 8 Data Generation: Block & Item Tags (MultiLoader 1.21+) 9 Custom Food Items (MultiLoader 1.21+) 10 Custom Tools (MultiLoader 1.21+) 11 Custom Armour (MultiLoader 1.21+) 12 Block Entities (MultiLoader 1.21+) 13 Config Files (MultiLoader 1.21+) 14 Custom Sounds (MultiLoader 1.21+) 15 Events and Listeners (MultiLoader 1.21+) 16 Networking and Custom Packets (MultiLoader 1.21+) 17 Data Generation: Advancements (MultiLoader 1.21+) 18 Data Generation: Language Files (MultiLoader 1.21+) 19 Custom Entities (MultiLoader 1.21+) 20 Ore Generation (MultiLoader 1.21+) 21 Introduction to Mixins (MultiLoader 1.21+) 22 Custom Particles (MultiLoader 1.21+) 23 Menus & Screens (MultiLoader 1.21+) 24 Key Bindings (MultiLoader 1.21+) 25 Custom Potion Effects (MultiLoader 1.21+) 26 Custom Enchantments (MultiLoader 1.21+) 27 Custom Commands (MultiLoader 1.21+) 28 Custom Biomes (MultiLoader 1.21+)

Minecraft's command system is built on Brigadier, a command-parsing library that represents commands as a tree of literal and argument nodes. Both Fabric and NeoForge use the same Brigadier API and the same CommandSourceStack type, so the command logic itself can live entirely in your common module. Only the registration hook differs between loaders.

The Command Tree

A Brigadier command is a tree where each node is either a literal (a fixed word the player types) or an argument (a typed value the player provides). Every path through the tree that ends with an .executes() call is a valid command.

This tutorial registers a root command /examplemod with two subcommands:

  • /examplemod greet <player>: Sends a greeting to the specified player. Available to all players.
  • /examplemod heal [amount]: Heals the executing player. Requires operator level 2. The amount argument is optional (defaults to full health).

The Command Handler Class

Create an ExampleCommands class in a commandpackage inside your common project. The register method takes a CommandDispatcher<CommandSourceStack> and a CommandBuildContext, which is required by some argument types that need access to the registry:

java
public class ExampleCommands {
public static void register(CommandDispatcher<CommandSourceStack> dispatcher,
CommandBuildContext buildContext) {
dispatcher.register(
Commands.literal("examplemod")
// /examplemod greet <player>
.then(Commands.literal("greet")
.then(Commands.argument("player", EntityArgument.player())
.executes(ctx -> greet(ctx,
EntityArgument.getPlayer(ctx, "player")))))
// /examplemod heal (no amount so defaults to full health)
.then(Commands.literal("heal")
.requires(src -> src.hasPermission(2))
.executes(ctx -> heal(ctx, -1))
// /examplemod heal <amount>
.then(Commands.argument("amount", IntegerArgumentType.integer(1, 100))
.executes(ctx -> heal(ctx,
IntegerArgumentType.getInteger(ctx, "amount")))))
);
}
private static int greet(CommandContext<CommandSourceStack> ctx,
ServerPlayer target) {
target.sendSystemMessage(
Component.literal("Hello from ")
.append(ctx.getSource().getDisplayName())
.append("!"));
ctx.getSource().sendSuccess(
() -> Component.literal("Greeted " + target.getName().getString()),
false // false = do not broadcast to admins in the console
);
return Command.SINGLE_SUCCESS; // 1 = success
}
private static int heal(CommandContext<CommandSourceStack> ctx,
int amount) throws CommandSyntaxException {
ServerPlayer player = ctx.getSource().getPlayerOrException();
float healAmount = (amount == -1)
? player.getMaxHealth() - player.getHealth()
: amount;
if (healAmount <= 0) {
throw new SimpleCommandExceptionType(
Component.literal("You are already at full health.")
).create();
}
player.heal(healAmount);
ctx.getSource().sendSuccess(
() -> Component.literal("Healed " + String.format("%.1f", healAmount) + " HP"),
true // true = broadcast to admins so they know a heal was performed
);
return Command.SINGLE_SUCCESS;
}
}

The register method receives the live CommandDispatcher and calls dispatcher.register() to attach the command tree. The root literal is "examplemod"; players type /examplemod in the chat box. All subcommands are chained using .then() calls that return the parent node, allowing deep nesting.

Argument Types

Brigadier's built-in argument types handle primitive Java types. Minecraft adds a large set of game-aware types via the Commands and ArgumentTypes classes:

  • BoolArgumentType.bool(): true or false.
  • IntegerArgumentType.integer(min, max): Integer with optional bounds.
  • FloatArgumentType.floatArg(min, max): Float with optional bounds.
  • StringArgumentType.word(): A single word (no spaces).
  • StringArgumentType.string(): A quoted string (allows spaces when quoted).
  • StringArgumentType.greedyString(): Everything from this argument to the end of the input. Must be the last argument in a chain.
  • EntityArgument.player(): A single online player by name or selector.
  • EntityArgument.players(): One or more players by name or selector.
  • BlockPosArgument.blockPos(): Absolute or relative block coordinates.
  • ItemArgument.item(buildContext): An item with optional NBT. Requires CommandBuildContext.
  • ResourceArgument.resource(buildContext, Registries.BIOME): Any registry entry. Replace Registries.BIOME with the registry you need.

Values are extracted from the context using the corresponding static getter on the argument class:

java
// Extract a single player from a context
ServerPlayer target = EntityArgument.getPlayer(ctx, "player");
// Extract multiple players
Collection<ServerPlayer> targets = EntityArgument.getPlayers(ctx, "targets");
// Extract a block position
BlockPos pos = BlockPosArgument.getBlockPos(ctx, "pos");
// Extract a string
String message = StringArgumentType.getString(ctx, "message");
TIP
Argument type mismatches (reading a string as an integer, for example) throw CommandSyntaxException at parse time before your execute function is called, so you do not need to validate types manually. You do still need to validate business logic (for example, whether the target player is in the right dimension).

Permissions

Call .requires(predicate) on any literal or argument node to restrict who can run or even see that branch of the command. The predicate receives the CommandSourceStack and should return true if the source is permitted:

java
// Require operator level 2 (standard for game-altering commands)
.requires(src -> src.hasPermission(2))
// Require operator level 4 (console / RCON only in most setups)
.requires(src -> src.hasPermission(4))
// Allow only players, not the console
.requires(src -> src.getEntity() instanceof Player)
// Allow players who are also operators
.requires(src -> src.getEntity() instanceof Player && src.hasPermission(2))

Permission level meanings: 0 = anyone (default), 1 = singleplayer bypass, 2 = standard operator, 3 = senior operator (world management), 4 = console. On a server, operators are assigned a level in ops.json. In singleplayer, the player always has level 4.

The .requires() predicate also controls tab-completion visibility. A player who fails the predicate will not see the command in their autocomplete list, as if it does not exist.

Sending Feedback

Use CommandSourceStack's feedback methods rather than sending messages directly to a player. This ensures the console also sees the output and that the messages follow Minecraft's command feedback conventions:

java
// Success: white text. Pass true to also log to the console and broadcast to admins.
ctx.getSource().sendSuccess(() -> Component.literal("Done!"), false);
// Failure: red text. Only shown to the executing source, never broadcast.
ctx.getSource().sendFailure(Component.literal("Something went wrong."));
// Throw to abort execution with a usage error (also shown in red):
throw new SimpleCommandExceptionType(
Component.literal("Invalid argument: must be positive.")
).create();

Return Command.SINGLE_SUCCESS (the integer value 1) from your execute lambda on success. Any non-zero return is treated as success; negative values are errors. Returning 0 indicates that the command ran but did nothing (used by commands like /clear when there is nothing to clear).

NeoForge Registration

Subscribe to RegisterCommandsEvent on the game bus. This event fires for every logical server start, including when a world is loaded in singleplayer:

java
@EventBusSubscriber(modid = Constants.MOD_ID, bus = EventBusSubscriber.Bus.GAME)
public class ExampleEvents {
@SubscribeEvent
public static void registerCommands(RegisterCommandsEvent event) {
ExampleCommands.register(event.getDispatcher(), event.getBuildContext());
}
}

Fabric Registration

Register via CommandRegistrationCallback from the Fabric API module fabric-command-api-v2. The callback fires in the same circumstances as NeoForge's event every time a logical server starts. Let's add this into the onInitializein our Fabric subproject main mod class:

java
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) ->
ExampleCommands.register(dispatcher, registryAccess));

The environment parameter is a CommandManager.RegistrationEnvironment enum that tells you whether commands are being registered for a dedicated server or for an integrated (singleplayer) server. Most mods can ignore it and register commands unconditionally. If you want a command available only on dedicated servers, guard with:

java
if (environment == CommandManager.RegistrationEnvironment.DEDICATED) {
ExampleCommands.register(dispatcher, registryAccess);
}

Testing

Launch a singleplayer world (cheats enabled) and type /examplemod greet <your username>. Tab completion should suggest your username after typing greet . After running, you should receive the greeting in chat and see the "Greeted..." confirmation message.

Run /examplemod heal when not at full health to confirm it restores your health to maximum. Run /examplemod heal 5 to confirm the optional argument works. Run it at full health to confirm the error message appears.

To test the permission restriction, temporarily give yourself operator level 0 with /op @s 0 if your server supports it, or test on a server where you are not an operator. The heal command should not appear in autocomplete and should fail with an "unknown command" or "insufficient permissions" error if typed manually.

If your command does not appear in autocomplete at all, check that ExampleCommands.register() was called. On NeoForge, verify the @EventBusSubscriber uses Bus.GAME and not Bus.MOD. On Fabric, confirm the callback is registered in the common (not client) mod initialiser.

You can find the source for this tutorial here:

View Source on GitHub
NEXT IN SERIES

Custom Biomes (MultiLoader 1.21+)

Define a biome JSON with climate parameters, sky/fog/water colours, mob spawners, and vanilla features, generate it with DatapackBuiltinEntriesProvider, and inject it into the overworld using TerraBlender's Region API.

Continue →