Skip to content

Commands

The server has a complete Brigadier-compatible command engine (a port of com.mojang.brigadier and vanilla’s net.minecraft.commands): the full vanilla command tree, suggestions, and the Commands packet the client uses for tab completion and syntax colouring. The commands page browses the vanilla tree.

Plugin commands are nodes of that same tree. They show up in the client’s command tree with typed arguments, tab-complete on the server, and run through the same dispatcher as vanilla commands. You build a tree with Command.Literal and Command.Argument and register it through plugin.Commands.

HomeCommands.cs
[PluginInfo("HomeCommands", Version = "1.0.0")]
public sealed class HomeCommandsPlugin : Plugin
{
private readonly Dictionary<Uuid, Location> _homes = [];
protected override void OnEnable()
{
Permissions.Register("homes.use", PermissionDefault.True, "Use /sethome and /home");
Commands.Register(Command.Literal("sethome").Permission("homes.use")
.Executes(ctx => SetHome(ctx)),
description: "Remember this place as your home");
Commands.Register(Command.Literal("home").Permission("homes.use")
.Executes(ctx => GoHome(ctx.RequirePlayer()))
.Then(Command.Literal("of").OpLevel(2)
.Then(Command.Argument("player", Arg.Player())
.Executes(ctx => GoHome(ctx.GetPlayer("player"))))),
aliases: ["h"], description: "Teleport to your home");
}
private int SetHome(CommandContext ctx)
{
var player = ctx.RequirePlayer();
_homes[player.Uuid] = player.Location;
ctx.Reply("Home set");
return 1;
}
private int GoHome(Player player)
{
if (!_homes.TryGetValue(player.Uuid, out var home))
throw new CommandFailure($"{player.Name} has no home. Use /sethome");
player.Teleport(home);
return 1;
}
}
  • Commands.Register(root, aliases, description) returns a RegisteredCommand. Dispose it to unregister; unloading the plugin does that for you.
  • The root must be a literal. Each alias runs the same tree, and the client shows it too.
  • A name that the server or another plugin already owns throws InvalidOperationException. A server operator can let a plugin take over a vanilla root with commands.override in the server configuration; the groups are tp (with teleport) and msg (with tell and w).
  • Commands.Register(name, handler, description) is sugar for a name literal followed by a greedy args argument. Use it for quick commands that parse their own text:
FlatHello.cs
[PluginInfo("FlatHello", Version = "1.0.0")]
public sealed class FlatHelloPlugin : Plugin
{
protected override void OnEnable()
{
Commands.Register("hello", ctx =>
{
ctx.Reply($"Hello, {(ctx.Args.Length > 0 ? ctx.Args[0] : "world")}!");
ctx.Result = ctx.Args.Length; // the command's numeric result
}, "Says hello");
}
}

To see every command a player types, including vanilla ones, subscribe to PlayerCommandEvent. Its Command field is the line without the slash, and cancelling it suppresses the command.

An executor gets a CommandContext:

Member Meaning
Source who runs it: a CommandSource
Sender, Player, RequirePlayer() Sender is the executing entity (Entity.Null for the console); Player is the executing player, or null; RequirePlayer() returns the player and throws CommandFailure for the console
Name, Input, RawArgs, Args the root word, the whole line, the text after the root, and the words of it
Has(name) whether an optional argument was given
GetInt, GetLong, GetFloat, GetDouble, GetBool, GetString typed values of the argument with that name
GetPlayer, GetPlayers, GetEntities selectors, resolved by the engine (@a, @e[...])
GetBlockPos, GetVec3, GetBlockState positions (relative coordinates resolved against the source) and block states
GetText the textual form of any argument, for types without a typed getter (items, components, NBT, uuids)
Reply(string), Reply(Component) answer whoever ran the command
Result the command result, the number /execute store sees (default 1)
Detach() a copy of what a deferred reply needs, valid after the executor returns

CommandSource has Entity, Player, IsConsole, PermissionLevel, Position, World, and feedback: Success(message, broadcastToOps) and Failure(message). Feedback is sent without aborting the command.

An executor returns an int, or is an Action<CommandContext> whose result is Result. 1 means success.

Each Arg factory creates the argument type the client shows: Arg.Int(min, max), Arg.Long, Arg.Float, Arg.Double, Arg.Bool(), Arg.Word(), Arg.String() (one phrase), Arg.GreedyString() (the rest of the line), Arg.Player(), Arg.Players(), Arg.Entity(), Arg.Entities(), Arg.Vec3(), Arg.BlockPos(), Arg.BlockState(), Arg.Item(), Arg.Component(), Arg.Uuid(), Arg.Time(min), Arg.Dimension(), Arg.GameMode(), Arg.Resource("registry") and many more, down to the vanilla particle, loot and feature arguments. The full list, with the client-side properties, is on command argument types.

There is no enum factory yet. Use Arg.Word() with a Suggests list and check the value in the executor.

Tab completion is computed on the server. A provider can return plain words:

Command.Argument("name", Arg.Word())
.Suggests(ctx => HomeNames(ctx.Player)) // Func<CommandContext, IEnumerable<string>>

or fill a builder, with tooltips and an integer form:

Command.Argument("count", Arg.Int(1, 64))
.Suggests((ctx, builder) => builder.Suggest("8", "a stack of eight")) // SuggestionProvider

The engine filters the candidates by what the player typed, the way vanilla does.

A node can require the sender to have a permission node, a vanilla operator level, or any condition:

  • .Permission("homes.use"): the sender needs the node (see permissions). A player without it does not see the node in the client’s tree.
  • .OpLevel(2): a vanilla operator level, 0 to 4.
  • .Requires(source => source.PermissionLevel >= 2): any condition on the CommandSource.

The console always passes a Permission check.

Throw CommandFailure to fail the command with a red message. The result is 0, and nothing is written to the log. Any other exception shows the sender a generic error and logs the stack trace. ctx.Source.Failure(message) shows a red message without ending the command.

throw new CommandFailure("You have no home called 'base'");

Everything you register is removed when the plugin unloads. Clients get their trees refreshed once per tick, not once per change. The CommandsRegistered event fires after the tree has changed (see events).

  • Running a command line from a plugin. No API runs a command as a player or the console yet. Put the shared code in a method and call that instead.
  • Async suggestions in the plugin worker. SuggestsAsync exists, but plugins run in the plugin worker, which does not start those providers yet: the client gets no suggestions from them. Use Suggests until that changes.
  • Typed getters for Uuid and enums. Use GetText and parse it, and Arg.Word() with a suggestion list.