This is an old revision of the document!
Table of Contents
Creating Commands
Creating commands can allow a mod developer to add functionality that can used through a command. This tutorial will teach you how to register commands, and the general command structure of Brigadier.
Note: All code written here was written for 1.14.4. Some mappings may have changed in yarn, but all code should still be applicable.
A basic command
Below is a command that contains no arguments:
dispatcher.register(CommandManager.literal("foo").executes(context -> { return 1; }));
CommandManager.literal(“foo”)
tells brigadier this command has one node, a literal called foo
.
To execute this command, one must type /foo
. If /Foo
, /FoO
, /FOO
, /fOO
or /fooo
is typed instead, the command will not run.
A sub command
To add a sub command, you register the first literal node of the command normally.
dispatcher.register(CommandManager.literal("foo")
In order to have a sub command, one needs to append the next node to the existing node. This is done use the then(ArgumentBuilder)
method which takes in an ArgumentBuilder
.
This creates the command foo <bar>
as shown below.
dispatcher.register(CommandManager.literal("foo") .then(CommandManager.literal("bar")) );
It is advised to indent your code as you add nodes to the command. Usually the indentation corresponds to how many nodes deep one is on the command tree. The new line also makes it visible that another node is being added. There are alternative styles on formatting the tree command that are shown later on in this tutorial.
So let's try running the command
Most likely if you typed /foo bar
in game, the command will fail to run. This is because there is no code for the game to execute when all the required arguments have been met.
Brigadier requires you specify the Command
to be run. A “command” is a fairly loose term, but typically it means an exit point of the command tree where code is executed.
To specify the command to be run, you must use the executes(Command)
method on your builder.
The executes method takes a Command
as the single parameter. A command is a functional interface that takes a CommandContext<S>
as the sole parameter and returns an integer. The run(CommandContext)
method can throw a CommandSyntaxException
, but this is covered later in the tutorial.
The integer can be considered the result of the command. In Minecraft, the result can correspond to the power of a redstone comparator feeding from a command block or the value that will be passed the chain command block the command block is facing. Typically negative values mean a command has failed and will do nothing. A result of 0
means the command has passed. Positive values mean the command was successful and did something.
dispatcher.register(CommandManager.literal("foo") .then(CommandManager.literal("bar") .executes(context -> { return 1; }) ) );
The command trees above one builds specify the arguments and paths to these Commands
to be run.
Registering the commands
Registering commands is done by registering a new listener in the CommandRegistrationCallback
.
The event should be registered in your mod's initializer.
The dedicated parameter if true will tell event listeners that the server commands are being registered on is a dedicated server
. If false than the commands will registered on an integrated server
.
Below are a few examples of how the commands can be registered.
// Method reference CommandRegistrationCallback.EVENT.register(TutorialCommands::register); // Using a lambda CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> { // This command will be registered regardless of the server being dedicated or integrated TutorialCommand.register(dispatcher); if (dedicated) { // This command will only be registered on a dedicated server TutorialHelpCommand.register(dispatcher); } else { // This command will only be registered on an integrated server. // Commands which call client only classes and methods should be registered in your ClientModInitializer IntegratedTutorialHelpCommand.register(dispatcher); } }); // Or register directly CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> { dispatcher.register(LiteralArgumentBuilder.literal("tutorial").executes(ctx -> execute(ctx))); });
Brigadier Explained
Brigadier starts with the CommandDispatcher
which should be thought more as a tree of command nodes.
Command nodes are similar to the branches that define the command tree. The executes blocks can be seen at the leaves of the tree where a branch ends and also supplies the command to be executed.
The execute blocks specify the command to be ran. As Brigadier's Command is a functional interface you can use lambdas to specify commands.
Command Context
When a command is ran, Brigadier provides a CommandContext
to the command that is ran.
The command context contains all arguments and other objects such as the inputted String and the Command Source
(ServerCommandSource in Minecraft's implementation).
Arguments
Arguments in Brigadier both parse and error check any inputted arguments.
Minecraft creates some special argument types for it's own use such as the EntityArgumentType
which represents the in-game entity selectors @a, @r, @p, @e[type=!player, limit=1, distance=..2]
, or an NbtTagArgumentType
that parses stringified nbt (snbt) and verifies that the input is the correct syntax.
Static Imports
You could type out CommandManager.literal(“foo”)
every time you want to create a literal. This works, but you can statically import the arguments and shorten the statement to literal(“foo”)
. This also works for getting the value of an argument. This shortens StringArgumentType.getString(ctx, “string”)
to getString(ctx, “string”)
.
This also works for Minecraft's own argument types.
And your imports would look something like this:
// getString(ctx, "string") import static com.mojang.brigadier.arguments.StringArgumentType.getString; // word() import static com.mojang.brigadier.arguments.StringArgumentType.word; // literal("foo") import static net.minecraft.server.command.CommandManager.literal; // argument("bar", word()) import static net.minecraft.server.command.CommandManager.argument; // Import everything import static net.minecraft.server.command.CommandManager.*;
Note: Please be sure you use the literal
and argument
from CommandManager or you may have issues with generics when trying to compile.
Brigadier's default arguments are at com.mojang.brigadier.arguments
Minecraft's arguments are in net.minecraft.command.arguments
.
CommandManager is in net.minecraft.server.command
Suggestions
Suggestions can be provided to the client to recommend what to input into the command. This is used for Scoreboards and Loot Tables ingame. The game stores these in the SuggestionProviders. A few examples of Minecraft's built in suggestions providers are below
SUMMONABLE_ENTITIES AVAILIBLE_SOUNDS ALL_RECIPES ASK_SERVER
Loot tables specify their own SuggestionProvider inside LootCommand
for example.
The example below is a dynamically changing SuggestionProvider that lists several words for a StringArgumentType to demonstrate how it works:
public static SuggestionProvider<ServerCommandSource> suggestedStrings() { return (ctx, builder) -> getSuggestionsBuilder(builder, /*Access to a list here*/); } private static CompletableFuture<Suggestions> getSuggestionsBuilder(SuggestionsBuilder builder, List<String> list) { if(list.isEmpty()) { // If the list is empty then return no suggestions return Suggestions.empty(); // No suggestions } builder.suggest(str); // Add every single entry to suggestions list. } } return builder.buildFuture(); // Create the CompletableFuture containing all the suggestions }
The SuggestionProvider is a functional interface that returns a CompletableFuture containing a list of suggestions. These suggestions are sent to client as a command is typed and can be changed while server is running. The SuggestionProvider provides a CommandContext and a SuggestionBuilder to allow determination of all the suggestions. The CommandSource can also be taken into account during the suggestion creation process as it is available through the CommandContext.
Though remember these are suggestions. The inputted command may not contain an argument you suggested so arguments are parsed without consideration for suggestions.
To use the suggestion you would append it right after the argument you want to suggest possible arguments for. This can be any argument and the normal client side exception popups will still work. Note this cannot be applied to literals.
argument(argumentName, word()) .suggests(CompletionProviders.suggestedStrings()) .then(/*Rest of the command*/));
Requiring Permissions
Lets say you have a command you only want operators to be able to execute. This is where the requires
method comes into play. The requires method has one argument of a Predicate<ServerCommandSource> which will supply a ServerCommandSource to test with and determine if the CommandSource can execute the command.
For example this may look like the following:
dispatcher.register(literal("foo") .requires(source -> source.hasPermissionLevel(4)) .executes(ctx -> { ctx.getSource().sendFeedback(new LiteralText("You are an operator", false)); return 1; });
This command will only execute if the Source of the command is a level 4 operator at minimum. If the predicate returns false, then the command will not execute. Also this has the side effect of not showing this command in tab completion to anyone who is not a level 4 operator.
Nothing prevents someone from specifying calls to permissions implementations within the requires
block. Just note that if permissions change, you need to re send the command tree.
Exceptions
Brigadier supports command exceptions which can be used to end a command such as if an argument didn't parse properly or the command failed to execute, as well as including richer details of the failure.
All the exceptions from Brigadier are based on the CommandSyntaxException. The two main types of exceptions Brigadier provides are Dynamic and Simple exception types, of which you must create()
the exception to throw it. These exceptions also allow you to specify the context in which the exception was thrown using createWithContext(ImmutableStringReader)
, which builds the error message to point to where on the inputted command line the error occured.
Below is a coin flip command to show an example of exceptions in use.
dispatcher.register(CommandManager.literal("coinflip") .executes(ctx -> { if(random.nextBoolean()) { // If heads succeed. ctx.getSource().sendMessage(new TranslateableText("coin.flip.heads")) return Command.SINGLE_SUCCESS; } throw new SimpleCommandExceptionType(new TranslateableText("coin.flip.tails")).create(); // Oh no tails, you lose. }));
Though you are not just limited to a single type of exception as Brigadier also supplies Dynamic exceptions which take additional parameters for context.
DynamicCommandExceptionType used_name = new DynamicCommandExceptionType(name -> { });
There are more Dynamic exception types which each take a different amount of arguments into account (Dynamic2CommandExceptionType
, Dynamic3CommandExceptionType
, Dynamic4CommandExceptionType
, DynamicNCommandExceptionType
).
You should remember that the Dynamic exceptions takes an object as an argument so you may have to cast the argument for your use.
Redirects (Aliases)
Redirects are Brigadier's form of aliases. Below is how Minecraft handles /msg have an alias of /tell and /w.
public static void register(CommandDispatcher<ServerCommandSource> dispatcher) { LiteralCommandNode node = registerMain(dispatcher); // Registers main command dispatcher.register(literal("tell") .redirect(node)); // Alias 1, redirect to main command dispatcher.register(literal("w") .redirect(node)); // Alias 2, redirect to main command } public static LiteralCommandNode registerMain(CommandDispatcher<ServerCommandSource> dispatcher) { return dispatcher.register(literal("msg") .then(argument("targets", EntityArgumentType.players()) .then(argument("message", MessageArgumentType.message()) .executes(ctx -> { return execute(ctx.getSource(), getPlayers(ctx, "targets"), getMessage(ctx, "message")); })))); }
The redirect tells brigadier to continue parsing the command at another command node.
Redirects (Chainable Commands)
Commands such as /execute as @e[type=player] in the_end run tp ~ ~ ~
are possible because of redirects. Below is an example of a chainable command:
LiteralCommandNode<ServerCommandSource> root = dispatcher.register(literal("fabric_test")); LiteralCommandNode<ServerCommandSource> root1 = dispatcher.register(literal("fabric_test") // You can register under the same literal more than once, it will just register new parts of the branch as shown below if you register a duplicate branch an error will popup in console warning of conflicting commands but one will still work. .then(literal("extra") .then(literal("long") .redirect(root, this::lengthen)) // Return to root for chaining .then(literal("short") .redirect(root, this::shorten))) // Return to root for chaining .then(literal("command") .executes(ctx -> { ctx.getSource().sendFeedback(new LiteralText("Chainable Command"), false); return Command.SINGLE_SUCCESS; })));
The redirect can also modify the CommandSource by use of a redirect modifier
which can be used for builder commands.
.redirect(rootNode, context -> { return ((ServerCommandSource) context.getSource()).withLookingAt(Vec3ArgumentType.getVec3(context, "pos")); })
ServerCommandSource
A server command source provides some additional implementation specific context when a command is run. This includes the ability to get the entity that executed the command, the world the command was ran in or the server the command was run on.
final ServerCommandSource source = ctx.getSource(); // Get the source. This will always work. // Unchecked, may be null if the sender was the console. // Will end the command if the source of the command was not an Entity. // The result of this could contain a player. Also will send feedback telling the sender of the command that they must be an entity. // This method will require your methods to throw a CommandSyntaxException. // The entity options in ServerCommandSource could return a CommandBlock entity, any living entity or a player. final ServerPlayerEntity player = source.getPlayer(); // Will end the command if the source of the command was not explicitly a Player. Also will send feedback telling the sender of the command that they must be a player. This method will require your methods to throw a CommandSyntaxException source.getPosition(); // Get's the sender's position as a Vec3 when the command was sent. This could be the location of the entity/command block or in the case of the console, the world's spawn point. source.getWorld(); // Get's the world the sender is within. The console's world is the same as the default spawn world. source.getRotation(); // Get's the sender's rotation as a Vec2f. source.getMinecraftServer(); // Access to the instance of the MinecraftServer this command was ran on. source.getName(); // The name of the command source. This could be the name of the entity, player, the name of a CommandBlock that has been renamed before being placed down or in the case of the Console, "Console" source.hasPermissionLevel(int level); // Returns true if the source of the command has a certain permission level. This is based on the operator status of the sender. (On an integrated server, the player must have cheats enabled to execute these commands)
A few examples
Broadcast a message
public static void register(CommandDispatcher<ServerCommandSource> dispatcher){ dispatcher.register(literal("broadcast") .requires(source -> source.hasPermissionLevel(2)) // Must be a game master to use the command. Command will not show up in tab completion or execute to non operators or any operator that is permission level 1. .then(argument("color", ColorArgumentType.color()) .then(argument("message", greedyString()) .executes(ctx -> broadcast(ctx.getSource(), getColor(ctx, "color"), getString(ctx, "message")))))); // You can deal with the arguments out here and pipe them into the command. } final Text text = new LiteralText(message).formatting(formatting); source.getMinecraftServer().getPlayerManager().broadcastChatMessage(text, false); return Command.SINGLE_SUCCESS; // Success }
/giveMeDiamond
First the basic code where we register “giveMeDiamond” as a literal and then an executes block to tell the dispatcher which method to run.
public static LiteralCommandNode register(CommandDispatcher<ServerCommandSource> dispatcher) { // You can also return a LiteralCommandNode for use with possible redirects return dispatcher.register(literal("giveMeDiamond") .executes(ctx -> giveDiamond(ctx))); }
Then since we only want to give to players, we check if the CommandSource is a player. But we can use getPlayer
and do both at the same time and throw an error if the source is not a player.
public static int giveDiamond(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException { final ServerCommandSource source = ctx.getSource(); final PlayerEntity self = source.getPlayer(); // If not a player than the command ends
Then we add to the player's inventory, with a check to see if the inventory is full:
if(!player.inventory.insertStack(new ItemStack(Items.DIAMOND))){ throw new SimpleCommandExceptionType(new TranslatableText("inventory.isfull")).create(); } return 1; }
Antioch
…lobbest thou thy Holy Hand Grenade of Antioch towards thy foe. who being naughty in My sight, shall snuff it.
Aside from the joke this command summons a primed TNT to a specified location or the location of the sender's cursor.
First create an entry into the CommandDispatcher that takes a literal of antioch with an optional argument of the location to summon the entity at.
public static void register(CommandDispatcher<ServerCommandSource> dispatcher) { dispatcher.register(literal("antioch") .then(required("location", BlockPosArgumentType.blockPos() .executes(ctx -> antioch(ctx.getSource(), BlockPosArgument.getBlockPos(ctx, "location"))))) .executes(ctx -> antioch(ctx.getSource(), null))); }
Then the creation and messages behind the joke.
public static int antioch(ServerCommandSource source, BlockPos blockPos) throws CommandSyntaxException { if(blockPos == null) { // For the case of no inputted argument we calculate the cursor position of the player or throw an error if the nearest position is too far or is outside of the world. // This class is used as an example and actually doesn't exist yet. blockPos = LocationUtil.calculateCursorOrThrow(source, source.getRotation()); } final TntEntity tnt = new TntEntity(source.getWorld(), blockPos.getX(), blockPos.getY(), blockPos.getZ(), null); tnt.setFuse(3); source.getMinecraftServer().getPlayerManager().broadcastChatMessage(new LiteralText("...lobbest thou thy Holy Hand Grenade of Antioch towards thy foe").formatting(Formatting.RED), false); source.getMinecraftServer().getPlayerManager().broadcastChatMessage(new LiteralText("who being naughty in My sight, shall snuff it.").formatting(Formatting.RED), false); source.getWorld().spawnEntity(tnt); return 1; }
Finding Biomes via Command
This example shows examples of redirects, exceptions, suggestions and a tiny bit of text. Note this command when used works but can take a bit of time to work similarly to /locate
public class CommandLocateBiome { // First make method to register public static void register(CommandDispatcher<ServerCommandSource> dispatcher) { LiteralCommandNode<ServerCommandSource> basenode = dispatcher.register(literal("findBiome") .then(argument("biome_identifier", identifier()).suggests(BiomeCompletionProvider.BIOMES) // We use Biome suggestions for identifier argument .then(argument("distance", integer(0, 20000)) .executes(ctx -> execute(ctx.getSource(), getIdentifier(ctx, "biome_identifier"), getInteger(ctx, "distance")))) .executes(ctx -> execute(ctx.getSource(), getIdentifier(ctx, "biome_identifier"), 1000)))); // Register a redirect for /biome alias dispatcher.register(literal("biome") .redirect(basenode)); } private static int execute(ServerCommandSource source, Identifier biomeId, int range) throws CommandSyntaxException { if(biome == null) { // Since the argument is an Identifier we need to check if the identifier actually exists in the registry throw new SimpleCommandExceptionType(new TranslatableText("biome.not.exist", biomeId)).create(); } List<Biome> bio = new ArrayList<Biome>(); bio.add(biome); ServerWorld world = source.getWorld(); BiomeSource bsource = world.getChunkManager().getChunkGenerator().getBiomeSource(); BlockPos loc = new BlockPos(source.getPosition()); // This call will likely block the main thread BlockPos pos = bsource.locateBiome(loc.getX(), loc.getZ(), range, bio, new Random(world.getSeed())); // If null, a biome was not found if(pos == null) { throw new SimpleCommandExceptionType(new TranslatableText("biome.notfound", biome.getTranslationKey())).create(); } int distance = MathHelper.floor(getDistance(loc.getX(), loc.getZ(), pos.getX(), pos.getZ())); // Popup text that can suggest commands. This is the exact same system that /locate uses. Text teleportButtonPopup = Texts.bracketed(new TranslatableText("chat.coordinates", new Object[] { pos.getX(), "~", pos.getZ()})).styled((style) -> { }); return 1; } // Just a normal old 2d distance method. private static float getDistance(int int_1, int int_2, int int_3, int int_4) { int int_5 = int_3 - int_1; int int_6 = int_4 - int_2; return MathHelper.sqrt((float) (int_5 * int_5 + int_6 * int_6)); } public static class BiomeCompletionProvider { // This provides suggestions of what biomes can be selected. Since this uses the registry, mods that add new biomes will work without any modifications. public static final SuggestionProvider<ServerCommandSource> BIOMES = SuggestionProviders.register(new Identifier("biomes"), (ctx, builder) -> { return builder.buildFuture(); }); }
Custom Argument Types
Brigadier has support for custom argument types and this section goes into showing how to create a simple argument type.
Warning: Custom arguments require client mod installation to be registered correctly! If you are making a server plugin, consider using existing argument type and a custom suggestions provider instead.
For this example we will create a UuidArgumentType.
First create a class which extends ArgumentType
. Note that ArgumentType is a generic, so the generic will define what type the ArgumentType will return
public class UuidArgumentType implements ArgumentType<UUID> {
ArgumentType requires you to implement the parse
method, the type it returns will match with the Generic type.
@Override public UUID parse(StringReader reader) throws CommandSyntaxException {
This method is where all of your parsing will occur. Either this method will return the object based on the arguments provided in the command line or throw a CommandSyntaxException and parsing will fail.
Next you will store the current position of the cursor, this is so you can substring out only the specific argument. This will always be at the beginning of where your argument appears on the command line.
int argBeginning = reader.getCursor(); // The starting position of the cursor is at the beginning of the argument. if (!reader.canRead()) { reader.skip(); }
Now we grab the entire argument. Depending on your argument type, you may have a different criteria or be similar to some arguments where detecting a {
on the command line will require it to be closed. For a UUID we will just figure out what cursor position the argument ends at.
while (reader.canRead() && reader.peek() != ' ') { // peek provides the character at the current cursor position. reader.skip(); // Tells the StringReader to move it's cursor to the next position. }
Then we will ask the StringReader what the current position of the cursor is an substring our argument out of the command line.
Now finally we check if our argument is correct and parse the specific argument to our liking, and throwing an exception if the parsing fails.
try { UUID uuid = UUID.fromString(uuidString); // Now our actual logic. return uuid; // And we return our type, in this case the parser will consider this argument to have parsed properly and then move on. // UUIDs can throw an exception when made by a string, so we catch the exception and repackage it into a CommandSyntaxException type. // Create with context tells Brigadier to supply some context to tell the user where the command failed at. // Though normal create method could be used. throw new SimpleCommandExceptionType(new LiteralText(ex.getMessage())).createWithContext(reader); }
The ArgumentType is done, however your client will refuse the parse the argument and throw an error. This is because the server will tell the client what argument type the command node is. And the client will not parse any argument types it does not know how to parse. To fix this we need to register an ArgumentSerializer. Within your ModInitializer. For more complex argument types, you may need to create your own ArgumentSerializer.
ArgumentTypes.register("mymod:uuid", UuidArgumentType.class, new ConstantArgumentSerializer(UuidArgumentType::uuid)); // The argument should be what will create the ArgumentType.
And here is the whole ArgumentType:
- UuidArgumentType.java
- import com.mojang.brigadier.StringReader;
- import com.mojang.brigadier.arguments.ArgumentType;
- import com.mojang.brigadier.context.CommandContext;
- import com.mojang.brigadier.exceptions.CommandSyntaxException;
- import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
- import net.minecraft.text.LiteralText;
- import net.minecraft.util.SystemUtil;
- import java.util.ArrayList;
- import java.util.Collection;
- import java.util.UUID;
- /**
- * Represents an ArgumentType that will return a UUID.
- */
- public class UuidArgumentType implements ArgumentType<UUID> {
- public static UuidArgumentType uuid() {
- return new UuidArgumentType();
- }
- // Note that you should assume the CommandSource wrapped inside of the CommandContext will always be a generic type.
- // If you need to access the ServerCommandSource make sure you verify the source is a server command source before casting.
- return context.getArgument(name, UUID.class);
- }
- private static final Collection<String> EXAMPLES = SystemUtil.consume(new ArrayList<>(), list -> {
- list.add("765e5d33-c991-454f-8775-b6a7a394c097"); // i509VCB: Username The_1_gamers
- list.add("069a79f4-44e9-4726-a5be-fca90e38aaf5"); // Notch
- list.add("61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"); // Dinnerbone
- });
- @Override
- int argBeginning = reader.getCursor(); // The starting position of the cursor is at the beginning of the argument.
- if (!reader.canRead()) {
- reader.skip();
- }
- // Now we check the contents of the argument till either we hit the end of the command line (When canRead becomes false)
- // Otherwise we go till reach reach a space, which signifies the next argument
- while (reader.canRead() && reader.peek() != ' ') { // peek provides the character at the current cursor position.
- reader.skip(); // Tells the StringReader to move it's cursor to the next position.
- }
- // Now we substring the specific part we want to see using the starting cursor position and the ends where the next argument starts.
- try {
- UUID uuid = UUID.fromString(uuidString); // Now our actual logic.
- return uuid; // And we return our type, in this case the parser will consider this argument to have parsed properly and then move on.
- // UUIDs can throw an exception when made by a string, so we catch the exception and repackage it into a CommandSyntaxException type.
- // Create with context tells Brigadier to supply some context to tell the user where the command failed at.
- // Though normal create method could be used.
- throw new SimpleCommandExceptionType(new LiteralText(ex.getMessage())).createWithContext(reader);
- }
- }
- @Override
- public Collection<String> getExamples() { // Brigadier has support to show examples for what the argument should look like, this should contain a Collection of only the argument this type will return. This is mainly used to calculate ambiguous commands which share the exact same
- return EXAMPLES;
- }
- }
FAQ
What else can I send feedback to the CommandSource?
You use the Text classes (LiteralText, TranslatableText, KeybindText, etc).
Why does my IDE complain saying that a method executed by my command needs to catch or throw a CommandSyntaxException
The solution to this is just to make the methods throw a CommandSyntaxException down the whole chain as the executes block handles the exceptions.
Can I register commands runtime?
You can do this but it is not reccomended. You would get the instance of the CommandManager and add anything you wish to the CommandDispatcher within it.
After that you will need to send the command tree to every player again using CommandManager.sendCommandTree(ServerPlayerEntity)
Can I unregister commands runtime?
You can also do this but it is very unstable and could cause unwanted side effects. Lets just say it involves a bunch of Reflection.
Once again you will need to send the command tree to every player again using CommandManager.sendCommandTree(ServerPlayerEntity)
afterwards.
Can I register client side commands?
Well Fabric currently doesn't support this natively but there is a mod by the Cotton team that adds this functionality where the commands do not run on the server and only on the client: https://github.com/CottonMC/ClientCommands