Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Document custom argument suggestions #536

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions config/sidebar.paper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ const paper: SidebarsConfig = {
"dev/api/command-api/basics/executors",
"dev/api/command-api/basics/registration",
"dev/api/command-api/basics/requirements",
"dev/api/command-api/basics/argument-suggestions",
],
},
{
Expand Down
199 changes: 199 additions & 0 deletions docs/paper/dev/api/command-api/basics/argument-suggestions.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
---
slug: /dev/command-api/basics/argument-suggestions
description: Documentation about defining custom argument suggestions.
---

import SuggestionTooltip from "./assets/suggestion-tooltip.png";
import GiveItemCommandMp4 from "./assets/give-item-command.mp4";
import SelectNameCommandMp4 from "./assets/select-name-command.mp4";

# Argument Suggestions
Sometimes, you want to send your own suggestions to users. For this, you can use the `suggests(SuggestionProvider<CommandSourceStack>)` method when declaring
arguments.

## Examining `SuggestionProvider<S>`
The `SuggestionProvider<S>` interface is defined as follows:

```java title="SuggestionProvider.java"
@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 is usually 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 consists of two parameters, `CommandContext<S>` and `SuggestionsBuilder`, and expects to have a `CompletableFuture<Suggestions>` returned.

A very simple lambda for our `suggests` method might look like this:
```java
Commands.argument("name", StringArgumentType.word())
.suggests((ctx, builder) -> builder.buildFuture());
```

This example obviously does not suggest anything, as we haven't added any suggestions yet.

## The `SuggestionBuilder`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small typo SuggestionsBuilder

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 each returns 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 add values that will be send to the client as argument suggestions:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same typo here


| 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 an int with a tooltip to the suggestions |

There are two ways of retrieving a `Message` instance:
- Using `LiteralMessage`, which can be used for basic, non-formatted text
- Using the `MessageComponentSerializer`, which can be used to serialize `Component` objects into `Message` objects.

For example, if you add a suggestion like this:
```java
builder.suggest("suggestion", MessageComponentSerializer.message().serialize(
miniMessage().deserialize("<green>Suggestion tooltip")
));
```

It will look like this on the client:
<img src={SuggestionTooltip} style={{width: "100%"}}/>

### Building
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,
whilst the other one returns a `CompletableFuture<Suggestions>`.

The reason for these two methods is that `SuggestionProvider` expects the return value to be `CompletableFuture<Suggestions>`. This for once
allows for constructing your suggestions asynchronously inside a `CompletableFuture.supplyAsync(Supplier<Suggestions>)` statement, or synchronously directly inside our
lambda and returning the final `Suggestions` object asynchronously.

Here are the same suggestions declared in the two different ways mentioned above:
```java
// 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 item command
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
items given. The command implementation could look like this:

```java
@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");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

amount is better for the argument name i think


// Set the item's amount and give it to the player
item.setAmount(amount);
player.getInventory().setItem(player.getInventory().firstEmpty(), item);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be better using the new Player#give method to handle stack size properly. But if you think it's too complicated the first empty slot could be saved in a separate var.


// 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:
<FullWidthVideo src={GiveItemCommandMp4}/>

## 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:

```java
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();
}
```

This simple setup filters suggestions by user input, providing a smooth user experience when running the command:
<FullWidthVideo src={SelectNameCommandMp4}/>
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/paper/dev/api/command-api/basics/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ The following sites are worth-while to look through first when learning about Br
- [Command Executors](./executors)
- [Command Registration](./registration)
- [Command Requirements](./requirements)
- [Argument Suggestions](./argument-suggestions)

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