Commands with arguments and suggestions
We build /calc <a> <+|-|*|/> <b>. Part 1 parses the raw command line yourself, from PlayerCommandEvent. Part 2 is the same command as a node of the server’s Brigadier command tree, with typed arguments, tab completion and a permission. Use Part 2 for your own commands; Part 1 shows what the engine hands you underneath.
Part 1: parse the line yourself
Section titled “Part 1: parse the line yourself”The engine fires PlayerCommandEvent for every command a player types (the line without the leading slash). A small router turns that into registered commands with split arguments and a usage message. It is the lowest-level way to see a command, and it has no tab completion or permission integration.
using Mc.Scripting;
namespace Calc;
/// <summary>Handler of one command. <paramref name="args"/> are the words after the command name.</summary>public delegate void CommandAction(Entity sender, ReadOnlySpan<string> args);
/// <summary>Routes player commands (PlayerCommandEvent) to registered handlers.</summary>public sealed class CommandRouter{ private readonly Dictionary<string, CommandAction> _commands = new(StringComparer.OrdinalIgnoreCase);
public void Add(string name, CommandAction action) => _commands[name] = action;
public void Attach(EventRegistry events) { events.On((ref PlayerCommandEvent e) => { // commands are rare compared to ticks: one string and one split per command is fine string[] words = e.Command.ToString().Split(' ', StringSplitOptions.RemoveEmptyEntries); if (words.Length == 0 || !_commands.TryGetValue(words[0], out var action)) return; // not ours: leave it alone
e.Cancelled = true; // handled: the engine must not answer "Unknown command" action(e.Player, words.AsSpan(1)); }); }}using System.Globalization;using Mc.Scripting;
namespace Calc;
[PluginInfo("Calc", Version = "1.0.0")]public sealed class PluginMain : Plugin{ private readonly CommandRouter _router = new();
protected override void OnEnable() { _router.Add("calc", Calc); _router.Attach(Events); }
private static void Calc(Entity sender, ReadOnlySpan<string> args) { if (args.Length != 3 || !double.TryParse(args[0], NumberStyles.Float, CultureInfo.InvariantCulture, out double a) || !double.TryParse(args[2], NumberStyles.Float, CultureInfo.InvariantCulture, out double b) || args[1].Length != 1) { Server.SendMessage(sender, "usage: /calc <a> <+|-|*|/> <b>"); return; }
double? result = args[1][0] switch { '+' => a + b, '-' => a - b, '*' => a * b, '/' when b != 0 => a / b, _ => null, }; Server.SendMessage(sender, result is { } r ? $"{a} {args[1]} {b} = {r.ToString(CultureInfo.InvariantCulture)}" : "cannot calculate that (unknown operator or division by zero)"); }}What you do not get here: no tab completion, no typed argument checks in the client, no permission integration. Server.SendMessage sends the text to the player (Entity.Null sends it to the console). Part 2 adds the rest.
Part 2: a typed Brigadier tree
Section titled “Part 2: a typed Brigadier tree”This is the code to use in place of Part 1. Save it as PluginMain.cs in your project and delete CommandRouter.cs. The namespace is Calc.Tree only so that the docs build can compile both versions side by side.
using Mc.Scripting;using Mc.Scripting.Commands;
namespace Calc.Tree;
public sealed class PluginMain : Plugin{ private static readonly string[] Operators = ["+", "-", "*", "/"];
protected override void OnEnable() { Commands.Register( Command.Literal("calc") .Permission("calc.use") // hides the node and blocks it .Then(Command.Argument("a", Arg.Double()) .Then(Command.Argument("op", Arg.Word()) .Suggests(ctx => Operators) // tab completion .Then(Command.Argument("b", Arg.Double()) .Executes(Calculate)))), description: "Calculate with two numbers"); }
private static int Calculate(CommandContext ctx) { double a = ctx.GetDouble("a"), b = ctx.GetDouble("b"); string op = ctx.GetString("op"); double result = op switch { "+" => a + b, "-" => a - b, "*" => a * b, "/" when b != 0 => a / b, "/" => throw new CommandFailure("Division by zero"), // red message, result 0, no log _ => throw new CommandFailure($"Unknown operator '{op}'"), }; ctx.Reply($"{a} {op} {b} = {result}"); return (int)result; // the command result (usable by /execute store) }}What changed compared to Part 1:
- The tree is sent to the client:
/calcshows<a>in the usage hint, number arguments are validated by the client and server (Arg.Double()rejectsabcbefore your code runs),opcompletes to+ - * /. .Permission("calc.use")makes the command invisible and non-executable for players without the node. The console always passes (permissions).- Errors are thrown as
CommandFailure: the player sees red text, the result is 0 and nothing is logged. Any other exception shows “An unexpected error occurred” to the player and logs a stack trace. - The return value is the command’s result, exactly like vanilla’s (the number
/execute store resultcaptures). - Unloading the plugin removes the nodes, and the command trees of the online players are sent again at the end of the tick.
Subcommands, optional arguments and aliases
Section titled “Subcommands, optional arguments and aliases”This fragment shows the shape of a larger tree. Teleport, SetHome and HomeNames are your own methods:
Commands.Register( Command.Literal("home") .Executes(ctx => Teleport(ctx.RequirePlayer(), "home")) // /home .Then(Command.Argument("name", Arg.Word()) // /home <name> .Suggests(ctx => HomeNames(ctx.Player)) .Executes(ctx => Teleport(ctx.RequirePlayer(), ctx.GetString("name")))) .Then(Command.Literal("set") // /home set <name> .Then(Command.Argument("name", Arg.Word()).Executes(SetHome))), aliases: ["h"], description: "Teleport to a home");An argument node with an executor is an optional last argument; a literal child is a subcommand. See the homes guide for the complete plugin and the command argument types for everything Arg can produce.