|
| 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}/> |
0 commit comments