Argument Suggestions
Sometimes, you want to send your own suggestions to users. For this, you can use the RequiredArgumentBuilder#suggests(SuggestionProvider)
method.
Examining the SuggestionProvider<S>
method
The SuggestionProvider<S>
interface is defined as follows:
@FunctionalInterface
public interface SuggestionProvider<S> {
CompletableFuture<Suggestions> getSuggestions(final CommandContext<S> context, final SuggestionsBuilder builder) throws CommandSyntaxException;
}
Similar to other classes or interfaces with a <S>
generic parameter, for Paper, this usually is a CommandSourceStack
. Furthermore, similar to the Command<S>
interface,
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.
Our lambda/method requires two parameters, CommandContext<S>
and SuggestionsBuilder
, returning a CompletableFuture<Suggestions>
.
In order to retrieve our return value, we can just run SuggestionsBuilder#buildFuture()
(or SuggestionsBuilder#build()
, if we already are inside a CompletableFuture).
A very simple lambda for our suggests
method might look like this:
Commands.argument("name", StringArgumentType.word())
.suggests((ctx, builder) -> builder.buildFuture())
This example obviously does not suggest anything, as we haven't added suggestions yet.
A suggestion builder's methods
The SuggestionsBuilder
has a few methods we can use to construct our suggestions:
Input retrieval
The first type of methods we will cover are the input retrieval methods: getInput()
, getStart()
, getRemaining()
, and getRemainingLowerCase()
.
The following table displays what they return with the following input typed in the chat bar: /customsuggestions Asumm13Text
.
Method | Return Value | Description |
---|---|---|
getInput() | /customsuggestions Asumm13Text | The full chat input |
getStart() | 19 | The index of the first character of the argument's input |
getRemaining() | Asumm13Text | The input for the current argument |
getRemainingLowerCase() | asumm13text | The input for the current argument, lowercased |
Suggestions
The following overloads of the SuggestionBuilder#suggest
method all add values that will be send to the client as suggestions, but accept
difference parameters:
Overload | Description |
---|---|
suggest(String) | Adds a String to the suggestions |
suggest(String, Message) | Adds a String with a tooltip to the suggestions |
suggest(int) | Adds an int to the suggestions |
suggest(int, Message) | Adds a String with a tooltip to the suggestions |
The Message
interface has the following implementations: LiteralMessage
, which can be used for basic, non-formatted text, and AdventureComponent
, which can be
constructed using an Adventure component by calling new AdventureComponent(Component)
.
For example, if you add a suggestion like this:
builder.suggest("suggestion", new AdventureComponent(miniMessage().deserialize("<green>Suggestion tooltip")));
It will look like this on the client:
Building
There are two methods we can use to build our suggestions. The only difference between the both are that one directly returns the finished Suggestions
object,
whilst the other one returns a CompletableFuture<Suggestions>
.
The reason for these two methods is that the SuggestionProvider
's method requires the return value to be a CompletableFuture<Suggestions>
. This for once
allows for constructing your suggestions asynchronously inside a CompletableFuture.supplyAsync(Supplier<Suggestions>)
or synchronously directly inside our
lambda/method and only returning the final Suggestions
object asynchronously.
Here are the same suggestions declared in the two different ways mentioned above:
// Here, you are safe to use all Paper API
Commands.argument("name", StringArgumentType.word())
.suggests((ctx, builder) -> {
builder.suggest("first");
builder.suggest("second");
return builder.buildFuture();
});
// Here, most Paper API is not usable
Commands.argument("name", StringArgumentType.word())
.suggests((ctx, builder) -> CompletableFuture.supplyAsync(() -> {
builder.suggest("first");
builder.suggest("second");
return builder.build();
}));
Example: Suggesting amounts in a give command
In commands where you give players certain items, you oftentimes include an amount argument. We could suggest 1
, 16
, 32
, and 64
as common amounts for
items given. The command implementation could look like this:
@NullMarked
public class SuggestionsTest {
public static LiteralCommandNode<CommandSourceStack> constructGiveItemCommand() {
// Create new command: /giveitem
return Commands.literal("giveitem")
// Require a player to execute the command
.requires(ctx -> ctx.getExecutor() instanceof Player)
// Declare a new ItemStack argument
.then(Commands.argument("item", ArgumentTypes.itemStack())
// Declare a new integer argument with the bounds of 1 to 99
.then(Commands.argument("stacksize", IntegerArgumentType.integer(1, 99))
// Here, we use method references, since otherwise, our command definition would grow too big
.suggests(SuggestionsTest::getStackSizeSuggestions)
.executes(SuggestionsTest::executeCommandLogic)
)
)
.build();
}
private static CompletableFuture<Suggestions> getStackSizeSuggestions(final CommandContext<CommandSourceStack> ctx, final SuggestionsBuilder builder) {
// Suggest 1, 16, 32, and 64 to the user when they reach the 'stacksize' argument
builder.suggest(1);
builder.suggest(16);
builder.suggest(32);
builder.suggest(64);
return builder.buildFuture();
}
private static int executeCommandLogic(final CommandContext<CommandSourceStack> ctx) {
// We know that the executor will be a player, so we can just silently return
if (!(ctx.getSource().getExecutor() instanceof Player player)) {
return Command.SINGLE_SUCCESS;
}
// If the player has no empty slot, we tell the player that they have no free inventory space
if (player.getInventory().firstEmpty() == -1) {
player.sendRichMessage("<light_purple>You do not have enough space in your inventory!");
return Command.SINGLE_SUCCESS;
}
// Retrieve our argument values
final ItemStack item = ctx.getArgument("item", ItemStack.class);
final int amount = IntegerArgumentType.getInteger(ctx, "stacksize");
// Set the item's amount and give it to the player
item.setAmount(amount);
player.getInventory().setItem(player.getInventory().firstEmpty(), item);
// Send a confirmation message
player.sendRichMessage("<light_purple>You have been given <white><amount>x</white> <aqua><item></aqua>!",
Placeholder.component("amount", Component.text(amount)),
Placeholder.component("item", Component.translatable(item).hoverEvent(item))
);
return Command.SINGLE_SUCCESS;
}
}
And here is how the command looks in-game:
Example: Filtering by user input
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 as a test:
public static LiteralCommandNode<CommandSourceStack> constructStringSuggestionsCommand() {
final List<String> names = List.of("Alex", "Andreas", "Stephanie", "Sophie", "Emily");
return Commands.literal("selectname")
.then(Commands.argument("name", StringArgumentType.word())
.suggests((ctx, builder) -> {
names.stream()
.filter(entry -> entry.toLowerCase().startsWith(builder.getRemainingLowerCase()))
.forEach(builder::suggest);
return builder.buildFuture();
})
).build();
}
And as you can see in the preview below, this simple setup filters suggestions by user input, providing a smooth user experience when using your command: