Skip to content

Commit

Permalink
Command Guides (#26)
Browse files Browse the repository at this point in the history
* add main commands page

* fix formatting using spotless

* requested changes

* Suggestions

* Custom Argument Types

* Refactor BlockPosArgumentType class to remove
unnecessary print statements

* Checkstyle fixes.

* Checkstyle fixes.

* Redirect commands.

* fix checkstyle error

---------

Co-authored-by: Calum <[email protected]>
  • Loading branch information
dicedpixels and IMB11 authored Feb 10, 2024
1 parent f962468 commit 6cd45d8
Show file tree
Hide file tree
Showing 11 changed files with 614 additions and 2 deletions.
Binary file added assets/develop/commands/custom-arguments_fail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
63 changes: 63 additions & 0 deletions develop/commands/arguments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
title: Command Arguments
description: Learn how to create commands with complex arguments.
---

# Command Arguments

Arguments are used in most of the commands. Sometimes they can be optional, which means if you do not provide that
argument,
the command will also run. One node may have multiple argument types, but be aware that there is a possibility of
ambiguity, which should be avoided.

@[code lang=java highlight={3} transcludeWith=:::4](@/reference/latest/src/main/java/com/example/docs/command/FabricDocsReferenceCommands.java)

In this case, after the command text `/argtater`, you should type an integer. For example, if you
run `/argtater 3`, you will get the feedback message `Called /argtater with value = 3`. If you
type `/argtater` without arguments, the command cannot be correctly parsed.

Then we add an optional second argument:

@[code lang=java highlight={3,13} transcludeWith=:::5](@/reference/latest/src/main/java/com/example/docs/command/FabricDocsReferenceCommands.java)

Now you can type one or two integers. If you give one integer, a feedback text with a single value is printed. If you
provide two integers, a feedback text with two values will be printed.

You may find it unnecessary to specify similar executions twice. Therefore, we can create a method that will be used in
both executions.

@[code lang=java highlight={3,5,6,7} transcludeWith=:::6](@/reference/latest/src/main/java/com/example/docs/command/FabricDocsReferenceCommands.java)

## Custom Argument Types

If vanilla does not have the argument type you need, you can create your own. To do this, you need to create a class that inherits the `ArgumentType<T>` interface where `T` is the type of the argument.

You will need to implement the `parse` method, which will parse the input string into the desired type.

For example, you can create an argument type that parses a `BlockPos` from a string with the following format: `{x, y, z}`

@[code lang=java transcludeWith=:::1](@/reference/latest/src/main/java/com/example/docs/command/BlockPosArgumentType.java)

### Registering Custom Argument Types

::: warning
You need to register the custom argument type on both the server and the client or else the command will not work!
:::

You can register your custom argument type in the `onInitialize` method of your mod initializer using the `ArgumentTypeRegistry` class:

@[code lang=java transcludeWith=:::11](@/reference/latest/src/main/java/com/example/docs/command/FabricDocsReferenceCommands.java)

### Using Custom Argument Types

We can use our custom argument type in a command - by passing an instance of it into the `.argument` method on the command builder.

@[code lang=java transcludeWith=:::10 highlight={3}](@/reference/latest/src/main/java/com/example/docs/command/FabricDocsReferenceCommands.java)

Running the command, we can test whether or not the argument type works:

![Invalid argument.](../../assets/develop/commands/custom-arguments_fail.png)

![Valid argument.](../../assets/develop/commands/custom-arguments_valid.png)

![Command result.](../../assets/develop/commands/custom-arguments_result.png)
197 changes: 197 additions & 0 deletions develop/commands/basics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
---
title: Creating Commands
description: Create commands with complex arguments and actions.
authors:
- dicedpixels
- i509VCB
- pyrofab
- natanfudge
- Juuxel
- solidblock
- modmuss50
- technici4n
- atakku
- haykam
- mschae23
- treeways
---

# Creating Commands

Creating commands can allow a mod developer to add functionality that can be used through a command. This tutorial will
teach you how to register commands and the general command structure of Brigadier.

::: info
Brigadier is a command parser and dispatcher written by Mojang for Minecraft. It is a tree-based command library where
you
build a tree of commands and arguments. Brigadier is open source: https://github.com/Mojang/brigadier
:::

### The `Command` Interface

`com.mojang.brigadier.Command` is a functional interface, which runs some specific code, and throws a
`CommandSyntaxException` in certain cases. It has a generic type `S`, which defines the type of the _command source_.
The command
source provides some context in which a command was run. In Minecraft, the command source is typically a
`ServerCommandSource` which can represent a server, a command block, a remote connection (RCON), a player or an entity.

The single method in `Command`, `run(CommandContext<S>)` takes a `CommandContext<S>` as the sole parameter and returns
an integer. The command context holds your command source of `S` and allows you to obtain arguments, look at the parsed
command nodes and see the input used in this command.

Like other functional interfaces, it is usually used as a lambda or a method reference:

```java
Command<ServerCommandSource> command = context -> {
return 0;
};
```

The integer can be considered the result of the command. 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. Brigadier provides a constant to indicate success; `Command#SINGLE_SUCCESS`.

#### What Can the `ServerCommandSource` Do?

A `ServerCommandSource` 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 run in or the server the command was run
on.

You can access the command source from a command context by calling `getSource()` on the `CommandContext` instance.

```java
Command<ServerCommandSource> command = context -> {
ServerCommandSource source = context.getSource();
return 0;
};
```

### Registering a Basic Command

Commands are registered within the `CommandRegistrationCallback` provided by the Fabric API.

::: info
For information on registering callbacks, please see the [Events](../events.md) guide.
:::

The event should be registered in your mod's initializer.

The callback has three parameters:

* `CommandDispatcher<ServerCommandSource> dispatcher` - Used to register, parse and execute commands. `S` is the type
of command source the command dispatcher supports.
* `CommandRegistryAccess registryAccess` - Provides an abstraction to registries that may be passed to certain command
argument methods
* `CommandManager.RegistrationEnvironment environment` - Identifies the type of server the commands are being registered
on.

In the mod initializer, we just register a simple command:

@[code lang=java transcludeWith=:::_1](@/reference/latest/src/main/java/com/example/docs/command/FabricDocsReferenceCommands.java)

In the `sendFeedback()` method, the first parameter is the text to be sent, which is a `Supplier<Text>` to avoid
instantiating Text objects when not needed.

The second parameter determines whether to broadcast the feedback to other
operators. Generally, if the command is to query something without actually affecting the world, such as query the
current time or some player's score, it should be `false`. If the command does something, such as changing the
time or modifying someone's score, it should be `true`.

If the command fails, instead of calling `sendFeedback()`, you may directly throw any exception and the server or client
will handle it appropriately.

`CommandSyntaxException` is generally thrown to indicate syntax errors in commands or arguments. You can also implement
your own exception.

To execute this command, you must type `/foo`, which is case-sensitive.

#### Registration Environment

If desired, you can also make sure a command is only registered under some specific circumstances, for example, only in
the dedicated environment:

@[code lang=java highlight={2} transcludeWith=:::2](@/reference/latest/src/main/java/com/example/docs/command/FabricDocsReferenceCommands.java)

#### Command Requirements

Let's say you have a command that 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<S>` which will supply a `ServerCommandSource`
to test with and determine if the `CommandSource` can execute the command.

@[code lang=java highlight={3} transcludeWith=:::3](@/reference/latest/src/main/java/com/example/docs/command/FabricDocsReferenceCommands.java)

This command will only execute if the source of the command is a level 2 operator at a minimum, including command
blocks. Otherwise, the command is not registered.

This has the side effect of not showing this command in tab completion to anyone who is not a level 2 operator. This is
also why you cannot tab-complete most commands when you do not enable cheats.

#### Sub Commands

To add a sub command, you register the first literal node of the command normally. To have a sub command, you have to append the next literal node to the existing node.

@[code lang=java highlight={3} transcludeWith=:::7](@/reference/latest/src/main/java/com/example/docs/command/FabricDocsReferenceCommands.java)

Similar to arguments, sub command nodes can also be set optional. In the following case, both `/subtater`
and `/subtater subcommand` will be valid.

@[code lang=java highlight={2,8} transcludeWith=:::8](@/reference/latest/src/main/java/com/example/docs/command/FabricDocsReferenceCommands.java)

### Client Commands

Fabric API has a `ClientCommandManager` in `net.fabricmc.fabric.api.client.command.v2` package that can be used to register client-side commands. The code should exist only in client-side code.

@[code lang=java transcludeWith=:::1](@/reference/latest/src/client/java/com/example/docs/client/command/FabricDocsReferenceClientCommands.java)

### Command Redirects

Command redirects - also known as aliases - are a way to redirect the functionality of one command to another. This is useful for when you want to change the name of a command, but still want to support the old name.

@[code lang=java transcludeWith=:::12](@/reference/latest/src/client/java/com/example/docs/client/command/FabricDocsReferenceClientCommands.java)

### FAQ

<br>

###### Why does my code not compile?

* Catch or throw a `CommandSyntaxException` - `CommandSyntaxException` is not a `RuntimeException`. If you throw it,
where it is thrown should be in methods that throw `CommandSyntaxException` in method signatures, or be caught.
Brigadier will handle the checked exceptions and forward the proper error message in the game for you.

* Issues with generics - You may have an issue with generics once in a while. If you are registering server
commands (which are most of the case), make sure you are using `CommandManager.literal`
or `CommandManager.argument` instead of `LiteralArgumentBuilder.literal` or `RequiredArgumentBuilder.argument`.

* Check `sendFeedback()` method - You may have forgotten to provide a boolean as the second argument. Also remember
that, since Minecraft 1.20, the first parameter is `Supplier<Text>` instead of `Text`.

* A Command should return an integer - When registering commands, the `executes()` method accepts a `Command` object,
which is usually a lambda. The lambda should return an integer, instead of other types.

###### Can I register commands in runtime?

::: warning
You can do this, but it is not recommended. You would get the `CommandManager` from the server and add anything commands
you wish to its `CommandDispatcher`.

After that, you need to send the command tree to every player again
using `CommandManager.sendCommandTree(ServerPlayerEntity)`.

This is required because the client locally caches the command tree it receives during login (or when operator packets
are sent) for local completions-rich error messages.
:::

###### Can I unregister commands in runtime?

::: warning
You can also do this, however, it is much less stable than registering commands at runtime and could cause unwanted side
effects.

To keep things simple, you need to use reflection on Brigadier and remove nodes. After this, you need to send the
command tree to every player again using `sendCommandTree(ServerPlayerEntity)`.

If you don't send the updated command tree, the client may think a command still exists, even though the server will
fail execution.
:::
46 changes: 46 additions & 0 deletions develop/commands/suggestions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
title: Command Suggestions
description: Learn how to suggest command argument values to users.
authors:
- IMB11
---

# Command Suggestions

Minecraft has a powerful command suggestion system that's used in many places, such as the `/give` command. This system allows you to suggest values for command arguments to the user, which they can then select from - it's a great way to make your commands more user-friendly and ergonomic.

## Suggestion Providers

A `SuggestionProvider` is used to make a list of suggestions that will be sent to the client. A suggestion provider is a functional interface that takes a `CommandContext` and a `SuggestionBuilder` and returns some `Suggestions`. The `SuggestionProvider` returns a `CompletableFuture` as the suggestions may not be available immediately.

## Using Suggestion Providers

To use a suggestion provider, you need to call the `suggests` method on the argument builder. This method takes a `SuggestionProvider` and returns a new argument builder with the suggestion provider attached.

@[code java transcludeWith=:::9 highlight={4}](@/reference/latest/src/main/java/com/example/docs/command/FabricDocsReferenceCommands.java)

## Built-in Suggestion Providers

There are a few built-in suggestion providers that you can use:

| Suggestion Provider | Description |
| ----------------------------------------- | -------------------------------------------- |
| `SuggestionProviders.SUMMONABLE_ENTITIES` | Suggests all entities that can be summoned. |
| `SuggestionProviders.AVAILABLE_SOUNDS` | Suggests all sounds that can be played. |
| `LootCommand.SUGGESTION_PROVIDER` | Suggests all loot tables that are available. |
| `SuggestionProviders.ALL_BIOMES` | Suggests all biomes that are available. |

## Creating a Custom Suggestion Provider

If a built-in provider doesn't satisfy your needs, you can create your own suggestion provider. To do this, you need to create a class that implements the `SuggestionProvider` interface and override the `getSuggestions` method.

For this example, we'll make a suggestion provider that suggests all the player usernames on the server.

@[code java transcludeWith=:::1](@/reference/latest/src/main/java/com/example/docs/command/PlayerSuggestionProvider.java)

To use this suggestion provider, you would simply pass an instance of it into the `.suggests` method on the argument builder.

Obviously, suggestion providers can be more complex, since they can also read the command context to provide suggestions based on the command's state - such as the arguments that have already been provided.

This could be in the form of reading the player's inventory and suggesting items, or entities that are nearby the player.

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.example.docs.client.command;

import net.minecraft.text.Text;

import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;

// Class to contain all mod client command registrations.
public class FabricDocsReferenceClientCommands implements ClientModInitializer {
@Override
public void onInitializeClient() {
// :::1
ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> {
dispatcher.register(ClientCommandManager.literal("clienttater").executes(context -> {
context.getSource().sendFeedback(Text.literal("Called /clienttater with no arguments."));
return 1;
}));
});
// :::1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.example.docs.command;

import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.exceptions.CommandSyntaxException;

import net.minecraft.util.math.BlockPos;

// :::1
public class BlockPosArgumentType implements ArgumentType<BlockPos> {
/**
* Parse the BlockPos from the reader in the {x, y, z} format.
*/
@Override
public BlockPos parse(StringReader reader) throws CommandSyntaxException {
try {
// This requires the argument to be surrounded by quotation marks.
// eg: "{1, 2, 3}"
String string = reader.readString();

// Remove the { and } from the string using regex.
string = string.replace("{", "").replace("}", "");

// Split the string into the x, y, and z values.
String[] split = string.split(",");

// Parse the x, y, and z values from the split string.
int x = Integer.parseInt(split[0].trim());
int y = Integer.parseInt(split[1].trim());
int z = Integer.parseInt(split[2].trim());

// Return the BlockPos.
return new BlockPos(x, y, z);
} catch (Exception e) {
// Throw an exception if anything fails inside the try block.
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherParseException().create("Invalid BlockPos format. Expected {x, y, z}");
}
}
}
// :::1
Loading

0 comments on commit 6cd45d8

Please sign in to comment.