diff --git a/core/src/main/java/org/geysermc/geyser/session/dialog/Dialog.java b/core/src/main/java/org/geysermc/geyser/session/dialog/Dialog.java index 93d30f641..394478e0d 100644 --- a/core/src/main/java/org/geysermc/geyser/session/dialog/Dialog.java +++ b/core/src/main/java/org/geysermc/geyser/session/dialog/Dialog.java @@ -67,6 +67,8 @@ public abstract class Dialog { private final AfterAction afterAction; private final List labels; private final List> inputs = new ArrayList<>(); + @Getter + private final ParsedInputs defaultInputs; protected Dialog(GeyserSession session, NbtMap map) { title = MessageTranslator.convertFromNullableNbtTag(session, map.get("title")); @@ -96,6 +98,7 @@ public abstract class Dialog { for (NbtMap input : inputTag) { inputs.add(DialogInput.read(session, input)); } + defaultInputs = inputs.isEmpty() ? ParsedInputs.EMPTY : new ParsedInputs(inputs); } private static Optional readBody(GeyserSession session, NbtMap tag) { @@ -123,6 +126,9 @@ public abstract class Dialog { CustomForm.Builder builder = CustomForm.builder() .translator(MinecraftLocale::getLocaleString, session.locale()) .title(title); + for (String label : labels) { + builder.label(label); + } restored.ifPresentOrElse(last -> last.restore(builder), () -> inputs.forEach(input -> input.addComponent(builder))); builder.closedOrInvalidResultHandler(response -> holder.closeDialog(onCancel())); @@ -143,8 +149,13 @@ public abstract class Dialog { session.sendDialogForm(createForm(session, Optional.of(inputs), holder).build()); } - protected ParsedInputs parseInput(CustomFormResponse response) { - return new ParsedInputs(inputs, response); + protected Optional parseInput(GeyserSession session, CustomFormResponse response, DialogHolder holder) { + ParsedInputs parsed = new ParsedInputs(inputs, response); + if (parsed.hasErrors()) { + restoreForm(session, parsed, holder); + return Optional.empty(); + } + return Optional.of(parsed); } public static Dialog readDialog(RegistryEntryContext context) { diff --git a/core/src/main/java/org/geysermc/geyser/session/dialog/DialogHolder.java b/core/src/main/java/org/geysermc/geyser/session/dialog/DialogHolder.java index 330079815..b12a761e6 100644 --- a/core/src/main/java/org/geysermc/geyser/session/dialog/DialogHolder.java +++ b/core/src/main/java/org/geysermc/geyser/session/dialog/DialogHolder.java @@ -146,7 +146,7 @@ public class DialogHolder { // Don't run close functionality if we're asking for command confirmation if (dialog.canCloseWithEscape()) { shouldClose = true; - if (runAction(onCancel, lastInputs == null ? ParsedInputs.EMPTY : lastInputs)) { + if (runAction(onCancel, lastInputs == null ? dialog.defaultInputs() : lastInputs)) { manager.close(); } return; @@ -216,7 +216,7 @@ public class DialogHolder { .translator(MinecraftLocale::getLocaleString, manager.session().locale()) .title("gui.waitingForResponse.title") .content(content) - .optionalButton("Back", sendBackButton) + .optionalButton("gui.back", sendBackButton) .closedOrInvalidResultHandler(() -> { if (stillValid()) { // If still waiting on a new dialog waitForResponse(); @@ -229,7 +229,7 @@ public class DialogHolder { /** * This method runs the given action, if present, with the given inputs. * - *

These inputs can be {@link ParsedInputs#EMPTY} when the dialog has no inputs, or the dialog was closed without entering anything, but can never be {@code null}. + *

These inputs can be {@link ParsedInputs#EMPTY} when the dialog has no inputs, but can never be {@code null}. * The method returns {@code true} if the dialog's after action can be executed, and {@code false} if not. The latter is the case when the action opened a new * dialog or screen, in which case the after action will not be handled or be handled by the screen, respectively.

* diff --git a/core/src/main/java/org/geysermc/geyser/session/dialog/DialogWithButtons.java b/core/src/main/java/org/geysermc/geyser/session/dialog/DialogWithButtons.java index 4ac0add5b..789dbfceb 100644 --- a/core/src/main/java/org/geysermc/geyser/session/dialog/DialogWithButtons.java +++ b/core/src/main/java/org/geysermc/geyser/session/dialog/DialogWithButtons.java @@ -58,13 +58,14 @@ public abstract class DialogWithButtons extends Dialog { builder.dropdown(dropdown); builder.validResultHandler(response -> { - ParsedInputs inputs = parseInput(response); - int selection = response.asDropdown(); - if (selection == buttons.size()) { - holder.runButton(exitAction, inputs); - } else { - holder.runButton(Optional.of(buttons.get(selection)), inputs); - } + parseInput(session, response, holder).ifPresent(inputs -> { + int selection = response.asDropdown(); + if (selection == buttons.size()) { + holder.runButton(exitAction, inputs); + } else { + holder.runButton(Optional.of(buttons.get(selection)), inputs); + } + }); }); } @@ -79,7 +80,6 @@ public abstract class DialogWithButtons extends Dialog { if (response.clickedButtonId() == buttons.size()) { holder.runButton(exitAction, ParsedInputs.EMPTY); } else { - holder.runButton(Optional.of(buttons.get(response.clickedButtonId())), ParsedInputs.EMPTY); } }); diff --git a/core/src/main/java/org/geysermc/geyser/session/dialog/NoticeDialog.java b/core/src/main/java/org/geysermc/geyser/session/dialog/NoticeDialog.java index 54c226b80..2c0804133 100644 --- a/core/src/main/java/org/geysermc/geyser/session/dialog/NoticeDialog.java +++ b/core/src/main/java/org/geysermc/geyser/session/dialog/NoticeDialog.java @@ -53,7 +53,7 @@ public class NoticeDialog extends Dialog { @Override protected void addCustomComponents(GeyserSession session, CustomForm.Builder builder, DialogHolder holder) { - builder.validResultHandler(response -> holder.runButton(button, parseInput(response))); + builder.validResultHandler(response -> parseInput(session, response, holder).ifPresent(inputs -> holder.runButton(button, inputs))); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/session/dialog/input/BooleanInput.java b/core/src/main/java/org/geysermc/geyser/session/dialog/input/BooleanInput.java index 4051194ad..e2388b970 100644 --- a/core/src/main/java/org/geysermc/geyser/session/dialog/input/BooleanInput.java +++ b/core/src/main/java/org/geysermc/geyser/session/dialog/input/BooleanInput.java @@ -56,7 +56,7 @@ public class BooleanInput extends DialogInput { } @Override - public Boolean read(CustomFormResponse response) { + public Boolean read(CustomFormResponse response) throws DialogInputParseException { return response.asToggle(); } diff --git a/core/src/main/java/org/geysermc/geyser/session/dialog/input/DialogInput.java b/core/src/main/java/org/geysermc/geyser/session/dialog/input/DialogInput.java index 9b3817e79..805df0751 100644 --- a/core/src/main/java/org/geysermc/geyser/session/dialog/input/DialogInput.java +++ b/core/src/main/java/org/geysermc/geyser/session/dialog/input/DialogInput.java @@ -51,7 +51,7 @@ public abstract class DialogInput { public abstract void addComponent(CustomForm.Builder builder, Optional restored); - public abstract T read(CustomFormResponse response); + public abstract T read(CustomFormResponse response) throws DialogInputParseException; public abstract String asSubstitution(T value); diff --git a/core/src/main/java/org/geysermc/geyser/session/dialog/input/DialogInputParseException.java b/core/src/main/java/org/geysermc/geyser/session/dialog/input/DialogInputParseException.java new file mode 100644 index 000000000..9542a4c8b --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/session/dialog/input/DialogInputParseException.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.session.dialog.input; + +import lombok.Getter; + +public class DialogInputParseException extends Exception { + + // Exceptions don't work with generics, so we have to do a bit of unsafe casting and assume the object is of the input type :( + @Getter + private final Object partial; + + public DialogInputParseException(String message, Object partial) { + super(message); + this.partial = partial; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/session/dialog/input/NumberRangeInput.java b/core/src/main/java/org/geysermc/geyser/session/dialog/input/NumberRangeInput.java index dd08c72a2..a6a2fd41e 100644 --- a/core/src/main/java/org/geysermc/geyser/session/dialog/input/NumberRangeInput.java +++ b/core/src/main/java/org/geysermc/geyser/session/dialog/input/NumberRangeInput.java @@ -59,7 +59,7 @@ public class NumberRangeInput extends DialogInput { } @Override - public Float read(CustomFormResponse response) { + public Float read(CustomFormResponse response) throws DialogInputParseException { return response.asSlider(); } diff --git a/core/src/main/java/org/geysermc/geyser/session/dialog/input/ParsedInputs.java b/core/src/main/java/org/geysermc/geyser/session/dialog/input/ParsedInputs.java index a632c9249..c52380ee9 100644 --- a/core/src/main/java/org/geysermc/geyser/session/dialog/input/ParsedInputs.java +++ b/core/src/main/java/org/geysermc/geyser/session/dialog/input/ParsedInputs.java @@ -30,19 +30,26 @@ import org.cloudburstmc.nbt.NbtMapBuilder; import org.geysermc.cumulus.form.CustomForm; import org.geysermc.cumulus.response.CustomFormResponse; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; public class ParsedInputs { - public static final ParsedInputs EMPTY = new ParsedInputs(List.of(), null); + public static final ParsedInputs EMPTY = new ParsedInputs(List.of()); private final Map, Object> values = new LinkedHashMap<>(); + private final Map, String> errors = new HashMap<>(); public ParsedInputs(List> inputs, CustomFormResponse response) { for (DialogInput input : inputs) { - values.put(input, input.read(response)); + try { + values.put(input, input.read(response)); + } catch (DialogInputParseException exception) { + values.put(input, exception.getPartial()); + errors.put(input, exception.getMessage()); + } } } @@ -54,6 +61,11 @@ public class ParsedInputs { public void restore(CustomForm.Builder builder) { for (Map.Entry, Object> entry : values.entrySet()) { + String error = errors.get(entry.getKey()); + if (error != null) { + builder.label("§cError parsing input data: " + error + "."); + builder.label("§cPlease adjust!"); + } // Can't be a Geyser update without eclipse dealing with generics ((DialogInput) entry.getKey()).addComponent(builder, Optional.of(entry.getValue())); } @@ -75,4 +87,8 @@ public class ParsedInputs { } return builder.build(); } + + public boolean hasErrors() { + return !errors.isEmpty(); + } } diff --git a/core/src/main/java/org/geysermc/geyser/session/dialog/input/SingleOptionInput.java b/core/src/main/java/org/geysermc/geyser/session/dialog/input/SingleOptionInput.java index f65217edd..f996f3da0 100644 --- a/core/src/main/java/org/geysermc/geyser/session/dialog/input/SingleOptionInput.java +++ b/core/src/main/java/org/geysermc/geyser/session/dialog/input/SingleOptionInput.java @@ -78,7 +78,7 @@ public class SingleOptionInput extends DialogInput { } @Override - public String read(CustomFormResponse response) { + public String read(CustomFormResponse response) throws DialogInputParseException { return entries.get(response.asDropdown()).id(); } diff --git a/core/src/main/java/org/geysermc/geyser/session/dialog/input/TextInput.java b/core/src/main/java/org/geysermc/geyser/session/dialog/input/TextInput.java index f9cb983b7..df3ce1029 100644 --- a/core/src/main/java/org/geysermc/geyser/session/dialog/input/TextInput.java +++ b/core/src/main/java/org/geysermc/geyser/session/dialog/input/TextInput.java @@ -56,11 +56,13 @@ public class TextInput extends DialogInput { } @Override - public String read(CustomFormResponse response) { - String raw = response.asInput(); - assert raw != null; - // Bedrock doesn't support setting a max length, so we just cut it off to not have the server complain - return raw.substring(0, Math.min(raw.length(), maxLength)); + public String read(CustomFormResponse response) throws DialogInputParseException { + String text = response.asInput(); + assert text != null; + if (text.length() > maxLength) { + throw new DialogInputParseException("length of text cannot be above " + maxLength, text); + } + return text; } @Override