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:
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():trueorfalse.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. RequiresCommandBuildContext.ResourceArgument.resource(buildContext, Registries.BIOME): Any registry entry. ReplaceRegistries.BIOMEwith the registry you need.
Values are extracted from the context using the corresponding static getter on the argument class:
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:
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:
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:
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:
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:
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 GitHubCustom 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 →