1
0
mirror of https://github.com/GeyserMC/Geyser.git synced 2025-12-29 11:49:16 +00:00

Add input validation, fix forms with inputs not having labels

This commit is contained in:
Eclipse
2025-06-05 12:24:14 +00:00
parent b5d1f70186
commit a71eb5e086
11 changed files with 94 additions and 25 deletions

View File

@@ -67,6 +67,8 @@ public abstract class Dialog {
private final AfterAction afterAction;
private final List<String> labels;
private final List<DialogInput<?>> 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<String> 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<ParsedInputs> 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) {

View File

@@ -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.
*
* <p>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}.
* <p>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.</p>
*

View File

@@ -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);
}
});

View File

@@ -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

View File

@@ -56,7 +56,7 @@ public class BooleanInput extends DialogInput<Boolean> {
}
@Override
public Boolean read(CustomFormResponse response) {
public Boolean read(CustomFormResponse response) throws DialogInputParseException {
return response.asToggle();
}

View File

@@ -51,7 +51,7 @@ public abstract class DialogInput<T> {
public abstract void addComponent(CustomForm.Builder builder, Optional<T> restored);
public abstract T read(CustomFormResponse response);
public abstract T read(CustomFormResponse response) throws DialogInputParseException;
public abstract String asSubstitution(T value);

View File

@@ -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;
}
}

View File

@@ -59,7 +59,7 @@ public class NumberRangeInput extends DialogInput<Float> {
}
@Override
public Float read(CustomFormResponse response) {
public Float read(CustomFormResponse response) throws DialogInputParseException {
return response.asSlider();
}

View File

@@ -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<DialogInput<?>, Object> values = new LinkedHashMap<>();
private final Map<DialogInput<?>, String> errors = new HashMap<>();
public ParsedInputs(List<DialogInput<?>> 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<DialogInput<?>, 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();
}
}

View File

@@ -78,7 +78,7 @@ public class SingleOptionInput extends DialogInput<String> {
}
@Override
public String read(CustomFormResponse response) {
public String read(CustomFormResponse response) throws DialogInputParseException {
return entries.get(response.asDropdown()).id();
}

View File

@@ -56,11 +56,13 @@ public class TextInput extends DialogInput<String> {
}
@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