Anvil
Building blocksPlayersCapabilities

Dialogs

Wait for a dialog the server shows, fill its inputs and press its buttons as the client does.

Dialogs observes the dialogs the server shows a player and presses their buttons as the Minecraft client does. Dialogs arrived in Minecraft 1.21.6, so players on 1.21.11 and newer get this capability; players on older releases do not, and retrieving it fails with CapabilityUnavailableException. Install the dialogs unit and its Session dependency, or explicitly select the default capability unit.

Answer a dialog

This fragment belongs in a live test after Alice has connected and joined a server whose plugin shows her a login dialog. It uses SimulatedPlayer alice and these imports:

import me.whereareiam.anvil.capability.dialogs.Dialogs;
import me.whereareiam.anvil.capability.dialogs.model.dialog.Dialog;
import java.time.Duration;
import java.util.Map;

Wait for the dialog by its title, fill its password input and press its button:

Dialogs dialogs = alice.capability(Dialogs.class);
Dialog login = dialogs.shown("Log in", Duration.ofSeconds(10));
dialogs.press("Log in", Map.of("password", "Secret123!"));
dialogs.closed(Duration.ofSeconds(10));

shown(title) waits until the open dialog's title contains the text. press(button, inputs) takes the button's label, matched exactly, and the values of the inputs to fill by their keys; inputs left out keep their initial value. press(button) presses with every input at its initial value.

What a button does

Anvil does what the Minecraft client does for the button's action:

ActionThe client
run_commandRuns the command, without its leading slash, after checking it
dynamic/run_commandRuns the template with each $(key) replaced by the value of that input, after checking it
customSends a custom click with the action's ID and payload
dynamic/customSends a custom click with the action's ID; its payload holds the action's additions and every input value under its key
show_dialogOpens the dialog in place of this one
suggest_command, open_url, copy_to_clipboard, change_page, or no actionSends nothing

A template puts in the text of a text input, the ID of the selected option, the number of a slider, without a fraction when it is whole, and the on_true or on_false text of a checkbox. A custom payload holds text and options as strings, a checkbox as a byte and a slider as a float. A value the input would not accept fails the press: text longer than its maximum length, a checkbox value other than true or false, an option the input does not have, or a number outside the slider's range.

The payload a custom or dynamic/custom action declares stays in the player's worker, which sends it with the click; the action's DialogAction carries the ID and a getHandle() that the press names it by. Pressing such a button fails with an IllegalStateException when the server replaced or cleared its dialog, or the connection that showed it ended, before the click was sent.

After the action the dialog stays open, closes or gives way to the waiting screen, as its after_action says. A dialog whose after_action Anvil does not know has DialogAfterAction.OTHER, and pressing its buttons fails without sending anything. waitingForResponse() tells whether the waiting screen is shown; it stays until the server shows another dialog, and close() leaves it once the client enables its back button, five seconds after it appeared.

Commands the client asks about

Before it runs a command from a button, the client checks it against the command tree the server sent, as Commands describes:

The check findsThe client
NO_ISSUESSends the command
PARSE_ERRORS or PERMISSIONS_REQUIREDAsks whether to run it
SIGNATURE_REQUIREDAsks, and never sends it; it only offers to put it into the chat input or the clipboard

While the client asks, confirmation() returns the command and the reason, and current() is empty. confirm() runs the command, or sends nothing for a signed one, and decline() returns to the dialog:

dialogs.press("Teleport");
CommandConfirmation question = dialogs.confirmation().orElseThrow();
assertEquals(CommandCheck.PERMISSIONS_REQUIRED, question.getReason());
dialogs.confirm();

Import me.whereareiam.anvil.capability.dialogs.model.click.CommandConfirmation and me.whereareiam.anvil.capability.commands.type.CommandCheck.

The check needs the player's Commands capability, which the default capability unit includes. Dialogs work without it, except that pressing a button that runs a command then fails and asks to install it.

Follow the screens

current() returns the dialog on screen, and history() every dialog the player was shown, including dialogs a button opened, across reconnects. Like the client:

  • a dialog the server shows replaces the open dialog and returns to the screen below it;
  • the server's clear packet returns from the open dialog to the screen below it;
  • joining a level, respawning, changing servers behind a proxy, starting configuration again or reconnecting closes every dialog;
  • close() presses Escape: on a dialog it presses the exit button, if there is one, and closes the dialog, which fails for a dialog that cannot be closed with Escape; on a confirmation it declines.

A Dialog holds its type, such as minecraft:multi_action, its title, its body texts, its inputs and its buttons, the exit button last. A dialog list has a button for each dialog it lists, labelled with the dialog's external title; a dialog tag lists nothing. Its text is plain text, built as Messages builds received messages, so a default notice button reads gui.ok.

A dialog shown during the configuration phase has isConfiguration() set. The client runs no commands there: a button that runs one sends nothing and leaves the dialog as it is.

The server may refer to a dialog of its dialog registry, such as one from a data pack, instead of sending it. Anvil resolves it from the registry the server sent during configuration and keeps its getRegistryId() and getRegistryName(). A dialog the server left out of the registry because the client knows it, such as a built-in one, carries only those two; its title, body, inputs and buttons are empty.

Watch the dialogs shown next

When the same dialog is shown again, such as a login form after a wrong password, start a watch before the press, so the earlier dialog cannot satisfy the wait:

DialogWatch shown = dialogs.watch();
dialogs.press("Log in", Map.of("password", "wrong"));
Dialog again = shown.shown(dialog -> dialog.getBody().contains("Invalid password."), Duration.ofSeconds(10));
shown.neverShown(dialog -> dialog.getTitle().contains("Welcome"), Duration.ofSeconds(2));

A watch sees only the dialogs shown after it was created, and each wait continues after the dialog the previous one found; neverShown does not move it. Pressing, closing and confirming stay on Dialogs. Import me.whereareiam.anvil.capability.dialogs.DialogWatch; expectShown(matcher) describes the next matching dialog for player.awaitFirst(...), and unread() lists the dialogs shown since the previous wait.

On this page