diff --git a/astro.config.ts b/astro.config.ts index 770c846ec..ef870847a 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -197,6 +197,7 @@ export default defineConfig({ "paper/dev/command-api/basics/requirements", "paper/dev/command-api/basics/argument-suggestions", "paper/dev/command-api/basics/custom-arguments", + "paper/dev/command-api/basics/exceptions", ], }, { diff --git a/src/content/docs/paper/dev/api/command-api/basics/assets/exception-component.png b/src/content/docs/paper/dev/api/command-api/basics/assets/exception-component.png new file mode 100644 index 000000000..fea73b012 Binary files /dev/null and b/src/content/docs/paper/dev/api/command-api/basics/assets/exception-component.png differ diff --git a/src/content/docs/paper/dev/api/command-api/basics/assets/exception-context.png b/src/content/docs/paper/dev/api/command-api/basics/assets/exception-context.png new file mode 100644 index 000000000..34f0c21b3 Binary files /dev/null and b/src/content/docs/paper/dev/api/command-api/basics/assets/exception-context.png differ diff --git a/src/content/docs/paper/dev/api/command-api/basics/assets/exception-full-health.png b/src/content/docs/paper/dev/api/command-api/basics/assets/exception-full-health.png new file mode 100644 index 000000000..ad0729e6f Binary files /dev/null and b/src/content/docs/paper/dev/api/command-api/basics/assets/exception-full-health.png differ diff --git a/src/content/docs/paper/dev/api/command-api/basics/custom-arguments.md b/src/content/docs/paper/dev/api/command-api/basics/custom-arguments.md index 500beada8..0e604ba88 100644 --- a/src/content/docs/paper/dev/api/command-api/basics/custom-arguments.md +++ b/src/content/docs/paper/dev/api/command-api/basics/custom-arguments.md @@ -9,7 +9,7 @@ reduce code repetition. ## Why would you use custom arguments? As example, if you want to have an argument for a player, which is currently online and an operator, you could use a player argument type, add custom suggestions, and throw a -`CommandSyntaxException` in your `executes(...)` method body. This would look like this: +[`CommandSyntaxException`](/paper/dev/command-api/basics/exceptions) in your `executes(...)` method body. This would look like this: ```java Commands.argument("player", ArgumentTypes.player()) diff --git a/src/content/docs/paper/dev/api/command-api/basics/exceptions.md b/src/content/docs/paper/dev/api/command-api/basics/exceptions.md new file mode 100644 index 000000000..b3cca5658 --- /dev/null +++ b/src/content/docs/paper/dev/api/command-api/basics/exceptions.md @@ -0,0 +1,167 @@ +--- +title: Exceptions +description: A guide to command syntax exceptions. +slug: paper/dev/command-api/basics/exceptions +version: "26.3" +--- + +If a command cannot run with the input it was given, you can throw a `CommandSyntaxException`. The server catches it and sends +its message to the command sender in red, without logging a stack trace. You can throw it in the `executes(...)` method of a command node, +as well as in the `parse` and `convert` methods of [custom arguments](/paper/dev/command-api/basics/custom-arguments). + +## Exception types +Instead of constructing a `CommandSyntaxException` yourself, you create an exception type, which holds the message, +and call its `create(...)` method whenever you want to throw the exception. Brigadier provides the following exception types, +all of which implement the `CommandExceptionType` interface: + +| Class | Message | Creating an exception | +|------------------------------------------------------------------|----------------------------------------|-------------------------------------------------------| +| `SimpleCommandExceptionType` | A fixed `Message`. | `create()` | +| `DynamicCommandExceptionType` | A `Function`. | `create(Object)` | +| `Dynamic2CommandExceptionType` to `Dynamic4CommandExceptionType` | A function taking two to four objects. | `create(Object, Object)`, and so on | +| `DynamicNCommandExceptionType` | A function taking an `Object[]`. | `createWithContext(ImmutableStringReader, Object...)` | + +:::caution + +`DynamicNCommandExceptionType#create(Object, Object...)` does not pass its first argument to the function. +For example, `create("a", "b", "c")` only passes `["b", "c"]`. Use `createWithContext` or one of the other exception types instead. + +::: + +An exception type can be used to create any number of exceptions, so declare it as a constant and reuse it: + +```java +private static final SimpleCommandExceptionType ERROR_NOT_PLAYER = new SimpleCommandExceptionType( + new LiteralMessage("Only players can use this command!") +); + +private static final DynamicCommandExceptionType ERROR_FULL_HEALTH = new DynamicCommandExceptionType(name -> { + return new LiteralMessage(name + " already has full health!"); +}); +``` + +You can then throw them in your command: + +```java +Commands.literal("heal") + .executes(ctx -> { + if (!(ctx.getSource().getExecutor() instanceof Player player)) { + throw ERROR_NOT_PLAYER.create(); + } + + final double maxHealth = player.getAttribute(Attribute.MAX_HEALTH).getValue(); + if (player.getHealth() >= maxHealth) { + throw ERROR_FULL_HEALTH.create(player.getName()); + } + + player.setHealth(maxHealth); + return Command.SINGLE_SUCCESS; + }); +``` + +If the player already has full health, they see the message in red: + +![The message "Steve already has full health!" in red in the chat](./assets/exception-full-health.png) + +## Using components as messages +`LiteralMessage` only contains plain text. If you want to use a `Component` as the message, convert it using +the [`MessageComponentSerializer`](jd:paper:io.papermc.paper.command.brigadier.MessageComponentSerializer): + +```java +private static final DynamicCommandExceptionType ERROR_FULL_HEALTH = new DynamicCommandExceptionType(name -> { + return MessageComponentSerializer.message().serialize(MiniMessage.miniMessage().deserialize( + " already has full health!", + Placeholder.unparsed("name", name.toString()) + )); +}); +``` + +The message is still shown in red, but parts with their own color, like the name above, keep it. + +![The message "Steve already has full health!" in the chat, with the name in gold and the rest in red](./assets/exception-component.png) + +## Showing where the input is invalid +While a custom argument is parsing its input in `parse(StringReader)`, you can use `createWithContext(reader, ...)` instead +of `create(...)`. The exception then remembers the input and the position of the reader's cursor, and the server shows a second +line below the message. This line contains up to 10 characters before the cursor, the rest of the input in red and underlined, +and `<--[HERE]`. Clicking the line puts the command back into the chat box. + +The following argument only accepts even numbers. It moves the cursor back to the start of the number before throwing, +so that the whole number is marked, which is also what Brigadier's own argument types do: + +```java title="EvenIntegerArgument.java" +public final class EvenIntegerArgument implements CustomArgumentType { + + private static final DynamicCommandExceptionType ERROR_NOT_EVEN = new DynamicCommandExceptionType(value -> { + return new LiteralMessage(value + " is not an even number!"); + }); + + @Override + public Integer parse(final StringReader reader) throws CommandSyntaxException { + final int start = reader.getCursor(); + final int value = reader.readInt(); + if (value % 2 != 0) { + reader.setCursor(start); + throw ERROR_NOT_EVEN.createWithContext(reader, value); + } + + return value; + } + + @Override + public ArgumentType getNativeType() { + return IntegerArgumentType.integer(); + } +} +``` + +Running `/split 7` with this argument shows the following: + +![The message "7 is not an even number!" in the chat, followed by the line "split 7<--[HERE]" with the 7 underlined in red](./assets/exception-context.png) + +The second line is only shown for exceptions thrown while the command is being parsed. Exceptions thrown in `executes(...)` +or in `CustomArgumentType.Converted#convert` only show the message, even if they were created with context. + +## Built-in exceptions +`CommandSyntaxException.BUILT_IN_EXCEPTIONS` provides the exception types Brigadier uses itself, for example when a number +argument is out of range. On Paper, Vanilla replaces them with translatable messages, so players see them in the language +of their client. You can reuse them in your own code instead of writing your own messages: + +```java +throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.integerTooHigh().createWithContext(reader, value, maximum); +``` + +Note that the exceptions for numbers that are too low or too high take the found value first and the limit second. +The exception above shows `Integer must not be more than : found `. + +The following table lists all built-in exceptions with their English message: + +| Method | Arguments | Message | +|-----------------------------------------|--------------------|---------------------------------------------------------------------| +| `doubleTooLow()` | `found`, `minimum` | Double must not be less than `minimum`: found `found` | +| `doubleTooHigh()` | `found`, `maximum` | Double must not be more than `maximum`: found `found` | +| `floatTooLow()` | `found`, `minimum` | Float must not be less than `minimum`: found `found` | +| `floatTooHigh()` | `found`, `maximum` | Float must not be more than `maximum`: found `found` | +| `integerTooLow()` | `found`, `minimum` | Integer must not be less than `minimum`: found `found` | +| `integerTooHigh()` | `found`, `maximum` | Integer must not be more than `maximum`: found `found` | +| `longTooLow()` | `found`, `minimum` | Long must not be less than `minimum`: found `found` | +| `longTooHigh()` | `found`, `maximum` | Long must not be more than `maximum`: found `found` | +| `literalIncorrect()` | `expected` | Expected literal `expected` | +| `readerExpectedStartOfQuote()` | | Expected quote to start a string | +| `readerExpectedEndOfQuote()` | | Unclosed quoted string | +| `readerInvalidEscape()` | `character` | Invalid escape sequence '\\`character`' in quoted string | +| `readerInvalidBool()` | `value` | Invalid boolean: expected 'true' or 'false' but found '`value`' | +| `readerInvalidInt()` | `value` | Invalid integer '`value`' | +| `readerExpectedInt()` | | Expected integer | +| `readerInvalidLong()` | `value` | Invalid long '`value`' | +| `readerExpectedLong()` | | Expected long | +| `readerInvalidDouble()` | `value` | Invalid double '`value`' | +| `readerExpectedDouble()` | | Expected double | +| `readerInvalidFloat()` | `value` | Invalid float '`value`' | +| `readerExpectedFloat()` | | Expected float | +| `readerExpectedBool()` | | Expected boolean | +| `readerExpectedSymbol()` | `symbol` | Expected '`symbol`' | +| `dispatcherUnknownCommand()` | | Unknown or incomplete command. See below for error | +| `dispatcherUnknownArgument()` | | Incorrect argument for command | +| `dispatcherExpectedArgumentSeparator()` | | Expected whitespace to end one argument, but found trailing data | +| `dispatcherParseException()` | `message` | Could not parse command: `message` |