From b2849798b53017785e1d1a75ac39bca49ff0f422 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 11 Jul 2026 16:44:32 -0700 Subject: [PATCH 1/2] Add Dialogs API and use it for command confirmations (#3021) Introduces a public api/dialogs/ package that wraps Paper's modal dialog system (io.papermc.paper.dialog.Dialog, present on Minecraft 26+) alongside the existing Panels API, so core and addons can present real modal choices without the player typing commands. - DialogButton: label + optional tooltip + an onClick Consumer run on the main thread when clicked. - DialogBuilder: fluent builder (title/body localized via User, escapable, pause) supporting confirmation(yes, no) and multi-action button() menus; build() -> BBDialog with show(User). Everything is carried as Adventure Components end-to-end so click/interaction data survives. - Dialogs.isSupported(): guard for graceful degradation on older servers. First consumer: ConfirmableCommand.askConfirmation now shows a [Confirm]/[Cancel] dialog instead of asking the player to re-type the command. Enabled by default via the new island.confirmation.use-dialog setting (opt out to keep the type-again prompt). If the dialog cannot be built or shown for any reason, it falls back to the classic prompt so a confirmation is never silently lost. New en-US locale keys (commands.confirmation.title, .dialog-body, .buttons.confirm, .buttons.cancel) translated into all 22 other locales. Note: DialogBuilder.build() of a full dialog needs the server's dialog registry provider, which MockBukkit does not supply (like NMS), so those paths run on a live server; unit tests cover support detection, button holders, pre-factory validation and the confirmation fallback. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01G7UmQDCP5MGHEZqFiw44zH --- .../world/bentobox/bentobox/Settings.java | 23 ++ .../api/commands/ConfirmableCommand.java | 53 +++++ .../bentobox/api/dialogs/BBDialog.java | 42 ++++ .../bentobox/api/dialogs/DialogBuilder.java | 209 ++++++++++++++++++ .../bentobox/api/dialogs/DialogButton.java | 89 ++++++++ .../bentobox/api/dialogs/Dialogs.java | 36 +++ .../bentobox/api/dialogs/package-info.java | 31 +++ src/main/resources/config.yml | 6 + src/main/resources/locales/cs.yml | 5 + src/main/resources/locales/de.yml | 5 + src/main/resources/locales/en-US.yml | 5 + src/main/resources/locales/es.yml | 5 + src/main/resources/locales/fr.yml | 5 + src/main/resources/locales/hr.yml | 5 + src/main/resources/locales/hu.yml | 5 + src/main/resources/locales/id.yml | 5 + src/main/resources/locales/it.yml | 5 + src/main/resources/locales/ja.yml | 5 + src/main/resources/locales/ko.yml | 5 + src/main/resources/locales/lv.yml | 5 + src/main/resources/locales/nl.yml | 5 + src/main/resources/locales/pl.yml | 5 + src/main/resources/locales/pt-BR.yml | 5 + src/main/resources/locales/pt.yml | 5 + src/main/resources/locales/ro.yml | 5 + src/main/resources/locales/ru.yml | 5 + src/main/resources/locales/tr.yml | 5 + src/main/resources/locales/uk.yml | 5 + src/main/resources/locales/vi.yml | 5 + src/main/resources/locales/zh-CN.yml | 5 + src/main/resources/locales/zh-HK.yml | 5 + .../island/IslandResetCommandTest.java | 26 +++ .../api/dialogs/DialogBuilderTest.java | 91 ++++++++ 33 files changed, 721 insertions(+) create mode 100644 src/main/java/world/bentobox/bentobox/api/dialogs/BBDialog.java create mode 100644 src/main/java/world/bentobox/bentobox/api/dialogs/DialogBuilder.java create mode 100644 src/main/java/world/bentobox/bentobox/api/dialogs/DialogButton.java create mode 100644 src/main/java/world/bentobox/bentobox/api/dialogs/Dialogs.java create mode 100644 src/main/java/world/bentobox/bentobox/api/dialogs/package-info.java create mode 100644 src/test/java/world/bentobox/bentobox/api/dialogs/DialogBuilderTest.java diff --git a/src/main/java/world/bentobox/bentobox/Settings.java b/src/main/java/world/bentobox/bentobox/Settings.java index 5c62e0289..658039b1a 100644 --- a/src/main/java/world/bentobox/bentobox/Settings.java +++ b/src/main/java/world/bentobox/bentobox/Settings.java @@ -344,6 +344,13 @@ public class Settings implements ConfigObject { @ConfigEntry(path = "island.confirmation.invites", since = "1.8.0") private boolean inviteConfirmation = false; + @ConfigComment("Show sensitive-command confirmations as a modal [Confirm]/[Cancel] dialog") + @ConfigComment("instead of asking the player to type the command again. Requires a server") + @ConfigComment("that supports dialogs (Minecraft 26+); older servers always use the type-again") + @ConfigComment("prompt regardless of this setting.") + @ConfigEntry(path = "island.confirmation.use-dialog", since = "3.21.0") + private boolean useDialogConfirmation = true; + @ConfigComment("Sets the minimum length an island custom name is required to have.") @ConfigEntry(path = "island.name.min-length") private int nameMinLength = 4; @@ -922,6 +929,22 @@ public void setInviteConfirmation(boolean inviteConfirmation) { this.inviteConfirmation = inviteConfirmation; } + /** + * @return whether sensitive-command confirmations should use a modal dialog + * @since 3.21.0 + */ + public boolean isUseDialogConfirmation() { + return useDialogConfirmation; + } + + /** + * @param useDialogConfirmation whether to use a modal dialog for confirmations + * @since 3.21.0 + */ + public void setUseDialogConfirmation(boolean useDialogConfirmation) { + this.useDialogConfirmation = useDialogConfirmation; + } + /** * @return the databasePrefix */ diff --git a/src/main/java/world/bentobox/bentobox/api/commands/ConfirmableCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/ConfirmableCommand.java index e4302d418..03d096442 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/ConfirmableCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/ConfirmableCommand.java @@ -7,7 +7,12 @@ import org.bukkit.scheduler.BukkitTask; import world.bentobox.bentobox.api.addons.Addon; +import world.bentobox.bentobox.api.dialogs.BBDialog; +import world.bentobox.bentobox.api.dialogs.DialogBuilder; +import world.bentobox.bentobox.api.dialogs.DialogButton; +import world.bentobox.bentobox.api.dialogs.Dialogs; import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.util.Util; /** * An extension of {@link CompositeCommand} that adds a confirmation step for @@ -96,6 +101,11 @@ protected ConfirmableCommand(CompositeCommand parent, String label, String... al * @param confirmed The action to execute upon successful confirmation. */ public void askConfirmation(User user, String message, Runnable confirmed) { + // Prefer a modal dialog when the server supports it and it is enabled. + // Falls through to the type-again prompt if the dialog cannot be shown. + if (useDialog(user) && showConfirmationDialog(user, message, confirmed)) { + return; + } // Check for pending confirmations if (toBeConfirmed.containsKey(user)) { if (toBeConfirmed.get(user).topLabel().equals(getTopLabel()) && toBeConfirmed.get(user).label().equalsIgnoreCase(getLabel())) { @@ -135,6 +145,49 @@ public void askConfirmation(User user, Runnable confirmed) { askConfirmation(user, "", confirmed); } + /** + * Whether this confirmation should be presented as a modal dialog rather than + * the type-the-command-again prompt. True only for online players, when the + * server supports dialogs and the {@code island.confirmation.use-dialog} + * setting is enabled. + * + * @param user the user being asked + * @return true if a dialog should be shown + */ + private boolean useDialog(User user) { + return user.isPlayer() && Dialogs.isSupported() && getPlugin().getSettings().isUseDialogConfirmation(); + } + + /** + * Shows a two-button [Confirm]/[Cancel] dialog. Confirming runs the action; + * cancelling (or dismissing with Escape) aborts it. + * + * @param user the user to ask + * @param message a pre-translated context message, or empty + * @param confirmed the action to run on confirmation + * @return true if the dialog was shown; false if it could not be built/shown, + * in which case the caller should fall back to the type-again prompt + */ + private boolean showConfirmationDialog(User user, String message, Runnable confirmed) { + try { + DialogBuilder builder = new DialogBuilder().title(user, "commands.confirmation.title"); + if (message != null && !message.trim().isEmpty()) { + builder.body(Util.parseMiniMessageOrLegacy(message)); + } + builder.body(user, "commands.confirmation.dialog-body") + .confirmation( + DialogButton.of(user, "commands.confirmation.buttons.confirm", u -> confirmed.run()), + DialogButton.of(user, "commands.confirmation.buttons.cancel", + u -> u.sendMessage("commands.confirmation.request-cancelled"))); + builder.build().show(user); + return true; + } catch (Exception e) { + getPlugin().logError("Could not show confirmation dialog, falling back to command prompt: " + + e.getMessage()); + return false; + } + } + /** * A record that holds the details of a pending confirmation request. * diff --git a/src/main/java/world/bentobox/bentobox/api/dialogs/BBDialog.java b/src/main/java/world/bentobox/bentobox/api/dialogs/BBDialog.java new file mode 100644 index 000000000..0ae03662d --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/api/dialogs/BBDialog.java @@ -0,0 +1,42 @@ +package world.bentobox.bentobox.api.dialogs; + +import org.eclipse.jdt.annotation.NonNull; + +import io.papermc.paper.dialog.Dialog; +import world.bentobox.bentobox.api.user.User; + +/** + * A built dialog, ready to be shown to a player. Create one with a + * {@link DialogBuilder}. + * + * @author tastybento + * @since 3.21.0 + */ +public class BBDialog { + + private final Dialog dialog; + + BBDialog(@NonNull Dialog dialog) { + this.dialog = dialog; + } + + /** + * Shows this dialog to the given user. Has no effect if the user is not an + * online player. + * + * @param user the user to show the dialog to, not null + */ + public void show(@NonNull User user) { + if (user.isPlayer() && user.getPlayer() != null) { + user.getPlayer().showDialog(dialog); + } + } + + /** + * @return the underlying Paper dialog, for advanced use + */ + @NonNull + public Dialog getDialog() { + return dialog; + } +} diff --git a/src/main/java/world/bentobox/bentobox/api/dialogs/DialogBuilder.java b/src/main/java/world/bentobox/bentobox/api/dialogs/DialogBuilder.java new file mode 100644 index 000000000..b013a9bcd --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/api/dialogs/DialogBuilder.java @@ -0,0 +1,209 @@ +package world.bentobox.bentobox.api.dialogs; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.eclipse.jdt.annotation.NonNull; + +import io.papermc.paper.dialog.Dialog; +import io.papermc.paper.registry.data.dialog.ActionButton; +import io.papermc.paper.registry.data.dialog.DialogBase; +import io.papermc.paper.registry.data.dialog.action.DialogAction; +import io.papermc.paper.registry.data.dialog.body.DialogBody; +import io.papermc.paper.registry.data.dialog.body.PlainMessageDialogBody; +import io.papermc.paper.registry.data.dialog.type.DialogType; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickCallback; +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.util.Util; + +/** + * Fluent builder for modal {@link BBDialog dialogs}, in the spirit of + * {@code PanelBuilder}. + *

+ * A dialog needs a {@link #title(Component) title} and either a + * {@link #confirmation(DialogButton, DialogButton) confirmation} (two buttons) + * or one or more {@link #button(DialogButton) buttons} (a multi-action menu). + * Titles and body lines are Adventure {@link Component Components}; the + * {@code title}/{@code body} overloads that take a {@link User} translate a + * locale key for that user and parse colors/formatting. + *

+ * Example: + *

{@code
+ * new DialogBuilder()
+ *     .title(user, "commands.island.reset.confirm-title")
+ *     .body(user, "commands.island.reset.confirm-body")
+ *     .confirmation(
+ *         DialogButton.of(user, "general.buttons.confirm", u -> doReset(u)),
+ *         DialogButton.of(user, "general.buttons.cancel", null))
+ *     .build()
+ *     .show(user);
+ * }
+ * + * @author tastybento + * @since 3.21.0 + */ +public class DialogBuilder { + + /** How long a button's server-side click callback stays valid after the dialog is shown. */ + private static final Duration CALLBACK_LIFETIME = Duration.ofMinutes(10); + + private Component title = Component.empty(); + private final List body = new ArrayList<>(); + private boolean escapable = true; + private boolean pause = false; + + private DialogButton yesButton; + private DialogButton noButton; + private final List buttons = new ArrayList<>(); + + /** + * Sets the dialog title from a component. + * + * @param title the title, not null + * @return this builder + */ + public DialogBuilder title(@NonNull Component title) { + this.title = title; + return this; + } + + /** + * Sets the dialog title from a localized translation for the given user. + * + * @param user the user whose locale is used, not null + * @param reference the locale key, not null + * @param variables optional translation variables + * @return this builder + */ + public DialogBuilder title(@NonNull User user, @NonNull String reference, String... variables) { + return title(Util.parseMiniMessageOrLegacy(user.getTranslation(reference, variables))); + } + + /** + * Adds a line of body text from a component. + * + * @param line the body line, not null + * @return this builder + */ + public DialogBuilder body(@NonNull Component line) { + this.body.add(line); + return this; + } + + /** + * Adds a line of body text from a localized translation for the given user. + * + * @param user the user whose locale is used, not null + * @param reference the locale key, not null + * @param variables optional translation variables + * @return this builder + */ + public DialogBuilder body(@NonNull User user, @NonNull String reference, String... variables) { + return body(Util.parseMiniMessageOrLegacy(user.getTranslation(reference, variables))); + } + + /** + * Sets whether the player can dismiss the dialog by pressing Escape. Defaults + * to {@code true}. Set to {@code false} for onboarding flows the player must + * answer, but note this is hostile for routine confirmations. + * + * @param escapable true if Escape dismisses the dialog + * @return this builder + */ + public DialogBuilder escapable(boolean escapable) { + this.escapable = escapable; + return this; + } + + /** + * Sets whether the dialog pauses the game. This only has an effect in + * single-player; on a server it is ignored. Defaults to {@code false}. + * + * @param pause true to pause the game while the dialog is open + * @return this builder + */ + public DialogBuilder pause(boolean pause) { + this.pause = pause; + return this; + } + + /** + * Makes this a two-button confirmation dialog. Mutually exclusive with + * {@link #button(DialogButton)}. + * + * @param yes the affirmative button, not null + * @param no the negative button, not null + * @return this builder + */ + public DialogBuilder confirmation(@NonNull DialogButton yes, @NonNull DialogButton no) { + this.yesButton = yes; + this.noButton = no; + return this; + } + + /** + * Adds a button to a multi-action menu dialog. Mutually exclusive with + * {@link #confirmation(DialogButton, DialogButton)}. + * + * @param button the button, not null + * @return this builder + */ + public DialogBuilder button(@NonNull DialogButton button) { + this.buttons.add(button); + return this; + } + + /** + * Builds the dialog. + * + * @return the built dialog, ready to {@link BBDialog#show(User) show} + * @throws IllegalStateException if neither a confirmation nor any button was set + */ + @NonNull + public BBDialog build() { + boolean confirmation = yesButton != null && noButton != null; + if (!confirmation && buttons.isEmpty()) { + throw new IllegalStateException("A dialog needs either a confirmation() or at least one button()"); + } + + List bodyLines = body.stream().map(DialogBody::plainMessage).toList(); + DialogBase base = DialogBase.builder(title).canCloseWithEscape(escapable).pause(pause) + .afterAction(DialogBase.DialogAfterAction.CLOSE).body(bodyLines).build(); + + DialogType type = confirmation ? DialogType.confirmation(toActionButton(yesButton), toActionButton(noButton)) + : DialogType.multiAction(buttons.stream().map(this::toActionButton).toList()).build(); + + Dialog dialog = Dialog.create(factory -> factory.empty().base(base).type(type)); + return new BBDialog(dialog); + } + + private ActionButton toActionButton(DialogButton button) { + ActionButton.Builder b = ActionButton.builder(button.label()) + .action(DialogAction.customClick((view, audience) -> runOnMainThread(button.onClick(), audience), + ClickCallback.Options.builder().uses(1).lifetime(CALLBACK_LIFETIME).build())); + if (button.tooltip() != null) { + b.tooltip(button.tooltip()); + } + return b.build(); + } + + /** + * Runs a button callback on the server's main thread with the clicking user. + * Server-side dialog callbacks may be delivered off the main thread, so BentoBox + * / addon code (which is not thread-safe) is dispatched back to it. + */ + private static void runOnMainThread(Consumer onClick, Audience audience) { + if (onClick == null || !(audience instanceof Player player)) { + return; + } + User user = User.getInstance(player); + Bukkit.getScheduler().runTask(BentoBox.getInstance(), () -> onClick.accept(user)); + } +} diff --git a/src/main/java/world/bentobox/bentobox/api/dialogs/DialogButton.java b/src/main/java/world/bentobox/bentobox/api/dialogs/DialogButton.java new file mode 100644 index 000000000..b6dab6450 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/api/dialogs/DialogButton.java @@ -0,0 +1,89 @@ +package world.bentobox.bentobox.api.dialogs; + +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + +import net.kyori.adventure.text.Component; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.util.Util; + +/** + * A single clickable button in a {@link DialogBuilder dialog}. + *

+ * A button has a label, an optional tooltip shown on hover and an optional + * {@link #onClick()} callback. The callback is run on the server's main thread, + * with the {@link User} who clicked, when the button is pressed. + * + * @author tastybento + * @since 3.21.0 + */ +public class DialogButton { + + private final Component label; + private final @Nullable Component tooltip; + private final @Nullable Consumer onClick; + + /** + * Creates a button from Adventure components. + * + * @param label the button label, not null + * @param tooltip the hover tooltip, or null for none + * @param onClick the action to run when clicked, or null for a button that just closes the dialog + */ + public DialogButton(@NonNull Component label, @Nullable Component tooltip, @Nullable Consumer onClick) { + this.label = label; + this.tooltip = tooltip; + this.onClick = onClick; + } + + /** + * Creates a button from a component label with no tooltip. + * + * @param label the button label, not null + * @param onClick the action to run when clicked, or null + */ + public DialogButton(@NonNull Component label, @Nullable Consumer onClick) { + this(label, null, onClick); + } + + /** + * Creates a button whose label is a localized translation for the given user. + * The reference is translated and parsed to a component so that colors and + * formatting in the locale entry are honored. + * + * @param user the user whose locale is used, not null + * @param reference the locale key of the label, not null + * @param onClick the action to run when clicked, or null + * @return a new button + */ + @NonNull + public static DialogButton of(@NonNull User user, @NonNull String reference, @Nullable Consumer onClick) { + return new DialogButton(Util.parseMiniMessageOrLegacy(user.getTranslation(reference)), onClick); + } + + /** + * @return the button label + */ + @NonNull + public Component label() { + return label; + } + + /** + * @return the hover tooltip, or null if there is none + */ + @Nullable + public Component tooltip() { + return tooltip; + } + + /** + * @return the action to run when the button is clicked, or null if none + */ + @Nullable + public Consumer onClick() { + return onClick; + } +} diff --git a/src/main/java/world/bentobox/bentobox/api/dialogs/Dialogs.java b/src/main/java/world/bentobox/bentobox/api/dialogs/Dialogs.java new file mode 100644 index 000000000..cae132949 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/api/dialogs/Dialogs.java @@ -0,0 +1,36 @@ +package world.bentobox.bentobox.api.dialogs; + +/** + * Utility helpers for the Dialogs API. + * + * @author tastybento + * @since 3.21.0 + */ +public final class Dialogs { + + private static final boolean SUPPORTED = classPresent("io.papermc.paper.dialog.Dialog"); + + private Dialogs() { + // Utility class + } + + /** + * Whether the running server supports Paper's dialog system. Dialogs were + * added in Minecraft 26; on older servers this returns {@code false} and + * callers should fall back to chat or panel presentation. + * + * @return true if dialogs can be shown + */ + public static boolean isSupported() { + return SUPPORTED; + } + + private static boolean classPresent(String name) { + try { + Class.forName(name, false, Dialogs.class.getClassLoader()); + return true; + } catch (Throwable e) { + return false; + } + } +} diff --git a/src/main/java/world/bentobox/bentobox/api/dialogs/package-info.java b/src/main/java/world/bentobox/bentobox/api/dialogs/package-info.java new file mode 100644 index 000000000..48927fc56 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/api/dialogs/package-info.java @@ -0,0 +1,31 @@ +/** + * API for server-driven modal dialog creation and usage. + * + *

+ * Alongside the {@link world.bentobox.bentobox.api.panels Panels API}, BentoBox + * provides a Dialogs API that wraps Paper's dialog system + * ({@code io.papermc.paper.dialog.Dialog}) so core and addons can present real + * modal choices to players without them having to type commands. Unlike + * inventory-GUI panels, dialogs are true modal UI: buttons, optional body text, + * and they can be configured so that pressing Esc does not dismiss + * them. + *

+ *

+ * Dialogs are built with a fluent {@link world.bentobox.bentobox.api.dialogs.DialogBuilder} + * in the spirit of {@code PanelBuilder}. Titles and body text are localized + * through the existing {@link world.bentobox.bentobox.api.user.User} translation + * system and are carried end-to-end as Adventure + * {@link net.kyori.adventure.text.Component Components}, so click/interaction + * data survives (the legacy {@code §}-string chat path cannot carry it). + *

+ *

+ * Buttons carry a {@link world.bentobox.bentobox.api.dialogs.DialogButton#onClick()} + * callback that runs on the server's main thread when the player clicks. Because + * the dialog classes only exist on Minecraft 26+, callers that need to degrade + * gracefully should guard with {@link world.bentobox.bentobox.api.dialogs.Dialogs#isSupported()}. + *

+ * + * @author tastybento + * @since 3.21.0 + */ +package world.bentobox.bentobox.api.dialogs; diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 12d428316..c6f3f294f 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -221,6 +221,12 @@ island: # Team invites will always require confirmation, for safety concerns. # Added since 1.8.0. invites: false + # Show sensitive-command confirmations as a modal [Confirm]/[Cancel] dialog + # instead of asking the player to type the command again. Requires a server + # that supports dialogs (Minecraft 26+); older servers always use the type-again + # prompt regardless of this setting. + # Added since 3.21.0. + use-dialog: true delay: # Time in seconds that players have to stand still before teleport commands activate, e.g. island go. time: 0 diff --git a/src/main/resources/locales/cs.yml b/src/main/resources/locales/cs.yml index 687fe3b51..a1dd4956e 100644 --- a/src/main/resources/locales/cs.yml +++ b/src/main/resources/locales/cs.yml @@ -674,6 +674,11 @@ commands: confirm: 'Napiš příkaz znovu během [seconds]s k potvrzení.' previous-request-cancelled: 'Předchozí žádost o potvrzení zamítnuta.' request-cancelled: 'Časový limit vypršel - žádost zamítnuta.' + title: 'Jsi si jistý?' + dialog-body: 'Potvrď pro pokračování, nebo zruš.' + buttons: + confirm: 'Potvrdit' + cancel: 'Zrušit' delay: previous-command-cancelled: 'Předchozí příkaz zrušen' stand-still: 'Nehýbej se! Teleportuji za [seconds] sekund' diff --git a/src/main/resources/locales/de.yml b/src/main/resources/locales/de.yml index 3653a9947..a5b62f97e 100644 --- a/src/main/resources/locales/de.yml +++ b/src/main/resources/locales/de.yml @@ -714,6 +714,11 @@ commands: confirm: 'Befehl innerhalb von[seconds]szur Bestätigung erneut eingeben.' previous-request-cancelled: 'Vorherige Bestätigungsanforderung abgebrochen.' request-cancelled: 'Bestätigungs-Timeout - -Anforderung abgebrochen.' + title: 'Bist du sicher?' + dialog-body: 'Bestätige, um fortzufahren, oder brich ab.' + buttons: + confirm: 'Bestätigen' + cancel: 'Abbrechen' delay: previous-command-cancelled: 'Vorheriger Befehl abgebrochen' stand-still: 'Nicht bewegen! Teleportiert in [seconds] Sekunden' diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 116e5a211..840eed70a 100644 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -646,6 +646,11 @@ commands: confirm: 'Type command again within [seconds]s to confirm.' previous-request-cancelled: 'Previous confirmation request cancelled.' request-cancelled: 'Confirmation timeout - request cancelled.' + title: 'Are you sure?' + dialog-body: 'Confirm to proceed, or cancel to abort.' + buttons: + confirm: 'Confirm' + cancel: 'Cancel' delay: previous-command-cancelled: 'Previous command cancelled' stand-still: 'Do not move! Teleporting in [seconds] seconds' diff --git a/src/main/resources/locales/es.yml b/src/main/resources/locales/es.yml index ea53bbe67..7f393f66b 100644 --- a/src/main/resources/locales/es.yml +++ b/src/main/resources/locales/es.yml @@ -703,6 +703,11 @@ commands: confirm: 'Vuelva a escribir el comando antes de [seconds]s para confirmar.' previous-request-cancelled: 'Previa solicitud de confirmación cancelada.' request-cancelled: 'Tiempo de espera de confirmación - solicitud cancelada.' + title: '¿Estás seguro?' + dialog-body: 'Confirma para continuar, o cancela para abortar.' + buttons: + confirm: 'Confirmar' + cancel: 'Cancelar' delay: previous-command-cancelled: 'Anterior comando cancelado' stand-still: '¡No te muevas! Teletransportación en [seconds] segundos' diff --git a/src/main/resources/locales/fr.yml b/src/main/resources/locales/fr.yml index 9a7235a33..20d91f0f9 100644 --- a/src/main/resources/locales/fr.yml +++ b/src/main/resources/locales/fr.yml @@ -726,6 +726,11 @@ commands: confirmer. previous-request-cancelled: 'La demande de confirmation est annulée.' request-cancelled: 'Délai de confirmation dépassé - demande annulée.' + title: 'Es-tu sûr ?' + dialog-body: 'Confirme pour continuer, ou annule pour abandonner.' + buttons: + confirm: 'Confirmer' + cancel: 'Annuler' delay: previous-command-cancelled: 'Commande annulée' stand-still: 'Ne bougez pas ! Téléportation dans [seconds] secondes.' diff --git a/src/main/resources/locales/hr.yml b/src/main/resources/locales/hr.yml index ea151873a..aa8cdef04 100644 --- a/src/main/resources/locales/hr.yml +++ b/src/main/resources/locales/hr.yml @@ -685,6 +685,11 @@ commands: confirm: 'Ponovno upišite naredbu unutar [seconds]sza potvrdu.' previous-request-cancelled: 'Prethodni zahtjev za potvrdu otkazan.' request-cancelled: 'Istek potvrde - zahtjev otkazan.' + title: 'Jesi li siguran?' + dialog-body: 'Potvrdi za nastavak ili odustani.' + buttons: + confirm: 'Potvrdi' + cancel: 'Odustani' delay: previous-command-cancelled: 'Prethodna naredba poništena' stand-still: 'Ne miči se! Teleportiranje za [seconds] sekundi' diff --git a/src/main/resources/locales/hu.yml b/src/main/resources/locales/hu.yml index 2656c7bf2..9ee305e37 100644 --- a/src/main/resources/locales/hu.yml +++ b/src/main/resources/locales/hu.yml @@ -722,6 +722,11 @@ commands: confirm: 'A megerősítéshez írja be újra a parancsot [seconds]sidőn belül.' previous-request-cancelled: 'Az előző megerősítési kérés törölve.' request-cancelled: 'Megerősítés időtúllépése – kérés törölve.' + title: 'Biztos vagy benne?' + dialog-body: 'Erősítsd meg a folytatáshoz, vagy mégse a megszakításhoz.' + buttons: + confirm: 'Megerősítés' + cancel: 'Mégse' delay: previous-command-cancelled: 'Az előző parancs megszakítva' stand-still: 'Ne mozdulj! Teleportálás [seconds] másodpercen belül' diff --git a/src/main/resources/locales/id.yml b/src/main/resources/locales/id.yml index 3a62e35ac..4f04960c2 100644 --- a/src/main/resources/locales/id.yml +++ b/src/main/resources/locales/id.yml @@ -696,6 +696,11 @@ commands: confirm: 'Ketik perintah lagi dalam [seconds]suntuk mengonfirmasi.' previous-request-cancelled: 'Permintaan konfirmasi sebelumnya dibatalkan.' request-cancelled: 'Batas waktu konfirmasi - permintaan dibatalkan.' + title: 'Apakah kamu yakin?' + dialog-body: 'Konfirmasi untuk melanjutkan, atau batalkan.' + buttons: + confirm: 'Konfirmasi' + cancel: 'Batal' delay: previous-command-cancelled: 'Perintah sebelumnya dibatalkan' stand-still: 'Jangan bergerak! Teleportasi dalam [seconds] detik' diff --git a/src/main/resources/locales/it.yml b/src/main/resources/locales/it.yml index 25a220b9b..1b2d463cb 100644 --- a/src/main/resources/locales/it.yml +++ b/src/main/resources/locales/it.yml @@ -711,6 +711,11 @@ commands: confirm: 'Riscrivi il comando entro [seconds]sper confermare.' previous-request-cancelled: 'Richiesta di conferma precedente cancellata.' request-cancelled: 'Timeout di conferma - richiesta cancellata.' + title: 'Sei sicuro?' + dialog-body: 'Conferma per procedere, o annulla.' + buttons: + confirm: 'Conferma' + cancel: 'Annulla' delay: previous-command-cancelled: 'Comando precedente annullato' stand-still: 'Non muoverti! Teletrasporto tra [seconds] secondi' diff --git a/src/main/resources/locales/ja.yml b/src/main/resources/locales/ja.yml index 61c19f2c9..ccb8c90e5 100644 --- a/src/main/resources/locales/ja.yml +++ b/src/main/resources/locales/ja.yml @@ -619,6 +619,11 @@ commands: confirm: '[seconds]秒でコマンドをもう一度入力して確認します。' previous-request-cancelled: '前の確認要求が取り消されました' request-cancelled: '確認のタイムアウト-要求が取り消されました' + title: '本当によろしいですか?' + dialog-body: '続行するには確認を、中止するにはキャンセルを選んでください。' + buttons: + confirm: '確認' + cancel: 'キャンセル' delay: previous-command-cancelled: '前のコマンドがキャンセルされました' stand-still: '移動しないでください! [seconds]秒でのテレポート' diff --git a/src/main/resources/locales/ko.yml b/src/main/resources/locales/ko.yml index 0af8d1149..6a92b00bb 100644 --- a/src/main/resources/locales/ko.yml +++ b/src/main/resources/locales/ko.yml @@ -639,6 +639,11 @@ commands: confirm: '계속하려면 [seconds]초안에 명령어를 다시 사용하세요.' previous-request-cancelled: '이전 확인 요청이 취소되었습니다.' request-cancelled: '확인 시간 종료 - 요청이 취소되었습니다.' + title: '정말 실행하시겠습니까?' + dialog-body: '계속하려면 확인을, 취소하려면 취소를 누르세요.' + buttons: + confirm: '확인' + cancel: '취소' delay: previous-command-cancelled: '전에 사용한 커맨드가 취소되어습니다' stand-still: '움직이지마세요! [seconds] 초후 이동됩니다!' diff --git a/src/main/resources/locales/lv.yml b/src/main/resources/locales/lv.yml index aca0c1328..99ce4ae77 100644 --- a/src/main/resources/locales/lv.yml +++ b/src/main/resources/locales/lv.yml @@ -689,6 +689,11 @@ commands: confirm: 'Ievadi komandu atkārtoti [seconds]slaikā, lai apstiprinātu.' previous-request-cancelled: 'Iepriekšējais apstiprinājumu pieprasījums ir apturēts.' request-cancelled: 'Apstiprinājuma noilgums - pieprasījums apturēts.' + title: 'Vai tu esi pārliecināts?' + dialog-body: 'Apstiprini, lai turpinātu, vai atcel.' + buttons: + confirm: 'Apstiprināt' + cancel: 'Atcelt' delay: previous-command-cancelled: 'Iepriekšēja komanda tika atcelta!' stand-still: 'Apstājies! Teleportēšana notiks pēc [seconds] sekundēm' diff --git a/src/main/resources/locales/nl.yml b/src/main/resources/locales/nl.yml index 2999e0dd6..e377ce9ec 100644 --- a/src/main/resources/locales/nl.yml +++ b/src/main/resources/locales/nl.yml @@ -721,6 +721,11 @@ commands: confirm: 'Typ het commando opnieuw binnen [seconds]s om te bevestigen.' previous-request-cancelled: 'Eerder bevestigingsverzoek geannuleerd.' request-cancelled: 'Time-out voor bevestiging - -verzoek geannuleerd.' + title: 'Weet je het zeker?' + dialog-body: 'Bevestig om door te gaan, of annuleer.' + buttons: + confirm: 'Bevestigen' + cancel: 'Annuleren' delay: previous-command-cancelled: 'Vorige opdracht geannuleerd' stand-still: 'Beweeg niet! Teleporteren in [seconds]seconden' diff --git a/src/main/resources/locales/pl.yml b/src/main/resources/locales/pl.yml index c1ffa806d..2c100aa34 100644 --- a/src/main/resources/locales/pl.yml +++ b/src/main/resources/locales/pl.yml @@ -693,6 +693,11 @@ commands: confirm: 'Wprowadź komendę w ciągu [seconds]s, by potwierdzić.' previous-request-cancelled: 'Poprzednie żądanie potwierdzenia zostało anulowane.' request-cancelled: 'Limit czasu potwierdzenia - żądanie anulowane.' + title: 'Jesteś pewien?' + dialog-body: 'Potwierdź, aby kontynuować, lub anuluj.' + buttons: + confirm: 'Potwierdź' + cancel: 'Anuluj' delay: previous-command-cancelled: 'Anulowano poprzednią komendę.' stand-still: 'Nie ruszaj się! Teleportowanie za [seconds] sekund...' diff --git a/src/main/resources/locales/pt-BR.yml b/src/main/resources/locales/pt-BR.yml index ec7154e56..51a1ac99d 100644 --- a/src/main/resources/locales/pt-BR.yml +++ b/src/main/resources/locales/pt-BR.yml @@ -689,6 +689,11 @@ commands: confirm: 'Digite o comando novamente dentro de [seconds]s para confirmar.' previous-request-cancelled: 'Pedido anterior de confirmação cancelado.' request-cancelled: 'Limite de confirmação ultrapassado - pedido cancelado.' + title: 'Você tem certeza?' + dialog-body: 'Confirme para continuar, ou cancele para abortar.' + buttons: + confirm: 'Confirmar' + cancel: 'Cancelar' delay: previous-command-cancelled: 'Comando anterior cancelado' stand-still: 'Não se mova! Teleportando em [seconds] segundos' diff --git a/src/main/resources/locales/pt.yml b/src/main/resources/locales/pt.yml index 30adc7edc..8f93cbbad 100644 --- a/src/main/resources/locales/pt.yml +++ b/src/main/resources/locales/pt.yml @@ -698,6 +698,11 @@ commands: confirm: 'Digite o comando novamente dentro de [segundos]spara confirmar.' previous-request-cancelled: 'Solicitação de confirmação anterior cancelada.' request-cancelled: 'Tempo limite de confirmação - solicitação cancelada.' + title: 'Tens a certeza?' + dialog-body: 'Confirma para continuar, ou cancela para abortar.' + buttons: + confirm: 'Confirmar' + cancel: 'Cancelar' delay: previous-command-cancelled: 'Comando anterior cancelado' stand-still: 'Não se mova! Teletransportando em [segundos] segundos' diff --git a/src/main/resources/locales/ro.yml b/src/main/resources/locales/ro.yml index 3e1b039b1..9fbb06776 100644 --- a/src/main/resources/locales/ro.yml +++ b/src/main/resources/locales/ro.yml @@ -706,6 +706,11 @@ commands: confirm: 'Tastați din nou comanda în [seconds] s pentru a confirma.' previous-request-cancelled: 'Cererea de confirmare anterioară a fost anulată.' request-cancelled: 'Timpul limită de confirmare - cererea a fost anulată.' + title: 'Ești sigur?' + dialog-body: 'Confirmă pentru a continua, sau anulează.' + buttons: + confirm: 'Confirmă' + cancel: 'Anulează' delay: previous-command-cancelled: 'Comanda anterioară a fost anulată' stand-still: 'Nu vă mișcați! Se teleportează în [seconds] secunde' diff --git a/src/main/resources/locales/ru.yml b/src/main/resources/locales/ru.yml index 31178f1d7..984c26559 100644 --- a/src/main/resources/locales/ru.yml +++ b/src/main/resources/locales/ru.yml @@ -684,6 +684,11 @@ commands: confirm: Подтвердите, повторив команду в течение [seconds] секунд. previous-request-cancelled: Предыдущий запрос на подтверждение был отменён. request-cancelled: Время подтверждения истекло - запрос отменён. + title: 'Вы уверены?' + dialog-body: 'Подтвердите, чтобы продолжить, или отмените.' + buttons: + confirm: 'Подтвердить' + cancel: 'Отмена' delay: previous-command-cancelled: Предыдущая команда была отменена stand-still: Не двигайтесь! Телепортация через [seconds] секунды diff --git a/src/main/resources/locales/tr.yml b/src/main/resources/locales/tr.yml index adb9ff7c0..342f28b39 100644 --- a/src/main/resources/locales/tr.yml +++ b/src/main/resources/locales/tr.yml @@ -679,6 +679,11 @@ commands: confirm: 'Onaylamak icin [seconds] saniye icinde tekrar yaz!' previous-request-cancelled: 'Istek iptal edildi!' request-cancelled: 'Istek zaman aşımına ugradı ve iptal edildi!' + title: 'Emin misin?' + dialog-body: 'Devam etmek için onayla, iptal etmek için vazgeç.' + buttons: + confirm: 'Onayla' + cancel: 'İptal' delay: previous-command-cancelled: 'Önceki komut iptal edildi.' stand-still: 'Sakın hareket etme. [seconds] saniye içerisinde ışınlanacaksın.' diff --git a/src/main/resources/locales/uk.yml b/src/main/resources/locales/uk.yml index 7218f2ac1..15a7c8068 100644 --- a/src/main/resources/locales/uk.yml +++ b/src/main/resources/locales/uk.yml @@ -625,6 +625,11 @@ commands: confirm: 'Повторіть команду протягом [seconds]сдля підтвердження.' previous-request-cancelled: 'Попередній запит підтвердження скасовано.' request-cancelled: 'Час підтвердження вийшов — запит скасовано.' + title: 'Ви впевнені?' + dialog-body: 'Підтвердьте, щоб продовжити, або скасуйте.' + buttons: + confirm: 'Підтвердити' + cancel: 'Скасувати' delay: previous-command-cancelled: 'Попередню команду скасовано' stand-still: 'Не рухайтеся! Телепорт через [seconds] секунд' diff --git a/src/main/resources/locales/vi.yml b/src/main/resources/locales/vi.yml index bd7e9211c..38282dae3 100644 --- a/src/main/resources/locales/vi.yml +++ b/src/main/resources/locales/vi.yml @@ -685,6 +685,11 @@ commands: confirm: 'Nhập lệnh đó lại trong [seconds] giây để xác nhận.' previous-request-cancelled: 'Lần xác nhận trước đã bị hủy.' request-cancelled: 'Hết thời gian xác nhận - yêu cầu đã bị hủy.' + title: 'Bạn có chắc không?' + dialog-body: 'Xác nhận để tiếp tục, hoặc hủy để dừng lại.' + buttons: + confirm: 'Xác nhận' + cancel: 'Hủy' delay: previous-command-cancelled: 'Lệnh trước đã bị hủy' stand-still: 'Đừng di chuyển! Dịch chuyển trong [seconds] giây' diff --git a/src/main/resources/locales/zh-CN.yml b/src/main/resources/locales/zh-CN.yml index cfc7a04d2..cd97beeb3 100644 --- a/src/main/resources/locales/zh-CN.yml +++ b/src/main/resources/locales/zh-CN.yml @@ -637,6 +637,11 @@ commands: confirm: '请在[seconds]秒内再次输入命令以确认.' previous-request-cancelled: '先前的确认请求已取消.' request-cancelled: '确认超时 - 请求已取消.' + title: '确定吗?' + dialog-body: '确认以继续,或取消以中止。' + buttons: + confirm: '确认' + cancel: '取消' delay: previous-command-cancelled: '上一个命令已取消.' stand-still: '请勿移动! 传送将在[seconds]秒后开始.' diff --git a/src/main/resources/locales/zh-HK.yml b/src/main/resources/locales/zh-HK.yml index 601cc0b8f..8896bdf7e 100644 --- a/src/main/resources/locales/zh-HK.yml +++ b/src/main/resources/locales/zh-HK.yml @@ -635,6 +635,11 @@ commands: confirm: '[seconds] 秒內再次輸入命令來確認' previous-request-cancelled: '上一個確認請求已取消' request-cancelled: '確認超時 - 請求已取消' + title: '確定嗎?' + dialog-body: '確認以繼續,或取消以中止。' + buttons: + confirm: '確認' + cancel: '取消' delay: previous-command-cancelled: '上一個命令已取消。' stand-still: '請不要移動! 將在 [seconds] 秒後傳送。' diff --git a/src/test/java/world/bentobox/bentobox/api/commands/island/IslandResetCommandTest.java b/src/test/java/world/bentobox/bentobox/api/commands/island/IslandResetCommandTest.java index 6481288c1..6fc202500 100644 --- a/src/test/java/world/bentobox/bentobox/api/commands/island/IslandResetCommandTest.java +++ b/src/test/java/world/bentobox/bentobox/api/commands/island/IslandResetCommandTest.java @@ -349,6 +349,32 @@ void testConfirmationRequired() throws Exception { // Some more checking can go here... } + /** + * When dialog confirmations are enabled but the dialog cannot be shown (no live + * dialog registry, as under test), the command must fall back to the classic + * type-again prompt rather than silently losing the confirmation. + * Test method for {@link IslandResetCommand#execute(User, String, java.util.List)} + */ + @Test + void testConfirmationDialogFallsBackWhenUnavailable() throws Exception { + when(im.hasIsland(any(), eq(uuid))).thenReturn(true); + when(im.inTeam(any(), eq(uuid))).thenReturn(false); + when(pm.getResetsLeft(world, uuid)).thenReturn(1); + when(im.getIsland(any(), eq(uuid))).thenReturn(mock(Island.class)); + + // Require confirmation, and prefer a dialog for an online player + when(s.isResetConfirmation()).thenReturn(true); + when(s.getConfirmationTime()).thenReturn(20); + when(s.isUseDialogConfirmation()).thenReturn(true); + when(user.isPlayer()).thenReturn(true); + + assertTrue(irc.execute(user, irc.getLabel(), Collections.emptyList())); + // The dialog could not be built under test, so we fell back to the prompt + verify(user).sendMessage("commands.confirmation.confirm", "[seconds]", String.valueOf(s.getConfirmationTime())); + // and the failure was logged + verify(plugin).logError(Mockito.contains("confirmation dialog")); + } + /** * Test method for {@link IslandResetCommand#execute(User, String, java.util.List)} */ diff --git a/src/test/java/world/bentobox/bentobox/api/dialogs/DialogBuilderTest.java b/src/test/java/world/bentobox/bentobox/api/dialogs/DialogBuilderTest.java new file mode 100644 index 000000000..712dfab09 --- /dev/null +++ b/src/test/java/world/bentobox/bentobox/api/dialogs/DialogBuilderTest.java @@ -0,0 +1,91 @@ +package world.bentobox.bentobox.api.dialogs; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import net.kyori.adventure.text.Component; +import world.bentobox.bentobox.CommonTestSetup; +import world.bentobox.bentobox.api.user.User; + +/** + * Tests the Dialogs API builder (#3021). + *

+ * Note: {@link DialogBuilder#build()} of a complete dialog needs the server's + * dialog registry provider (Paper {@code DialogInstancesProvider}), which + * MockBukkit does not supply - much like NMS paste handling. Those paths are + * therefore exercised on a live server, not here. This test covers the pure + * logic: support detection, button holders and pre-factory validation. + * + * @author tastybento + */ +class DialogBuilderTest extends CommonTestSetup { + + private User user; + + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + user = User.getInstance(mockPlayer); + } + + @Override + @AfterEach + public void tearDown() throws Exception { + super.tearDown(); + } + + /** + * Test method for {@link Dialogs#isSupported()}. The Paper dialog classes are on + * the compile/test classpath, so support must be reported. + */ + @Test + void testDialogsSupported() { + assertTrue(Dialogs.isSupported()); + } + + /** + * Test method for {@link DialogButton}. + */ + @Test + void testDialogButtonHoldsFields() { + AtomicReference clicked = new AtomicReference<>(); + DialogButton b = new DialogButton(Component.text("Go"), Component.text("tip"), clicked::set); + assertNotNull(b.label()); + assertNotNull(b.tooltip()); + assertNotNull(b.onClick()); + b.onClick().accept(user); + // The callback ran with our user + assertSame(user, clicked.get()); + } + + /** + * Test method for {@link DialogButton} with no tooltip. + */ + @Test + void testDialogButtonNoTooltip() { + DialogButton b = new DialogButton(Component.text("Go"), null); + assertNotNull(b.label()); + assertTrue(b.tooltip() == null); + assertTrue(b.onClick() == null); + } + + /** + * Test method for {@link DialogBuilder#build()} - a dialog with neither a + * confirmation nor any button is invalid, and this is rejected before any + * server-side factory is touched. + */ + @Test + void testBuildNoButtonsThrows() { + DialogBuilder builder = new DialogBuilder().title(Component.text("Empty")); + assertThrows(IllegalStateException.class, builder::build); + } +} From 6ee30415cc4028839b06f84c5f57800106dda6bf Mon Sep 17 00:00:00 2001 From: tastybento Date: Sun, 12 Jul 2026 14:46:13 -0700 Subject: [PATCH 2/2] Wire dialogs into go picker, team invites and first-join (#3021) Adds the remaining three Dialogs API consumers and consolidates every dialog toggle under one island.dialogs.* config section (the confirmation toggle moves from island.confirmation.use-dialog to island.dialogs.confirmations; still pre-release, so no compatibility break). - go picker: a bare /island go with more than one island/home now opens a button-per-destination dialog; each button teleports there. Gated by island.dialogs.go-picker (default true). - team invites: receiving an invite auto-opens an [Accept]/[Decline] dialog that runs the accept/reject command, instead of relying on the player to type it. Gated by island.dialogs.team-invites (default true). - first join: brand new players can be shown a non-dismissable "choose your game" dialog when several game modes are installed; each button runs that game mode's command. Gated by island.dialogs.game-mode-selection (default FALSE - it is intrusive and non-dismissable). Every consumer degrades gracefully: if the dialog cannot be built/shown it logs and falls back to the previous behaviour (teleport / chat invite / per-world first-login), so nothing is lost on older servers or under errors. New en-US keys (commands.island.go.picker.title, the commands.island.team.invite.dialog.* group, and general.dialogs.game-mode-selection.title) translated into all 22 other locales. Fallback/gating paths covered by tests in IslandGoCommandTest, IslandTeamInviteCommandTest and JoinLeaveListenerTest. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01G7UmQDCP5MGHEZqFiw44zH --- .../world/bentobox/bentobox/Settings.java | 86 +++++++++++++++--- .../api/commands/ConfirmableCommand.java | 4 +- .../api/commands/island/IslandGoCommand.java | 89 ++++++++++++++----- .../island/team/IslandTeamInviteCommand.java | 41 ++++++++- .../bentobox/listeners/JoinLeaveListener.java | 46 ++++++++++ src/main/resources/config.yml | 23 +++-- src/main/resources/locales/cs.yml | 10 +++ src/main/resources/locales/de.yml | 10 +++ src/main/resources/locales/en-US.yml | 10 +++ src/main/resources/locales/es.yml | 10 +++ src/main/resources/locales/fr.yml | 10 +++ src/main/resources/locales/hr.yml | 10 +++ src/main/resources/locales/hu.yml | 10 +++ src/main/resources/locales/id.yml | 10 +++ src/main/resources/locales/it.yml | 10 +++ src/main/resources/locales/ja.yml | 10 +++ src/main/resources/locales/ko.yml | 10 +++ src/main/resources/locales/lv.yml | 10 +++ src/main/resources/locales/nl.yml | 10 +++ src/main/resources/locales/pl.yml | 10 +++ src/main/resources/locales/pt-BR.yml | 10 +++ src/main/resources/locales/pt.yml | 10 +++ src/main/resources/locales/ro.yml | 10 +++ src/main/resources/locales/ru.yml | 10 +++ src/main/resources/locales/tr.yml | 10 +++ src/main/resources/locales/uk.yml | 10 +++ src/main/resources/locales/vi.yml | 10 +++ src/main/resources/locales/zh-CN.yml | 10 +++ src/main/resources/locales/zh-HK.yml | 10 +++ .../commands/island/IslandGoCommandTest.java | 30 +++++++ .../island/IslandResetCommandTest.java | 2 +- .../team/IslandTeamInviteCommandTest.java | 21 +++++ .../listeners/JoinLeaveListenerTest.java | 28 ++++++ 33 files changed, 560 insertions(+), 40 deletions(-) diff --git a/src/main/java/world/bentobox/bentobox/Settings.java b/src/main/java/world/bentobox/bentobox/Settings.java index 658039b1a..3f1c857f1 100644 --- a/src/main/java/world/bentobox/bentobox/Settings.java +++ b/src/main/java/world/bentobox/bentobox/Settings.java @@ -344,12 +344,30 @@ public class Settings implements ConfigObject { @ConfigEntry(path = "island.confirmation.invites", since = "1.8.0") private boolean inviteConfirmation = false; + @ConfigComment("Modal dialog options. Dialogs are the 'menu they can't click away' shown to") + @ConfigComment("players for high-friction flows. They require a server that supports dialogs") + @ConfigComment("(Minecraft 26+); on older servers every option below falls back automatically") + @ConfigComment("to the classic chat/command behaviour regardless of the setting.") @ConfigComment("Show sensitive-command confirmations as a modal [Confirm]/[Cancel] dialog") - @ConfigComment("instead of asking the player to type the command again. Requires a server") - @ConfigComment("that supports dialogs (Minecraft 26+); older servers always use the type-again") - @ConfigComment("prompt regardless of this setting.") - @ConfigEntry(path = "island.confirmation.use-dialog", since = "3.21.0") - private boolean useDialogConfirmation = true; + @ConfigComment("instead of asking the player to type the command again.") + @ConfigEntry(path = "island.dialogs.confirmations", since = "3.21.0") + private boolean dialogConfirmations = true; + + @ConfigComment("Auto-open an [Accept]/[Decline] dialog when a player receives a team invite,") + @ConfigComment("instead of relying on them to type the accept/reject command.") + @ConfigEntry(path = "island.dialogs.team-invites", since = "3.21.0") + private boolean dialogTeamInvites = true; + + @ConfigComment("Show a button-per-destination picker when a player runs the island go/home") + @ConfigComment("command with no name and has more than one island or home to choose from.") + @ConfigEntry(path = "island.dialogs.go-picker", since = "3.21.0") + private boolean dialogGoPicker = true; + + @ConfigComment("Show a non-dismissable 'choose your game' dialog to brand new players when") + @ConfigComment("more than one game mode is installed. Off by default: it is intrusive and") + @ConfigComment("clicking a game runs that game mode's command for the player.") + @ConfigEntry(path = "island.dialogs.game-mode-selection", since = "3.21.0") + private boolean dialogGameModeSelection = false; @ConfigComment("Sets the minimum length an island custom name is required to have.") @ConfigEntry(path = "island.name.min-length") @@ -933,16 +951,64 @@ public void setInviteConfirmation(boolean inviteConfirmation) { * @return whether sensitive-command confirmations should use a modal dialog * @since 3.21.0 */ - public boolean isUseDialogConfirmation() { - return useDialogConfirmation; + public boolean isDialogConfirmations() { + return dialogConfirmations; } /** - * @param useDialogConfirmation whether to use a modal dialog for confirmations + * @param dialogConfirmations whether to use a modal dialog for confirmations * @since 3.21.0 */ - public void setUseDialogConfirmation(boolean useDialogConfirmation) { - this.useDialogConfirmation = useDialogConfirmation; + public void setDialogConfirmations(boolean dialogConfirmations) { + this.dialogConfirmations = dialogConfirmations; + } + + /** + * @return whether team invites should auto-open an accept/decline dialog + * @since 3.21.0 + */ + public boolean isDialogTeamInvites() { + return dialogTeamInvites; + } + + /** + * @param dialogTeamInvites whether team invites should open a dialog + * @since 3.21.0 + */ + public void setDialogTeamInvites(boolean dialogTeamInvites) { + this.dialogTeamInvites = dialogTeamInvites; + } + + /** + * @return whether the island go/home command shows a destination picker dialog + * @since 3.21.0 + */ + public boolean isDialogGoPicker() { + return dialogGoPicker; + } + + /** + * @param dialogGoPicker whether the go/home command shows a picker dialog + * @since 3.21.0 + */ + public void setDialogGoPicker(boolean dialogGoPicker) { + this.dialogGoPicker = dialogGoPicker; + } + + /** + * @return whether brand new players see a game-mode selection dialog + * @since 3.21.0 + */ + public boolean isDialogGameModeSelection() { + return dialogGameModeSelection; + } + + /** + * @param dialogGameModeSelection whether new players see a game-mode dialog + * @since 3.21.0 + */ + public void setDialogGameModeSelection(boolean dialogGameModeSelection) { + this.dialogGameModeSelection = dialogGameModeSelection; } /** diff --git a/src/main/java/world/bentobox/bentobox/api/commands/ConfirmableCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/ConfirmableCommand.java index 03d096442..25bfd24bf 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/ConfirmableCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/ConfirmableCommand.java @@ -148,14 +148,14 @@ public void askConfirmation(User user, Runnable confirmed) { /** * Whether this confirmation should be presented as a modal dialog rather than * the type-the-command-again prompt. True only for online players, when the - * server supports dialogs and the {@code island.confirmation.use-dialog} + * server supports dialogs and the {@code island.dialogs.confirmations} * setting is enabled. * * @param user the user being asked * @return true if a dialog should be shown */ private boolean useDialog(User user) { - return user.isPlayer() && Dialogs.isSupported() && getPlugin().getSettings().isUseDialogConfirmation(); + return user.isPlayer() && Dialogs.isSupported() && getPlugin().getSettings().isDialogConfirmations(); } /** diff --git a/src/main/java/world/bentobox/bentobox/api/commands/island/IslandGoCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/island/IslandGoCommand.java index e9be9258b..1d3b4955e 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/island/IslandGoCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/island/IslandGoCommand.java @@ -11,9 +11,13 @@ import org.bukkit.World; +import net.kyori.adventure.text.Component; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.commands.DelayedTeleportCommand; +import world.bentobox.bentobox.api.dialogs.DialogBuilder; +import world.bentobox.bentobox.api.dialogs.DialogButton; +import world.bentobox.bentobox.api.dialogs.Dialogs; import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; @@ -122,33 +126,78 @@ public boolean execute(User user, String label, List args) { + "[hover: " + user.getTranslation("commands.island.sethome.click-to-teleport") + "]")); return false; } else { - // We know where this location is. Now work out if it's an island name or a home name - IslandInfo info = names.get(name); - getIslands().setPrimaryIsland(user.getUniqueId(), info.island); - if (!info.islandName) { - // This is a home name, not an island name - this.delayCommand(user, () -> getIslands().homeTeleportAsync(getWorld(), user.getPlayer(), name) // Teleport to the named home for this player - .thenAccept(r -> { - if (Boolean.TRUE.equals(r)) { - // Success - getIslands().setPrimaryIsland(user.getUniqueId(), info.island); - } else { - user.sendMessage("commands.island.go.failure"); - getPlugin().logError(user.getName() + " could not teleport to their island - async teleport issue"); - } - })); - return true; - } - // Not a home name, an island name, so teleport to the island - this.delayCommand(user, () -> getIslands().homeTeleportAsync(info.island(), user)); + // We know where this location is. Teleport there. + teleportToNamed(user, name, names.get(name)); return true; } } - + + // No name given. If the player has several destinations, offer a picker dialog + if (showGoPicker(user, names)) { + return true; + } + this.delayCommand(user, () -> getIslands().homeTeleportAsync(getWorld(), user.getPlayer())); return true; } + /** + * Teleports the user to a resolved destination, whether it is a named home or an + * island name. + * + * @param user the user to teleport + * @param name the resolved (canonical) destination name + * @param info the island/home the name refers to + */ + private void teleportToNamed(User user, String name, IslandInfo info) { + getIslands().setPrimaryIsland(user.getUniqueId(), info.island()); + if (!info.islandName()) { + // This is a home name, not an island name + this.delayCommand(user, () -> getIslands().homeTeleportAsync(getWorld(), user.getPlayer(), name) + .thenAccept(r -> { + if (Boolean.TRUE.equals(r)) { + getIslands().setPrimaryIsland(user.getUniqueId(), info.island()); + } else { + user.sendMessage("commands.island.go.failure"); + getPlugin().logError( + user.getName() + " could not teleport to their island - async teleport issue"); + } + })); + } else { + // An island name, so teleport to the island + this.delayCommand(user, () -> getIslands().homeTeleportAsync(info.island(), user)); + } + } + + /** + * Shows a button-per-destination picker dialog when the player has more than one + * island or home. Each button teleports to that destination. + * + * @param user the user + * @param names the destination map + * @return true if the picker was shown; false to fall through to the default teleport + */ + private boolean showGoPicker(User user, Map names) { + if (!user.isPlayer() || names.size() < 2 || !Dialogs.isSupported() + || !getPlugin().getSettings().isDialogGoPicker()) { + return false; + } + try { + DialogBuilder builder = new DialogBuilder().title(user, "commands.island.go.picker.title"); + // Stable, alphabetical button order + names.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(en -> { + String name = en.getKey(); + IslandInfo info = en.getValue(); + builder.button(new DialogButton(Component.text(name), u -> teleportToNamed(u, name, info))); + }); + builder.build().show(user); + return true; + } catch (Exception e) { + getPlugin().logError("Could not show go picker dialog, falling back to teleport: " + e.getMessage()); + return false; + } + } + /** * Checks if any of the user's islands are reserved. * If a reserved island is found, redirects the user to island creation. diff --git a/src/main/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamInviteCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamInviteCommand.java index 2b08383a3..aff7e6d1a 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamInviteCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamInviteCommand.java @@ -10,6 +10,9 @@ import org.eclipse.jdt.annotation.Nullable; import world.bentobox.bentobox.api.commands.CompositeCommand; +import world.bentobox.bentobox.api.dialogs.DialogBuilder; +import world.bentobox.bentobox.api.dialogs.DialogButton; +import world.bentobox.bentobox.api.dialogs.Dialogs; import world.bentobox.bentobox.api.events.IslandBaseEvent; import world.bentobox.bentobox.api.events.team.TeamEvent; import world.bentobox.bentobox.api.localization.TextVariables; @@ -230,14 +233,48 @@ public boolean execute(User user, String label, List args) { // Send message to online player invitedPlayer.sendMessage("commands.island.team.invite.name-has-invited-you", TextVariables.NAME, user.getName(), TextVariables.DISPLAY_NAME, user.getDisplayName()); invitedPlayer.sendMessage("commands.island.team.invite.to-accept-or-reject", TextVariables.LABEL, getTopLabel()); - if (getIWM().getWorldSettings(getWorld()).isDisallowTeamMemberIslands() - && getIslands().hasIsland(getWorld(), invitedPlayer.getUniqueId())) { + boolean willLoseIsland = getIWM().getWorldSettings(getWorld()).isDisallowTeamMemberIslands() + && getIslands().hasIsland(getWorld(), invitedPlayer.getUniqueId()); + if (willLoseIsland) { invitedPlayer.sendMessage("commands.island.team.invite.you-will-lose-your-island"); } + // Auto-open an accept/decline dialog for the invited player if enabled + showInviteDialog(user, invitedPlayer, willLoseIsland); invitedPlayer = null; return true; } + /** + * Opens an [Accept]/[Decline] dialog for the invited player, so they do not have + * to find and type the accept/reject command. Silently does nothing (leaving the + * chat messages as the fallback) if dialogs are disabled or unsupported. + * + * @param inviter the player who sent the invite + * @param invited the player who received it + * @param willLoseIsland whether accepting will cost the invited player their island + */ + private void showInviteDialog(User inviter, User invited, boolean willLoseIsland) { + if (!invited.isPlayer() || !Dialogs.isSupported() || !getSettings().isDialogTeamInvites()) { + return; + } + try { + DialogBuilder builder = new DialogBuilder() + .title(invited, "commands.island.team.invite.dialog.title", TextVariables.NAME, inviter.getName()); + builder.body(invited, "commands.island.team.invite.dialog.body", TextVariables.NAME, inviter.getName()); + if (willLoseIsland) { + builder.body(invited, "commands.island.team.invite.you-will-lose-your-island"); + } + builder.confirmation( + DialogButton.of(invited, "commands.island.team.invite.dialog.accept", + u -> itc.getSubCommand("accept").ifPresent(c -> c.call(u, c.getLabel(), List.of()))), + DialogButton.of(invited, "commands.island.team.invite.dialog.decline", + u -> itc.getSubCommand("reject").ifPresent(c -> c.call(u, c.getLabel(), List.of())))); + builder.build().show(invited); + } catch (Exception e) { + getPlugin().logError("Could not show team invite dialog: " + e.getMessage()); + } + } + /** * Provides tab completion for online player names. * Requires at least first letter to avoid showing all players. diff --git a/src/main/java/world/bentobox/bentobox/listeners/JoinLeaveListener.java b/src/main/java/world/bentobox/bentobox/listeners/JoinLeaveListener.java index d05786a60..ff7407d5f 100644 --- a/src/main/java/world/bentobox/bentobox/listeners/JoinLeaveListener.java +++ b/src/main/java/world/bentobox/bentobox/listeners/JoinLeaveListener.java @@ -1,6 +1,7 @@ package world.bentobox.bentobox.listeners; import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.Set; import java.util.UUID; @@ -16,8 +17,13 @@ import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; +import net.kyori.adventure.text.Component; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.addons.GameModeAddon; +import world.bentobox.bentobox.api.dialogs.BBDialog; +import world.bentobox.bentobox.api.dialogs.DialogBuilder; +import world.bentobox.bentobox.api.dialogs.DialogButton; +import world.bentobox.bentobox.api.dialogs.Dialogs; import world.bentobox.bentobox.api.events.island.IslandEvent; import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.user.User; @@ -141,6 +147,12 @@ private void firstTime(User user) { // don't exist players.getPlayer(user.getUniqueId()); + // If enabled and several game modes are installed, let the player pick one via a + // dialog instead of silently auto-creating islands per world. + if (showGameModeDialog(user)) { + return; + } + plugin.getIWM().getOverWorlds().stream().filter(w -> plugin.getIWM().isCreateIslandOnFirstLoginEnabled(w)) .forEach(w -> { // Even if that'd be extremely unlikely, it's better to check if the player @@ -171,6 +183,40 @@ private void firstTime(User user) { } } }); + } + + /** + * Shows a non-dismissable "choose your game" dialog to a brand new player when + * more than one game mode is installed and the option is enabled. Each button runs + * that game mode's player command. Shown a short moment after join so the client is + * ready. + * + * @param user the joining player + * @return true if the dialog was built and scheduled; false to fall through to the + * default per-world first-login behaviour + */ + private boolean showGameModeDialog(User user) { + if (!user.isPlayer() || !Dialogs.isSupported() || !plugin.getSettings().isDialogGameModeSelection()) { + return false; + } + List modes = plugin.getAddonsManager().getGameModeAddons().stream() + .filter(gm -> gm.getPlayerCommand().isPresent()).toList(); + if (modes.size() < 2) { + return false; + } + try { + DialogBuilder builder = new DialogBuilder().title(user, "general.dialogs.game-mode-selection.title") + .escapable(false); + modes.forEach(gm -> builder.button(new DialogButton(Component.text(gm.getDescription().getName()), + u -> gm.getPlayerCommand().ifPresent(c -> c.call(u, c.getLabel(), Collections.emptyList()))))); + // Build now so a build failure falls back; show a little after join so the client is ready + BBDialog dialog = builder.build(); + Bukkit.getScheduler().runTaskLater(plugin, () -> dialog.show(user), 20L); + return true; + } catch (Exception e) { + plugin.logError("Could not show game mode selection dialog: " + e.getMessage()); + return false; + } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index c6f3f294f..b3f1f02f1 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -221,12 +221,25 @@ island: # Team invites will always require confirmation, for safety concerns. # Added since 1.8.0. invites: false + # Modal dialog options. Dialogs are the 'menu they can't click away' shown to + # players for high-friction flows. They require a server that supports dialogs + # (Minecraft 26+); on older servers every option below falls back automatically + # to the classic chat/command behaviour regardless of the setting. + # Added since 3.21.0. + dialogs: # Show sensitive-command confirmations as a modal [Confirm]/[Cancel] dialog - # instead of asking the player to type the command again. Requires a server - # that supports dialogs (Minecraft 26+); older servers always use the type-again - # prompt regardless of this setting. - # Added since 3.21.0. - use-dialog: true + # instead of asking the player to type the command again. + confirmations: true + # Auto-open an [Accept]/[Decline] dialog when a player receives a team invite, + # instead of relying on them to type the accept/reject command. + team-invites: true + # Show a button-per-destination picker when a player runs the island go/home + # command with no name and has more than one island or home to choose from. + go-picker: true + # Show a non-dismissable 'choose your game' dialog to brand new players when + # more than one game mode is installed. Off by default: it is intrusive and + # clicking a game runs that game mode's command for the player. + game-mode-selection: false delay: # Time in seconds that players have to stand still before teleport commands activate, e.g. island go. time: 0 diff --git a/src/main/resources/locales/cs.yml b/src/main/resources/locales/cs.yml index a1dd4956e..47a083227 100644 --- a/src/main/resources/locales/cs.yml +++ b/src/main/resources/locales/cs.yml @@ -11,6 +11,9 @@ prefixes: an-island: ostrov islands: ostrovy general: + dialogs: + game-mode-selection: + title: 'Vyber si svou hru' did-you-mean: suggestion: 'Měli jste na mysli [command]? Klikněte sem nebo napište yes pro spuštění.[run_command: [command]][hover: Kliknutím spustíte [command]]' header: 'Měli jste na mysli něco z tohoto? Klikněte pro spuštění:' @@ -696,6 +699,8 @@ commands: Teleportování z nějakého důvodu selhalo. Zkuste to prosím znovu později. unknown-home: 'Neznámé domácí jméno!' + picker: + title: 'Kam chceš jít?' help: description: Hlavní příkaz ostrova spawn: @@ -893,6 +898,11 @@ commands: invite: description: pozvi hráče jako člena tvého ostrova invitation-sent: 'Pozvánka poslána hráči [name]' + dialog: + title: 'Pozvánka do týmu od [name]' + body: '[name] tě pozval, aby ses připojil k jeho ostrovu. Přijmout?' + accept: 'Přijmout' + decline: 'Odmítnout' removing-invite: 'Odstraňuji pozvánku' name-has-invited-you: '[name] tě pozval jako člena jeho ostrova.' to-accept-or-reject: >- diff --git a/src/main/resources/locales/de.yml b/src/main/resources/locales/de.yml index a5b62f97e..969b38907 100644 --- a/src/main/resources/locales/de.yml +++ b/src/main/resources/locales/de.yml @@ -12,6 +12,9 @@ prefixes: an-island: eine Insel islands: Inseln general: + dialogs: + game-mode-selection: + title: 'Wähle dein Spiel' did-you-mean: suggestion: 'Meintest du [command]? Klicke hier oder tippe yes, um ihn auszuführen.[run_command: [command]][hover: Klicken, um [command] auszuführen]' header: 'Meintest du einen dieser Befehle? Klicke zum Ausführen:' @@ -736,6 +739,8 @@ commands: Das Teleportieren ist aus irgendeinem Grund gescheitert. Bitte versuchen Sie es später erneut. unknown-home: 'Unbekannter Home-Name!' + picker: + title: 'Wohin möchtest du?' help: description: Der wichtigste Inselbefehl spawn: @@ -950,6 +955,11 @@ commands: invite: description: Einen Spieler auf deine Insel einladen invitation-sent: 'Einladung gesendet an [name]' + dialog: + title: 'Team-Einladung von [name]' + body: '[name] hat dich eingeladen, seiner Insel beizutreten. Annehmen?' + accept: 'Annehmen' + decline: 'Ablehnen' removing-invite: 'Einladung entfernen' name-has-invited-you: | [name] hat Sie eingeladen, diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 840eed70a..0b73803d0 100644 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -22,6 +22,9 @@ general: success: 'Success!' invalid: Invalid beta: 'This command is in beta. Make sure you have backups!' + dialogs: + game-mode-selection: + title: 'Choose your game' errors: command-cancelled: 'Command cancelled.' no-permission: 'You don''t have the permission to execute this command ( @@ -666,6 +669,8 @@ commands: teleported: 'Teleported you to home [number].' failure: 'Teleporting failed for some reason. Please try again later.' unknown-home: 'Unknown home name!' + picker: + title: 'Where would you like to go?' help: description: the main [prefix_island] command spawn: @@ -877,6 +882,11 @@ commands: you-will-lose-your-island: | WARNING! You will lose all your [prefix_islands] if you accept! + dialog: + title: 'Team invite from [name]' + body: '[name] has invited you to join their island. Accept?' + accept: 'Accept' + decline: 'Decline' gui: titles: team-invite-panel: Invite Players diff --git a/src/main/resources/locales/es.yml b/src/main/resources/locales/es.yml index 7f393f66b..b07b6d61c 100644 --- a/src/main/resources/locales/es.yml +++ b/src/main/resources/locales/es.yml @@ -12,6 +12,9 @@ prefixes: an-island: una isla islands: islas general: + dialogs: + game-mode-selection: + title: 'Elige tu juego' did-you-mean: suggestion: '¿Quisiste decir [command]? Haz clic aquí o escribe yes para ejecutarlo.[run_command: [command]][hover: Haz clic para ejecutar [command]]' header: '¿Quisiste decir uno de estos? Haz clic en uno para ejecutarlo:' @@ -725,6 +728,8 @@ commands: La teletransportación falló por alguna razón. Por favor, intenta de nuevo más tarde. unknown-home: '¡Nombre de hogar desconocido!' + picker: + title: '¿A dónde quieres ir?' help: description: El comando principal de la isla. spawn: @@ -929,6 +934,11 @@ commands: invite: description: invita a un jugador a unirse a tu isla invitation-sent: 'Invitación enviada a [name]' + dialog: + title: 'Invitación de equipo de [name]' + body: '[name] te ha invitado a unirte a su isla. ¿Aceptar?' + accept: 'Aceptar' + decline: 'Rechazar' removing-invite: 'Eliminando invitación' name-has-invited-you: '[name] Te ha invitado a unirte a su isla.' to-accept-or-reject: >- diff --git a/src/main/resources/locales/fr.yml b/src/main/resources/locales/fr.yml index 20d91f0f9..99d35b3b0 100644 --- a/src/main/resources/locales/fr.yml +++ b/src/main/resources/locales/fr.yml @@ -11,6 +11,9 @@ prefixes: an-island: une île islands: îles general: + dialogs: + game-mode-selection: + title: 'Choisis ton jeu' did-you-mean: suggestion: 'Vouliez-vous dire [command] ? Cliquez ici ou tapez yes pour l’exécuter.[run_command: [command]][hover: Cliquez pour exécuter [command]]' header: 'Vouliez-vous dire l’un de ceux-ci ? Cliquez pour exécuter :' @@ -748,6 +751,8 @@ commands: La téléportation a échoué pour une raison quelconque. Veuillez réessayer plus tard. unknown-home: 'Nom de maison inconnu!' + picker: + title: 'Où veux-tu aller ?' help: description: commande principale pour l'île spawn: @@ -950,6 +955,11 @@ commands: invite: description: inviter un joueur à rejoindre votre île invitation-sent: 'Invitation envoyée à [name].' + dialog: + title: 'Invitation d''équipe de [name]' + body: '[name] t''a invité à rejoindre son île. Accepter ?' + accept: 'Accepter' + decline: 'Refuser' removing-invite: 'Suppression de l''invitation.' name-has-invited-you: '[name] vous a invité à rejoindre son île.' to-accept-or-reject: >- diff --git a/src/main/resources/locales/hr.yml b/src/main/resources/locales/hr.yml index aa8cdef04..fcd85f863 100644 --- a/src/main/resources/locales/hr.yml +++ b/src/main/resources/locales/hr.yml @@ -11,6 +11,9 @@ prefixes: an-island: otok islands: ostrva general: + dialogs: + game-mode-selection: + title: 'Odaberi svoju igru' did-you-mean: suggestion: 'Jeste li mislili [command]? Kliknite ovdje ili upišite yes za pokretanje.[run_command: [command]][hover: Kliknite za pokretanje [command]]' header: 'Jeste li mislili nešto od ovoga? Kliknite za pokretanje:' @@ -707,6 +710,8 @@ commands: Teleportacija je neuspjela iz nekog razloga. Molimo pokušajte ponovno kasnije. unknown-home: 'Nepoznato kućno ime!' + picker: + title: 'Kamo želiš ići?' help: description: glavna otočka komanda spawn: @@ -907,6 +912,11 @@ commands: invite: description: pozovite igrača da se pridruži vašem otoku invitation-sent: 'Pozivnica poslana na [name].' + dialog: + title: 'Pozivnica tima od [name]' + body: '[name] te pozvao da se pridružiš njegovom otoku. Prihvatiti?' + accept: 'Prihvati' + decline: 'Odbij' removing-invite: 'Uklanjanje pozivnice.' name-has-invited-you: '[name] vas je pozvao da se pridružite njihovom otoku.' to-accept-or-reject: >- diff --git a/src/main/resources/locales/hu.yml b/src/main/resources/locales/hu.yml index 9ee305e37..5f689880f 100644 --- a/src/main/resources/locales/hu.yml +++ b/src/main/resources/locales/hu.yml @@ -11,6 +11,9 @@ prefixes: an-island: egy sziget islands: szigetek general: + dialogs: + game-mode-selection: + title: 'Válaszd ki a játékod' did-you-mean: suggestion: 'Erre gondoltál: [command]? Kattints ide, vagy írd be: yes a futtatáshoz.[run_command: [command]][hover: Kattints a futtatáshoz: [command]]' header: 'Ezek egyikére gondoltál? Kattints egyre a futtatáshoz:' @@ -742,6 +745,8 @@ commands: teleported: 'hazateleportált [number].' failure: 'A teleportálás valamiért sikertelen. Kérlek, próbáld újra később.' unknown-home: 'Ismeretlen otthonnév!' + picker: + title: 'Hová szeretnél menni?' help: description: a főszigeti parancsnokság spawn: @@ -951,6 +956,11 @@ commands: invite: description: hívj meg egy játékost a szigetedre invitation-sent: 'Meghívó elküldve a [name]címre.' + dialog: + title: 'Csapatmeghívó [name] játékostól' + body: '[name] meghívott, hogy csatlakozz a szigetéhez. Elfogadod?' + accept: 'Elfogadás' + decline: 'Elutasítás' removing-invite: 'Meghívó eltávolítása.' name-has-invited-you: '[name] meghívott téged, hogy csatlakozz a szigetéhez.' to-accept-or-reject: >- diff --git a/src/main/resources/locales/id.yml b/src/main/resources/locales/id.yml index 4f04960c2..61d5d5bf9 100644 --- a/src/main/resources/locales/id.yml +++ b/src/main/resources/locales/id.yml @@ -11,6 +11,9 @@ prefixes: an-island: sebuah pulau islands: pulau-pulau general: + dialogs: + game-mode-selection: + title: 'Pilih permainanmu' did-you-mean: suggestion: 'Apakah maksud Anda [command]? Klik di sini atau ketik yes untuk menjalankannya.[run_command: [command]][hover: Klik untuk menjalankan [command]]' header: 'Apakah maksud Anda salah satu dari ini? Klik salah satu untuk menjalankan:' @@ -716,6 +719,8 @@ commands: teleported: 'Teleportasi Anda ke rumah [number].' failure: 'Teleportasi gagal karena suatu alasan. Silakan coba lagi nanti.' unknown-home: 'Nama rumah tidak diketahui!' + picker: + title: 'Mau pergi ke mana?' help: description: komando pulau utama spawn: @@ -920,6 +925,11 @@ commands: invite: description: undang pemain untuk bergabung dengan pulau Anda invitation-sent: 'Undangan dikirim ke [name].' + dialog: + title: 'Undangan tim dari [name]' + body: '[name] mengundangmu untuk bergabung ke pulaunya. Terima?' + accept: 'Terima' + decline: 'Tolak' removing-invite: 'Menghapus undangan.' name-has-invited-you: '[name] telah mengundang Anda untuk bergabung dengan pulau mereka.' to-accept-or-reject: >- diff --git a/src/main/resources/locales/it.yml b/src/main/resources/locales/it.yml index 1b2d463cb..3ba224b3e 100644 --- a/src/main/resources/locales/it.yml +++ b/src/main/resources/locales/it.yml @@ -11,6 +11,9 @@ prefixes: an-island: un'isola islands: isole general: + dialogs: + game-mode-selection: + title: 'Scegli il tuo gioco' did-you-mean: suggestion: 'Intendevi [command]? Clicca qui o scrivi yes per eseguirlo.[run_command: [command]][hover: Clicca per eseguire [command]]' header: 'Intendevi uno di questi? Cliccane uno per eseguirlo:' @@ -733,6 +736,8 @@ commands: Teletrasporto non riuscito per qualche motivo. Per favore riprova più tardi. unknown-home: 'Nome di casa sconosciuto!' + picker: + title: 'Dove vuoi andare?' help: description: Comando principale dell'isola spawn: @@ -941,6 +946,11 @@ commands: invite: description: invita un giocatore ad essere membro della tua isola invitation-sent: 'Invito inviato a [name]' + dialog: + title: 'Invito al team da [name]' + body: '[name] ti ha invitato a unirti alla sua isola. Accettare?' + accept: 'Accetta' + decline: 'Rifiuta' removing-invite: 'Rimuovendo l''invito' name-has-invited-you: '[name] ti ha invitato ad essere membro della sua isola.' to-accept-or-reject: >- diff --git a/src/main/resources/locales/ja.yml b/src/main/resources/locales/ja.yml index ccb8c90e5..dbb0ca3ad 100644 --- a/src/main/resources/locales/ja.yml +++ b/src/main/resources/locales/ja.yml @@ -11,6 +11,9 @@ prefixes: an-island: 島 islands: 島 general: + dialogs: + game-mode-selection: + title: 'ゲームを選んでください' did-you-mean: suggestion: 'もしかして [command]?ここをクリックするか、yes と入力すると実行されます。[run_command: [command]][hover: クリックで [command] を実行]' header: 'もしかして次のいずれかですか?クリックすると実行されます:' @@ -639,6 +642,8 @@ commands: teleported: 'テレポート #[number]をホームにします。' failure: '何らかの理由でテレポートが失敗しました。後でもう一度やり直してください。' unknown-home: '不明な家の名前!' + picker: + title: 'どこへ行きますか?' help: description: 本島のコマンド spawn: @@ -829,6 +834,11 @@ commands: invite: description: 島に参加するプレーヤーを招待 invitation-sent: 招待状を [name] に送信 + dialog: + title: '[name] からのチーム招待' + body: '[name] があなたを島に招待しました。承認しますか?' + accept: '承認' + decline: '拒否' removing-invite: 招待の削除 name-has-invited-you: '[name]は、彼らの島に参加することを招待しました。' to-accept-or-reject: /[label] team accept を受け入れるか、/[label] team reject を拒否すると言う diff --git a/src/main/resources/locales/ko.yml b/src/main/resources/locales/ko.yml index 6a92b00bb..8b1a669c1 100644 --- a/src/main/resources/locales/ko.yml +++ b/src/main/resources/locales/ko.yml @@ -12,6 +12,9 @@ prefixes: an-island: 섬 islands: 섬들 general: + dialogs: + game-mode-selection: + title: '게임을 선택하세요' did-you-mean: suggestion: '혹시 [command] 명령어를 입력하려고 하셨나요? 여기를 클릭하거나 yes를 입력하면 실행됩니다.[run_command: [command]][hover: 클릭하여 [command] 실행]' header: '다음 중 하나를 입력하려고 하셨나요? 클릭하면 실행됩니다:' @@ -659,6 +662,8 @@ commands: teleported: '# [번호] (으)로 이동했습니다.' failure: '전송이 어떤 이유로 실패했습니다. 나중에 다시 시도해 주세요.' unknown-home: '알 수 없는 홈 이름입니다!' + picker: + title: '어디로 갈까요?' help: description: 섬의 명령어보기 spawn: @@ -845,6 +850,11 @@ commands: invite: description: 플레이어를 섬원으로 초대합니다 invitation-sent: '[name]에게 초대를 보냈습니다.' + dialog: + title: '[name] 님의 팀 초대' + body: '[name] 님이 섬에 초대했습니다. 수락하시겠습니까?' + accept: '수락' + decline: '거절' removing-invite: '보낸 초대를 취소했습니다.' name-has-invited-you: '[name] 이(가) 당신을 섬원으로 초대하고 있습니다' to-accept-or-reject: >- diff --git a/src/main/resources/locales/lv.yml b/src/main/resources/locales/lv.yml index 99ce4ae77..2f749032b 100644 --- a/src/main/resources/locales/lv.yml +++ b/src/main/resources/locales/lv.yml @@ -11,6 +11,9 @@ prefixes: an-island: salā islands: saliņas general: + dialogs: + game-mode-selection: + title: 'Izvēlies savu spēli' did-you-mean: suggestion: 'Vai jūs domājāt [command]? Noklikšķiniet šeit vai ierakstiet yes, lai to palaistu.[run_command: [command]][hover: Noklikšķiniet, lai palaistu [command]]' header: 'Vai jūs domājāt kādu no šiem? Noklikšķiniet, lai palaistu:' @@ -711,6 +714,8 @@ commands: Teleportācija neizdevās kāda iemesla dēļ. Lūdzu, mēģiniet vēlreiz vēlāk. unknown-home: 'Nezināms mājas nosaukums!' + picker: + title: 'Kurp vēlies doties?' help: description: Galvenā salas komanda spawn: @@ -908,6 +913,11 @@ commands: invite: description: uzaicini spēlētāju pievienoties tavai salai invitation-sent: 'Ielūgums nosūtīts [name]' + dialog: + title: 'Komandas ielūgums no [name]' + body: '[name] uzaicināja tevi pievienoties savai salai. Pieņemt?' + accept: 'Pieņemt' + decline: 'Noraidīt' removing-invite: 'Ielūgumu atsauc' name-has-invited-you: '[name] ielūdza tevi pievienoties viņa komandai.' to-accept-or-reject: >- diff --git a/src/main/resources/locales/nl.yml b/src/main/resources/locales/nl.yml index e377ce9ec..cf0f4d231 100644 --- a/src/main/resources/locales/nl.yml +++ b/src/main/resources/locales/nl.yml @@ -11,6 +11,9 @@ prefixes: an-island: een eiland islands: eilanden general: + dialogs: + game-mode-selection: + title: 'Kies je spel' did-you-mean: suggestion: 'Bedoelde je [command]? Klik hier of typ yes om het uit te voeren.[run_command: [command]][hover: Klik om [command] uit te voeren]' header: 'Bedoelde je een van deze? Klik op één om uit te voeren:' @@ -743,6 +746,8 @@ commands: Teleporteren is om een of andere reden mislukt. Probeer het later opnieuw. unknown-home: 'Onbekende thuisnaam!' + picker: + title: 'Waar wil je heen?' help: description: het commando van het hoofdeiland spawn: @@ -951,6 +956,11 @@ commands: invite: description: nodig een speler uit om zich bij je eiland aan te sluiten invitation-sent: 'uitnodiging verzonden naar [name] .' + dialog: + title: 'Teamuitnodiging van [name]' + body: '[name] heeft je uitgenodigd om zich bij hun eiland aan te sluiten. Accepteren?' + accept: 'Accepteren' + decline: 'Weigeren' removing-invite: 'Uitnodiging verwijderen.' name-has-invited-you: '[name] heeft je uitgenodigd om lid te worden van hun eiland.' to-accept-or-reject: >- diff --git a/src/main/resources/locales/pl.yml b/src/main/resources/locales/pl.yml index 2c100aa34..80ba60575 100644 --- a/src/main/resources/locales/pl.yml +++ b/src/main/resources/locales/pl.yml @@ -11,6 +11,9 @@ prefixes: an-island: wyspa islands: wyspy general: + dialogs: + game-mode-selection: + title: 'Wybierz swoją grę' did-you-mean: suggestion: 'Czy chodziło ci o [command]? Kliknij tutaj lub wpisz yes, aby uruchomić.[run_command: [command]][hover: Kliknij, aby uruchomić [command]]' header: 'Czy chodziło ci o któreś z tych? Kliknij, aby uruchomić:' @@ -715,6 +718,8 @@ commands: Teleportacja nie powiodła się z jakiegoś powodu. Proszę spróbować ponownie później. unknown-home: 'Nieznana nazwa domu!' + picker: + title: 'Dokąd chcesz iść?' help: description: Główne komendy wyspy spawn: @@ -917,6 +922,11 @@ commands: invite: description: zaproś gracza, by dołączył do Twojej wyspy invitation-sent: Zaproszenie wysłane do [name] + dialog: + title: 'Zaproszenie do drużyny od [name]' + body: '[name] zaprosił cię do swojej wyspy. Zaakceptować?' + accept: 'Zaakceptuj' + decline: 'Odrzuć' removing-invite: Usuwanie zaproszenia name-has-invited-you: '[name] zaprosił cię do przyłączenia się do jego wyspy.' to-accept-or-reject: >- diff --git a/src/main/resources/locales/pt-BR.yml b/src/main/resources/locales/pt-BR.yml index 51a1ac99d..82c2453ea 100644 --- a/src/main/resources/locales/pt-BR.yml +++ b/src/main/resources/locales/pt-BR.yml @@ -10,6 +10,9 @@ prefixes: an-island: uma ilha islands: ilhas general: + dialogs: + game-mode-selection: + title: 'Escolha seu jogo' did-you-mean: suggestion: 'Você quis dizer [command]? Clique aqui ou digite yes para executá-lo.[run_command: [command]][hover: Clique para executar [command]]' header: 'Você quis dizer um destes? Clique em um para executar:' @@ -711,6 +714,8 @@ commands: A teletransporte falhou por algum motivo. Por favor, tente novamente mais tarde. unknown-home: 'Nome de lar desconhecido!' + picker: + title: 'Para onde você quer ir?' help: description: o comando principal de ilhas spawn: @@ -913,6 +918,11 @@ commands: invite: description: convidar um jogador a equipe de sua ilha invitation-sent: 'Convite enviado pra [name].' + dialog: + title: 'Convite de equipe de [name]' + body: '[name] convidou você para entrar na ilha dele. Aceitar?' + accept: 'Aceitar' + decline: 'Recusar' removing-invite: 'Removendo convite.' name-has-invited-you: '[name] te convidou a se juntar a ilha.' to-accept-or-reject: >- diff --git a/src/main/resources/locales/pt.yml b/src/main/resources/locales/pt.yml index 8f93cbbad..254dee4ea 100644 --- a/src/main/resources/locales/pt.yml +++ b/src/main/resources/locales/pt.yml @@ -11,6 +11,9 @@ prefixes: an-island: uma ilha islands: ilhas general: + dialogs: + game-mode-selection: + title: 'Escolhe o teu jogo' did-you-mean: suggestion: 'Queria dizer [command]? Clique aqui ou escreva yes para o executar.[run_command: [command]][hover: Clique para executar [command]]' header: 'Queria dizer um destes? Clique num para executar:' @@ -720,6 +723,8 @@ commands: A teletransporte falhou por algum motivo. Por favor, tente novamente mais tarde. unknown-home: 'Nome residencial desconhecido!' + picker: + title: 'Para onde queres ir?' help: description: o comando da ilha principal spawn: @@ -922,6 +927,11 @@ commands: invite: description: convide um jogador para se juntar à sua ilha invitation-sent: 'Convite enviado para [name].' + dialog: + title: 'Convite de equipa de [name]' + body: '[name] convidou-te para te juntares à ilha dele. Aceitar?' + accept: 'Aceitar' + decline: 'Recusar' removing-invite: 'Removendo convite.' name-has-invited-you: '[name] convidou você para se juntar à ilha deles.' to-accept-or-reject: >- diff --git a/src/main/resources/locales/ro.yml b/src/main/resources/locales/ro.yml index 9fbb06776..8bd157939 100644 --- a/src/main/resources/locales/ro.yml +++ b/src/main/resources/locales/ro.yml @@ -11,6 +11,9 @@ prefixes: an-island: o insulă islands: insule general: + dialogs: + game-mode-selection: + title: 'Alege-ți jocul' did-you-mean: suggestion: 'Ai vrut să spui [command]? Dă clic aici sau tastează yes pentru a-l rula.[run_command: [command]][hover: Dă clic pentru a rula [command]]' header: 'Ai vrut să spui una dintre acestea? Dă clic pe una pentru a o rula:' @@ -728,6 +731,8 @@ commands: Teleportarea a eșuat din anumite motive. Vă rugăm să încercați din nou mai târziu. unknown-home: 'Nume de casă necunoscut!' + picker: + title: 'Unde vrei să mergi?' help: description: comandamentul insulei principale spawn: @@ -934,6 +939,11 @@ commands: invite: description: invită un jucător să se alăture insulei tale invitation-sent: 'Invitație trimisă la [name] .' + dialog: + title: 'Invitație de echipă de la [name]' + body: '[name] te-a invitat să te alături insulei sale. Accepți?' + accept: 'Acceptă' + decline: 'Refuză' removing-invite: 'Eliminarea invitației.' name-has-invited-you: | [name] v-a invitat diff --git a/src/main/resources/locales/ru.yml b/src/main/resources/locales/ru.yml index 984c26559..ba9f5685c 100644 --- a/src/main/resources/locales/ru.yml +++ b/src/main/resources/locales/ru.yml @@ -17,6 +17,9 @@ prefixes: an-island: остров islands: острова general: + dialogs: + game-mode-selection: + title: 'Выберите свою игру' did-you-mean: suggestion: 'Возможно, вы имели в виду [command]? Нажмите здесь или введите yes, чтобы выполнить.[run_command: [command]][hover: Нажмите, чтобы выполнить [command]]' header: 'Возможно, вы имели в виду одну из этих команд? Нажмите, чтобы выполнить:' @@ -705,6 +708,8 @@ commands: failure: Телепортация не удалась по какой-то причине. Пожалуйста, попробуйте снова позже. unknown-home: Неизвестное название точки дома! + picker: + title: 'Куда вы хотите отправиться?' help: description: основная команда режима spawn: @@ -903,6 +908,11 @@ commands: invite: description: приглашает игрока посетить ваш остров invitation-sent: Приглашение отправлено игроку [name]. + dialog: + title: 'Приглашение в команду от [name]' + body: '[name] пригласил вас присоединиться к своему острову. Принять?' + accept: 'Принять' + decline: 'Отклонить' removing-invite: Удаление приглашения. name-has-invited-you: [name] пригласил вас посетить его остров. to-accept-or-reject: Введите /[label] team accept чтобы принять либо diff --git a/src/main/resources/locales/tr.yml b/src/main/resources/locales/tr.yml index 342f28b39..91c5f7583 100644 --- a/src/main/resources/locales/tr.yml +++ b/src/main/resources/locales/tr.yml @@ -12,6 +12,9 @@ prefixes: an-island: bir ada islands: adalar general: + dialogs: + game-mode-selection: + title: 'Oyununu seç' did-you-mean: suggestion: 'Şunu mu demek istediniz: [command]? Çalıştırmak için buraya tıklayın veya yes yazın.[run_command: [command]][hover: [command] çalıştırmak için tıklayın]' header: 'Şunlardan birini mi demek istediniz? Çalıştırmak için birine tıklayın:' @@ -701,6 +704,8 @@ commands: Bir nedenden dolayı ışınlanma başarısız oldu. Lütfen daha sonra tekrar deneyin. unknown-home: 'Bilinmeyen ev adı!' + picker: + title: 'Nereye gitmek istersin?' help: description: Ana ada komudu spawn: @@ -894,6 +899,11 @@ commands: invite: description: Adana oyuncu davet et. invitation-sent: '[name] oyuncusuna davet gonderildi!' + dialog: + title: '[name] adlı oyuncudan takım daveti' + body: '[name] seni adasına katılmaya davet etti. Kabul edilsin mi?' + accept: 'Kabul et' + decline: 'Reddet' removing-invite: 'Davet iptal edildi.' name-has-invited-you: '[name] seni adasına katılman icin davet etti!' to-accept-or-reject: >- diff --git a/src/main/resources/locales/uk.yml b/src/main/resources/locales/uk.yml index 15a7c8068..cf17f2b09 100644 --- a/src/main/resources/locales/uk.yml +++ b/src/main/resources/locales/uk.yml @@ -18,6 +18,9 @@ prefixes: an-island: острів islands: острови general: + dialogs: + game-mode-selection: + title: 'Виберіть свою гру' did-you-mean: suggestion: 'Можливо, ви мали на увазі [command]? Натисніть тут або введіть yes, щоб виконати.[run_command: [command]][hover: Натисніть, щоб виконати [command]]' header: 'Можливо, ви мали на увазі одну з цих команд? Натисніть, щоб виконати:' @@ -645,6 +648,8 @@ commands: teleported: 'Телепортовано до дому [number].' failure: 'Не вдалося телепортуватися. Спробуйте пізніше.' unknown-home: 'Невідома назва дому!' + picker: + title: 'Куди ви хочете піти?' help: description: головна команда [prefix_island] spawn: @@ -837,6 +842,11 @@ commands: invite: description: запросити гравця приєднатися до вашого [prefix_island] invitation-sent: 'Запрошення надіслано для [name].' + dialog: + title: 'Запрошення в команду від [name]' + body: '[name] запросив вас приєднатися до свого острова. Прийняти?' + accept: 'Прийняти' + decline: 'Відхилити' removing-invite: 'Видаляю запрошення.' name-has-invited-you: | [name] запросив(ла) вас diff --git a/src/main/resources/locales/vi.yml b/src/main/resources/locales/vi.yml index 38282dae3..c6c3c7107 100644 --- a/src/main/resources/locales/vi.yml +++ b/src/main/resources/locales/vi.yml @@ -11,6 +11,9 @@ prefixes: an-island: một hòn đảo islands: đảo general: + dialogs: + game-mode-selection: + title: 'Chọn trò chơi của bạn' did-you-mean: suggestion: 'Có phải bạn muốn nhập [command]? Nhấp vào đây hoặc gõ yes để chạy lệnh.[run_command: [command]][hover: Nhấp để chạy [command]]' header: 'Có phải bạn muốn nhập một trong các lệnh sau? Nhấp vào một lệnh để chạy:' @@ -705,6 +708,8 @@ commands: teleported: 'Đang dịch chuyển về nhà #[number].' failure: 'Di chuyển không thành công vì một số lý do. Vui lòng thử lại sau.' unknown-home: 'Tên nhà không tồn tại!' + picker: + title: 'Bạn muốn đi đâu?' help: description: lệnh chính cho đảo spawn: @@ -898,6 +903,11 @@ commands: invite: description: mời người chơi vào đảo của bạn invitation-sent: 'Lời mời đã gửi tới [name].' + dialog: + title: 'Lời mời nhóm từ [name]' + body: '[name] đã mời bạn tham gia đảo của họ. Chấp nhận?' + accept: 'Chấp nhận' + decline: 'Từ chối' removing-invite: 'Đang xóa lời mời.' name-has-invited-you: '[name] đã mời bạn vào đảo họ.' to-accept-or-reject: >- diff --git a/src/main/resources/locales/zh-CN.yml b/src/main/resources/locales/zh-CN.yml index cd97beeb3..efa8e0f42 100644 --- a/src/main/resources/locales/zh-CN.yml +++ b/src/main/resources/locales/zh-CN.yml @@ -15,6 +15,9 @@ prefixes: an-island: 岛屿 islands: 岛屿 general: + dialogs: + game-mode-selection: + title: '选择你的游戏' did-you-mean: suggestion: '你是想输入 [command] 吗?点击此处或输入 yes 来执行。[run_command: [command]][hover: 点击执行 [command]]' header: '你是想输入以下命令之一吗?点击即可执行:' @@ -657,6 +660,8 @@ commands: teleported: '正在将你传送到你的岛屿传送点: [number].' failure: '传送失败,出于某种原因。请稍后再试。' unknown-home: '未知岛屿传送点名称!' + picker: + title: '想去哪里?' help: description: 岛屿主要命令 spawn: @@ -842,6 +847,11 @@ commands: invite: description: 邀请一位玩家以成员身份加入团队(对方会失去自己领取的岛屿) invitation-sent: '邀请已发送给[name].' + dialog: + title: '来自 [name] 的队伍邀请' + body: '[name] 邀请你加入他们的岛屿。接受吗?' + accept: '接受' + decline: '拒绝' removing-invite: '邀请已取消.' name-has-invited-you: '[name]邀请你以成员身份加入TA的岛屿.' to-accept-or-reject: |- diff --git a/src/main/resources/locales/zh-HK.yml b/src/main/resources/locales/zh-HK.yml index 8896bdf7e..8c4b7bac1 100644 --- a/src/main/resources/locales/zh-HK.yml +++ b/src/main/resources/locales/zh-HK.yml @@ -14,6 +14,9 @@ prefixes: an-island: 一個島 islands: 島嶼 general: + dialogs: + game-mode-selection: + title: '選擇你的遊戲' did-you-mean: suggestion: '你是想輸入 [command] 嗎?點擊此處或輸入 yes 來執行。[run_command: [command]][hover: 點擊執行 [command]]' header: '你是想輸入以下指令之一嗎?點擊即可執行:' @@ -655,6 +658,8 @@ commands: teleported: '傳送到您的第 #[number] 號家。' failure: '傳送由於某種原因失敗。請稍後再試。' unknown-home: '未知島嶼傳送點。' + picker: + title: '想去哪裡?' help: description: 主要島嶼命令 spawn: @@ -841,6 +846,11 @@ commands: invite: description: 邀請玩家來您的島嶼 invitation-sent: '邀請已經發送給 [name]' + dialog: + title: '來自 [name] 的隊伍邀請' + body: '[name] 邀請你加入他們的島嶼。接受嗎?' + accept: '接受' + decline: '拒絕' removing-invite: '移除邀請' name-has-invited-you: '[name] 邀請您去他們的島嶼。' to-accept-or-reject: |- diff --git a/src/test/java/world/bentobox/bentobox/api/commands/island/IslandGoCommandTest.java b/src/test/java/world/bentobox/bentobox/api/commands/island/IslandGoCommandTest.java index bb059af19..0af742d9a 100644 --- a/src/test/java/world/bentobox/bentobox/api/commands/island/IslandGoCommandTest.java +++ b/src/test/java/world/bentobox/bentobox/api/commands/island/IslandGoCommandTest.java @@ -250,6 +250,36 @@ void testExecuteNoArgsMultipleHomes() { assertTrue(igc.execute(user, igc.getLabel(), Collections.emptyList())); } + /** + * With the go picker enabled and several destinations, a bare /go tries to show the + * dialog; when it cannot be built (as under test) it falls back to the default teleport. + * Test method for {@link IslandGoCommand#execute(User, String, List)} + */ + @Test + void testExecuteNoArgsGoPickerFallsBack() { + when(island.getName()).thenReturn("MyIsland"); + when(island.getHomes()).thenReturn(Map.of("Home", mock(Location.class))); + when(s.isDialogGoPicker()).thenReturn(true); + assertTrue(igc.execute(user, igc.getLabel(), Collections.emptyList())); + verify(plugin).logError(Mockito.contains("go picker")); + // Fell back to the default home teleport + verify(im).homeTeleportAsync(world, mockPlayer); + } + + /** + * With the go picker disabled, a bare /go teleports directly without touching dialogs. + * Test method for {@link IslandGoCommand#execute(User, String, List)} + */ + @Test + void testExecuteNoArgsGoPickerDisabled() { + when(island.getName()).thenReturn("MyIsland"); + when(island.getHomes()).thenReturn(Map.of("Home", mock(Location.class))); + when(s.isDialogGoPicker()).thenReturn(false); + assertTrue(igc.execute(user, igc.getLabel(), Collections.emptyList())); + verify(plugin, Mockito.never()).logError(Mockito.contains("go picker")); + verify(im).homeTeleportAsync(world, mockPlayer); + } + /** * Test method for {@link IslandGoCommand#execute(User, String, List)} */ diff --git a/src/test/java/world/bentobox/bentobox/api/commands/island/IslandResetCommandTest.java b/src/test/java/world/bentobox/bentobox/api/commands/island/IslandResetCommandTest.java index 6fc202500..f23e51835 100644 --- a/src/test/java/world/bentobox/bentobox/api/commands/island/IslandResetCommandTest.java +++ b/src/test/java/world/bentobox/bentobox/api/commands/island/IslandResetCommandTest.java @@ -365,7 +365,7 @@ void testConfirmationDialogFallsBackWhenUnavailable() throws Exception { // Require confirmation, and prefer a dialog for an online player when(s.isResetConfirmation()).thenReturn(true); when(s.getConfirmationTime()).thenReturn(20); - when(s.isUseDialogConfirmation()).thenReturn(true); + when(s.isDialogConfirmations()).thenReturn(true); when(user.isPlayer()).thenReturn(true); assertTrue(irc.execute(user, irc.getLabel(), Collections.emptyList())); diff --git a/src/test/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamInviteCommandTest.java b/src/test/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamInviteCommandTest.java index 2c1a68f17..aca7a4395 100644 --- a/src/test/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamInviteCommandTest.java +++ b/src/test/java/world/bentobox/bentobox/api/commands/island/team/IslandTeamInviteCommandTest.java @@ -306,6 +306,27 @@ void testExecuteSuccessTargetHasIsland() { } + /** + * With team-invite dialogs enabled and an online target, execute attempts to show the + * accept/decline dialog; when it cannot be built (as under test) the chat invite still + * goes out and the failure is logged. + * Test method for + * {@link world.bentobox.bentobox.api.commands.island.team.IslandTeamInviteCommand#execute(User, String, java.util.List)}. + */ + @Test + void testExecuteInviteDialogFallsBack() { + when(im.getIsland(world, uuid)).thenReturn(island); + testCanExecuteSuccess(); + when(target.isPlayer()).thenReturn(true); + when(s.isDialogTeamInvites()).thenReturn(true); + assertTrue(itl.execute(user, itl.getLabel(), List.of("target"))); + // Chat invite still sent as the fallback + verify(user).sendMessage("commands.island.team.invite.invitation-sent", TextVariables.NAME, "target", + TextVariables.DISPLAY_NAME, "&Ctarget"); + // Dialog build failed under test and was logged + verify(plugin).logError(Mockito.contains("team invite dialog")); + } + /** * Test method for * {@link world.bentobox.bentobox.api.commands.island.team.IslandTeamInviteCommand#execute(User, String, java.util.List)}. diff --git a/src/test/java/world/bentobox/bentobox/listeners/JoinLeaveListenerTest.java b/src/test/java/world/bentobox/bentobox/listeners/JoinLeaveListenerTest.java index cc6511d6c..4152e68db 100644 --- a/src/test/java/world/bentobox/bentobox/listeners/JoinLeaveListenerTest.java +++ b/src/test/java/world/bentobox/bentobox/listeners/JoinLeaveListenerTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.contains; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -16,6 +17,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -44,6 +46,7 @@ import world.bentobox.bentobox.Settings; import world.bentobox.bentobox.api.addons.AddonDescription; import world.bentobox.bentobox.api.addons.GameModeAddon; +import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.database.objects.Players; @@ -290,6 +293,31 @@ void testOnPlayerJoinNotKnownAutoCreate() { checkSpigotMessage("commands.island.create.on-first-login"); } + /** + * With the game-mode selection dialog enabled and several game modes installed, a first + * join tries to show the dialog; when it cannot be built (as under test) it logs and + * falls through to the normal first-login handling. + * Test method for {@link world.bentobox.bentobox.listeners.JoinLeaveListener#onPlayerJoin(PlayerJoinEvent)}. + */ + @Test + void testOnPlayerJoinGameModeDialogFallsBack() { + when(settings.isDialogGameModeSelection()).thenReturn(true); + AddonDescription desc = mock(AddonDescription.class); + when(desc.getName()).thenReturn("BSkyBlock"); + CompositeCommand cmd = mock(CompositeCommand.class); + GameModeAddon gm1 = mock(GameModeAddon.class); + GameModeAddon gm2 = mock(GameModeAddon.class); + when(gm1.getPlayerCommand()).thenReturn(Optional.of(cmd)); + when(gm2.getPlayerCommand()).thenReturn(Optional.of(cmd)); + when(gm1.getDescription()).thenReturn(desc); + when(gm2.getDescription()).thenReturn(desc); + when(am.getGameModeAddons()).thenReturn(List.of(gm1, gm2)); + + jll.onPlayerJoin(new PlayerJoinEvent(mockPlayer, component)); + + verify(plugin).logError(contains("game mode selection dialog")); + } + /** * Test method for * {@link world.bentobox.bentobox.listeners.JoinLeaveListener#onPlayerSwitchWorld(org.bukkit.event.player.PlayerChangedWorldEvent)}.