Skip to content

Commit eb14e71

Browse files
feat: custom Brigadier argument suggestions (#536)
* Initial argument suggestions commit * Add example with filtering * Simple cleanup * Fix spelling * Update example command + small fixup * Replace internal class with api serializer * Small text reformat * Put long code into multiple lines for better readabilty * Wording changes * Small change * Document site in introduction * Small fixup * Rename argument from stacksize to amount * Apply suggestions from code review Co-authored-by: Matouš Kučera <mk@kcra.me> --------- Co-authored-by: Matouš Kučera <mk@kcra.me>
1 parent 44e7161 commit eb14e71

File tree

6 files changed

+202
-0
lines changed

6 files changed

+202
-0
lines changed

config/sidebar.paper.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ const paper: SidebarsConfig = {
143143
"dev/api/command-api/basics/executors",
144144
"dev/api/command-api/basics/registration",
145145
"dev/api/command-api/basics/requirements",
146+
"dev/api/command-api/basics/argument-suggestions",
146147
],
147148
},
148149
{
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
---
2+
slug: /dev/command-api/basics/argument-suggestions
3+
description: Documentation about defining custom argument suggestions.
4+
---
5+
6+
import SuggestionTooltip from "./assets/suggestion-tooltip.png";
7+
import GiveItemCommandMp4 from "./assets/give-item-command.mp4";
8+
import SelectNameCommandMp4 from "./assets/select-name-command.mp4";
9+
10+
# Argument Suggestions
11+
Sometimes, you want to send your own suggestions to users. For this, you can use the `suggests(SuggestionProvider<CommandSourceStack>)` method when declaring
12+
arguments.
13+
14+
## Examining `SuggestionProvider<S>`
15+
The `SuggestionProvider<S>` interface is defined as follows:
16+
17+
```java title="SuggestionProvider.java"
18+
@FunctionalInterface
19+
public interface SuggestionProvider<S> {
20+
CompletableFuture<Suggestions> getSuggestions(final CommandContext<S> context, final SuggestionsBuilder builder) throws CommandSyntaxException;
21+
}
22+
```
23+
24+
Similar to other classes or interfaces with a `<S>` generic parameter, for Paper, this is usually a `CommandSourceStack`. Furthermore, similar to the `Command<S>` interface,
25+
this is a functional interface, which means that instead of passing in a class which implements this interface, we can just pass a lambda statement or a method reference.
26+
27+
Our lambda consists of two parameters, `CommandContext<S>` and `SuggestionsBuilder`, and expects to have a `CompletableFuture<Suggestions>` returned.
28+
29+
A very simple lambda for our `suggests` method might look like this:
30+
```java
31+
Commands.argument("name", StringArgumentType.word())
32+
.suggests((ctx, builder) -> builder.buildFuture());
33+
```
34+
35+
This example obviously does not suggest anything, as we haven't added any suggestions yet.
36+
37+
## The `SuggestionsBuilder`
38+
The `SuggestionsBuilder` has a few methods we can use to construct our suggestions:
39+
40+
### Input retrieval
41+
The first type of methods we will cover are the input retrieval methods: `getInput()`, `getStart()`, `getRemaining()`, and `getRemainingLowerCase()`.
42+
The following table displays what each returns with the following input typed in the chat bar: `/customsuggestions Asumm13Text`.
43+
44+
| Method | Return Value | Description |
45+
|----------------------------|--------------------------------|---------------------------------------------------------------|
46+
| getInput() | /customsuggestions Asumm13Text | The full chat input |
47+
| getStart() | 19 | The index of the first character of the argument's input |
48+
| getRemaining() | Asumm13Text | The input for the current argument |
49+
| getRemainingLowerCase() | asumm13text | The input for the current argument, lowercased |
50+
51+
### Suggestions
52+
The following overloads of the `SuggestionsBuilder#suggest` method add values that will be send to the client as argument suggestions:
53+
54+
| Overload | Description |
55+
|--------------------------|-------------------------------------------------|
56+
| suggest(String) | Adds a String to the suggestions |
57+
| suggest(String, Message) | Adds a String with a tooltip to the suggestions |
58+
| suggest(int) | Adds an int to the suggestions |
59+
| suggest(int, Message) | Adds an int with a tooltip to the suggestions |
60+
61+
There are two ways of retrieving a `Message` instance:
62+
- Using `LiteralMessage`, which can be used for basic, non-formatted text.
63+
- Using the `MessageComponentSerializer`, which can be used to serialize `Component` objects into `Message` objects.
64+
65+
For example, if you add a suggestion like this:
66+
```java
67+
builder.suggest("suggestion", MessageComponentSerializer.message().serialize(
68+
MiniMessage.miniMessage().deserialize("<green>Suggestion tooltip")
69+
));
70+
```
71+
72+
It will look like this on the client:
73+
<img src={SuggestionTooltip} style={{width: "100%"}}/>
74+
75+
### Building
76+
There are two methods we can use to build our `Suggestions` object. The only difference between those is that one directly returns the finished `Suggestions` object,
77+
whilst the other one returns a `CompletableFuture<Suggestions>`.
78+
79+
The reason for these two methods is that `SuggestionProvider` expects the return value to be `CompletableFuture<Suggestions>`. This for once
80+
allows for constructing your suggestions asynchronously inside a `CompletableFuture.supplyAsync(Supplier<Suggestions>)` statement, or synchronously directly inside our
81+
lambda and returning the final `Suggestions` object asynchronously.
82+
83+
Here are the same suggestions declared in the two different ways mentioned above:
84+
```java
85+
// Here, you are safe to use all Paper API
86+
Commands.argument("name", StringArgumentType.word())
87+
.suggests((ctx, builder) -> {
88+
builder.suggest("first");
89+
builder.suggest("second");
90+
91+
return builder.buildFuture();
92+
});
93+
94+
// Here, most Paper API is not usable
95+
Commands.argument("name", StringArgumentType.word())
96+
.suggests((ctx, builder) -> CompletableFuture.supplyAsync(() -> {
97+
builder.suggest("first");
98+
builder.suggest("second");
99+
100+
return builder.build();
101+
}));
102+
```
103+
104+
## Example: Suggesting amounts in a give item command
105+
In commands, where you give players items, you oftentimes include an amount argument. We could suggest `1`, `16`, `32`, and `64` as common amounts for
106+
items given. The command implementation could look like this:
107+
108+
```java
109+
@NullMarked
110+
public class SuggestionsTest {
111+
112+
public static LiteralCommandNode<CommandSourceStack> constructGiveItemCommand() {
113+
// Create new command: /giveitem
114+
return Commands.literal("giveitem")
115+
116+
// Require a player to execute the command
117+
.requires(ctx -> ctx.getExecutor() instanceof Player)
118+
119+
// Declare a new ItemStack argument
120+
.then(Commands.argument("item", ArgumentTypes.itemStack())
121+
122+
// Declare a new integer argument with the bounds of 1 to 99
123+
.then(Commands.argument("stacksize", IntegerArgumentType.integer(1, 99))
124+
125+
// Here, we use method references, since otherwise, our command definition would grow too big
126+
.suggests(SuggestionsTest::getStackSizeSuggestions)
127+
.executes(SuggestionsTest::executeCommandLogic)
128+
129+
)
130+
)
131+
.build();
132+
}
133+
134+
private static CompletableFuture<Suggestions> getStackSizeSuggestions(final CommandContext<CommandSourceStack> ctx, final SuggestionsBuilder builder) {
135+
// Suggest 1, 16, 32, and 64 to the user when they reach the 'amount' argument
136+
builder.suggest(1);
137+
builder.suggest(16);
138+
builder.suggest(32);
139+
builder.suggest(64);
140+
return builder.buildFuture();
141+
}
142+
143+
private static int executeCommandLogic(final CommandContext<CommandSourceStack> ctx) {
144+
// We know that the executor will be a player, so we can just silently return
145+
if (!(ctx.getSource().getExecutor() instanceof Player player)) {
146+
return Command.SINGLE_SUCCESS;
147+
}
148+
149+
// If the player has no empty slot, we tell the player that they have no free inventory space
150+
if (player.getInventory().firstEmpty() == -1) {
151+
player.sendRichMessage("<light_purple>You do not have enough space in your inventory!");
152+
return Command.SINGLE_SUCCESS;
153+
}
154+
155+
// Retrieve our argument values
156+
final ItemStack item = ctx.getArgument("item", ItemStack.class);
157+
final int amount = IntegerArgumentType.getInteger(ctx, "amount");
158+
159+
// Set the item's amount and give it to the player
160+
item.setAmount(amount);
161+
final int firstEmptySlot = player.getInventory().firstEmpty();
162+
player.getInventory().setItem(firstEmptySlot, item);
163+
164+
// Send a confirmation message
165+
player.sendRichMessage("<light_purple>You have been given <white><amount>x</white> <aqua><item></aqua>!",
166+
Placeholder.component("amount", Component.text(amount)),
167+
Placeholder.component("item", Component.translatable(item).hoverEvent(item))
168+
);
169+
return Command.SINGLE_SUCCESS;
170+
}
171+
}
172+
```
173+
174+
And here is how the command looks in-game:
175+
<FullWidthVideo src={GiveItemCommandMp4}/>
176+
177+
## Example: Filtering by user input
178+
If you have multiple values, it is suggested that you filter your suggestions by what the user has already put in. For this, we can declare the following, simple command
179+
as a test:
180+
181+
```java
182+
public static LiteralCommandNode<CommandSourceStack> constructStringSuggestionsCommand() {
183+
final List<String> names = List.of("Alex", "Andreas", "Stephanie", "Sophie", "Emily");
184+
185+
return Commands.literal("selectname")
186+
.then(Commands.argument("name", StringArgumentType.word())
187+
188+
.suggests((ctx, builder) -> {
189+
names.stream()
190+
.filter(entry -> entry.toLowerCase().startsWith(builder.getRemainingLowerCase()))
191+
.forEach(builder::suggest);
192+
return builder.buildFuture();
193+
})
194+
195+
).build();
196+
}
197+
```
198+
199+
This simple setup filters suggestions by user input, providing a smooth user experience when running the command:
200+
<FullWidthVideo src={SelectNameCommandMp4}/>
736 KB
Binary file not shown.
1.14 MB
Binary file not shown.
89.3 KB
Loading

docs/paper/dev/api/command-api/basics/introduction.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ The following sites are worth-while to look through first when learning about Br
3030
- [Command Executors](./executors)
3131
- [Command Registration](./registration)
3232
- [Command Requirements](./requirements)
33+
- [Argument Suggestions](./argument-suggestions)
3334

3435
For a reference of more advanced arguments, you should look here:
3536
- [Minecraft Arguments](../arguments/minecraft)

0 commit comments

Comments
 (0)