Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Changelog

Every minor version ships one batch of changes. Nothing changes for code that does not use the
new APIs, except where a section says otherwise.

## 5.7.0

### Added

- Annotations: `@Default("value")` makes an argument optional with a value used when it is left
out, converted by the parameter type's converter (on an `Optional` parameter, the value is
wrapped). An invalid default on a primitive parameter is refused at registration.
`Optional<T>` parameters, already supported, are now documented.
- `CommandManager#describe()` returns a public tree of `CommandDescription` / `ArgumentDescription`
records (name, path, aliases, description, usage, permission, in-game only, required and optional
arguments, subcommands) to generate a plugin's command documentation.

## 5.6.0

### Added

- **`platform-paper`**: registers commands through Paper's Brigadier API (`LifecycleEvents.COMMANDS`).
Typed arguments become Brigadier nodes (numbers, boolean, word, greedy string; a type with a
custom converter becomes a word with its tab completer's suggestions), permission and
requirements become the node's `requires`, and messages can be components
(`setComponentFormatter`). Compiled against the Paper API 1.21.4 (Java 21), tested on Paper
1.21.4 and 26.3. See the README for what happens to commands added or removed after startup.
- `BrigadierCommandTree`, the platform-neutral translation of a manager's commands into Brigadier
nodes.
- `CommandManager#getConverter(Class)`.
- `paper-test-plugin`, a test plugin for the Paper platform.

### Fixed

- `ComponentMessageFormatter.miniMessage(...)`: the `<arg>`, `<requirement>` and `<usage>` tags are
known for every key, empty when the key has no such value, instead of being shown as typed.

## 5.5.0

### Added

- **Messages as `Component`** on Paper and Velocity: `manager.setComponentFormatter(...)` with a
`ComponentMessageFormatter<S>`, which receives the `MessageKey` and the values (`arg`,
`requirement`, `usage`) apart, and whose component is sent as a component, click and hover
events included. `ComponentMessageFormatter.miniMessage(...)` reads MiniMessage templates, from a
function or from a `MessageHandler`.
- New `adventure-addon` module holding it (`ComponentMessages.install` for other Adventure
platforms); the Velocity platform depends on it.
- Core: `MessageKey`, `MessageDispatcher` and `CommandManager#setMessageDispatcher` /
`#sendMessage(receiver, key, text, values)`, the platform-neutral hook the component formatter
uses. `MessageHandler#getMessage(MessageKey)` gives the text of a key.

Without a component formatter or a dispatcher, messages are sent exactly as in 5.4.0.

## 5.4.0

### Changed

- **No network call when a manager is created.** The `CommandManager` constructor no longer calls
`Updater.checkUpdates()`; call it yourself to check for a newer version. Visible change: the
"CommandsAPI is up to date" line is no longer logged at startup.
- `-Dcommands.updater=false` turns the update check off, even when a plugin calls it.
- The update check compares versions as versions: a local version newer than the published one
(a snapshot, a build named after a commit) is no longer announced as outdated, and a failed
check announces nothing.

### Added

- `CommandManager#unregisterAll()` removes every command the manager registered, with their
aliases and subcommands, for the shutdown of a plugin. Commands of other managers stay.
- `Requirement#name()` fills `%requirement%` in the generic requirement message, instead of the
class name. It defaults to the simple class name, so existing messages read the same. A
non-empty `errorMessage()` is still sent as-is.

### Fixed

- Spigot: unregistering a command now removes it from the command map's known commands, under
its label, aliases and namespaced forms. It used to be marked as unregistered but kept
answering.
- Spigot and Velocity: unregistering a label that another plugin holds (because this manager did
not get it) no longer removes that plugin's command.
174 changes: 169 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,158 @@ the JDA platform, and it receives the message **after** the internal placeholder
`null` restores the default behaviour: with no formatter registered, messages are sent
exactly as the handler produced them.

> ⚠️ Messages are plain `String` in and out, because the `core` module also serves platforms
> without Adventure, such as JDA. A consumer working with `Component` has to serialize it,
> usually to the legacy `Β§` format, which drops `hover` and `click` events β€” command messages
> such as "you do not have permission" do not need them, and honouring them would mean
> pulling Adventure into `core`.
> ℹ️ The text formatter works on plain `String`, because the `core` module also serves
> platforms without Adventure, such as JDA. To keep `click` and `hover` events, use a
> component formatter (below) on Paper and Velocity.

### Messages as `Component` (Paper, Velocity)

A **component formatter** builds each message as an Adventure `Component`, which is then sent
as a component β€” click and hover events reach the player intact:

```java
public interface ComponentMessageFormatter<S> {
Component format(S receiver, MessageKey key, Map<String, String> placeholders);
}
```

The message comes by its `MessageKey` (`NO_PERMISSION`, `ONLY_IN_GAME`, `ARG_NOT_RECOGNIZED`,
`REQUIREMENT`, `DISABLED`, `TOO_LONG`, `INVALID_FORMAT`, `USAGE`, `INTERNAL_ERROR`), and its values
come apart, never substituted in the text: `arg` (the input), `requirement` (the requirement's
`errorMessage()`, or its `name()` when empty) and `usage`.

```java
// MiniMessage templates from the server's configuration; values become <arg>, <requirement>, <usage>
manager.setComponentFormatter(ComponentMessageFormatter.miniMessage(
key -> getConfig().getString("messages." + key.name().toLowerCase())));

// Or keep the texts in a MessageHandler written in MiniMessage: %arg% is read as <arg>
manager.setComponentFormatter(ComponentMessageFormatter.miniMessage(myHandler));
```

Values are inserted as plain text: an argument typed as `<red>abc` is shown as typed. A formatter
returning `null` for a key sends that message as text, through the `MessageHandler` and the text
formatter, as before. With a component formatter set, the text formatter only sees those messages.

The formatter lives in the `adventure-addon` module, pulled in by the Paper and Velocity
platforms. On another Adventure platform, install it with `ComponentMessages.install(manager, formatter)`.

---

## 🧭 Paper platform (Brigadier)

`platform-paper` registers commands through Paper's Brigadier API (`LifecycleEvents.COMMANDS`)
instead of the Bukkit command map, so the client knows the shape of each command: arguments are
coloured, a wrong value is flagged before it is sent, and suggestions come from the tab completers.

```java
public final class MyPlugin extends JavaPlugin {
@Override
public void onEnable() {
CommandManager<MyPlugin> manager = new fr.traqueur.commands.paper.CommandManager<>(this);
manager.command("give").arg("amount", Integer.class).executor((sender, args) -> { ... }).register();
}
}
```

| CommandsAPI argument | Brigadier node |
|---|---|
| `int`/`Integer`, `long`, `double`, `float`, `byte` (library converter) | the matching number |
| `boolean`/`Boolean` (library converter) | boolean |
| `Infinite` | greedy string, to the end of the line |
| `String`, a type with a custom converter, a number with your own converter | word, with the tab completer's suggestions |

The permission and the requirements of a command become the `requires` of its node: a player who
cannot run a command does not see it. A command is executable once its required arguments are
given; running it goes through the usual invoker, so conversion, checks and messages are unchanged.
`setComponentFormatter` is available as on Velocity.

> ℹ️ A word only accepts `0-9 A-Z a-z _ - . +`, as in vanilla: the client refuses a `String`
> argument holding other characters (accents, `<`, `&`). Declare it `Infinite` when it must accept
> any text.

### When Paper builds the tree

Paper asks for the commands when it builds its command tree: **once at startup, after the plugins
are enabled, then on every `/minecraft:reload`**. The nodes are built from what the manager holds
at that moment. Tested on Paper 1.21.4 and 26.3:

| What happens | Result |
|---|---|
| Command registered in `onEnable` | Available at startup. |
| New label registered after startup | Unknown command until the next `/minecraft:reload` (a warning is logged), then available. |
| Command unregistered after startup | Its node stays, hidden by its `requires`: unknown command, and the clients' command lists are refreshed. |
| Same label registered again | Visible again, **with the arguments the node had when it was built**; a changed shape needs a reload. |
| `unregisterAll()` | Every command of the manager becomes unknown; other plugins' commands stay. |

`platform-spigot` stays available for Spigot, or for a Paper server that prefers the command map.

---

## πŸ“– Describing the commands

`manager.describe()` returns a public, read-only tree of the commands registered through the
manager, to generate a plugin's documentation without touching the internal structures:

```java
for (CommandDescription command : manager.describe()) {
System.out.println(command.usage() + " - " + command.description()
+ (command.permission().isEmpty() ? "" : " (" + command.permission() + ")"));
command.requiredArguments(); // List<ArgumentDescription>: name, type, optional, infinite
command.optionalArguments();
command.subcommands(); // List<CommandDescription>, with their full path ("home set")
}
```

The usage is the one set on the command, or the one the library generates, listing every
subcommand.

---

## 🧹 Removing every command of a manager

When a plugin stops, `unregisterAll()` removes every command its manager registered, with their
aliases and subcommands:

```java
@Override
public void onDisable() {
manager.unregisterAll();
}
```

Only the commands of **this** manager go: those of other managers, other plugins and the
server stay in place, including a label another plugin held and this manager did not get.

---

## 🚧 Requirement messages

A requirement that fails sends its `errorMessage()` as-is. When it is empty, the generic
`MessageHandler#getRequirementMessage()` is sent instead, and its `%requirement%` placeholder
is filled with `Requirement#name()` β€” the simple name of the requirement's class by default.
Override `name()` to show something readable:

```java
@Override
public String name() {
return "Γͺtre dans le Nether";
}
```

---

## πŸ”„ Update check

Creating a `CommandManager` makes **no network call**. To be told about a newer version of the
library, ask for it:

```java
Updater.checkUpdates(); // asynchronous, logs once
```

A failed check, or a version that cannot be compared (a build named after a commit), announces
nothing. A server can turn the check off for every plugin with `-Dcommands.updater=false`.

---

Expand All @@ -93,6 +240,8 @@ exactly as the handler produced them.
traqueur-dev-commandsapi/
β”œβ”€β”€ core/ # Platform-agnostic command logic
β”œβ”€β”€ spigot/ # Spigot implementation
β”œβ”€β”€ paper/ # Paper implementation, on Brigadier
β”œβ”€β”€ adventure-addon/ # Component messages, for Adventure platforms
β”œβ”€β”€ <platform>-test-plugin/ # The test plugin for the specified platform
└── velocity/ # Velocity implementation
```
Expand Down Expand Up @@ -181,6 +330,21 @@ tasks.withType<JavaCompile>().configureEach {

> πŸ’‘ Without this flag, parameter names default to `arg0`, `arg1`, etc. You can always use `@Arg("name")` explicitly to avoid this requirement.

### Optional arguments and default values

| Parameter | Left out by the sender |
|---|---|
| `@Arg("amount") int amount` | required: the usage is sent |
| `@Arg("amount") Optional<Integer> amount` | `Optional.empty()` |
| `@Arg("amount") @Default("1") int amount` | `1` |
| `@Arg("amount") @Default("1") Optional<Integer> amount` | `Optional.of(1)` |
| `@Arg("text") @Infinite @Default("hello") String text` | `"hello"` |

The `@Default` value is written as the sender would type it and goes through the converter of the
parameter's type when the command runs, custom converters included. On a primitive parameter, an
invalid value (`@Default("lots") int`) is refused when the commands are registered. Optional
parameters must come after the required ones: required arguments are always read first.

---

## πŸ’‘ Example (Spigot)
Expand Down
9 changes: 9 additions & 0 deletions adventure-addon/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
dependencies {
api project(':core')
compileOnly 'net.kyori:adventure-api:4.17.0'
compileOnly 'net.kyori:adventure-text-minimessage:4.17.0'
testImplementation 'net.kyori:adventure-api:4.17.0'
testImplementation 'net.kyori:adventure-text-minimessage:4.17.0'
testImplementation 'net.kyori:adventure-text-serializer-plain:4.17.0'
testImplementation project(':core').sourceSets.test.output
}
Loading
Loading