1
0
mirror of https://github.com/GeyserMC/Geyser.git synced 2026-01-06 15:41:50 +00:00

Work on dialog inputs and dynamic actions

This commit is contained in:
Eclipse
2025-06-02 19:06:35 +00:00
parent 50f7fbbc2e
commit 779d54812b
10 changed files with 348 additions and 48 deletions

View File

@@ -28,10 +28,8 @@ package org.geysermc.geyser.session.dialog;
import net.kyori.adventure.key.Key;
import org.cloudburstmc.nbt.NbtMap;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.dialog.action.DialogAction;
import org.geysermc.geyser.util.MinecraftKey;
import java.util.List;
import java.util.Optional;
public class ConfirmationDialog extends DialogWithButtons {
@@ -43,7 +41,7 @@ public class ConfirmationDialog extends DialogWithButtons {
}
@Override
protected Optional<DialogAction> onCancel() {
return buttons.get(1).action(); // "no" button
protected Optional<DialogButton> onCancel() {
return buttons.size() > 1 ? Optional.of(buttons.get(1)) : Optional.empty(); // "no" button, there should always be 2 buttons but check just to be sure
}
}

View File

@@ -27,6 +27,7 @@ package org.geysermc.geyser.session.dialog;
import net.kyori.adventure.key.Key;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtType;
import org.geysermc.cumulus.form.CustomForm;
import org.geysermc.cumulus.form.Form;
import org.geysermc.cumulus.form.SimpleForm;
@@ -36,7 +37,8 @@ import org.geysermc.cumulus.response.FormResponse;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.registry.JavaRegistries;
import org.geysermc.geyser.session.cache.registry.RegistryEntryContext;
import org.geysermc.geyser.session.dialog.action.DialogAction;
import org.geysermc.geyser.session.dialog.input.DialogInput;
import org.geysermc.geyser.session.dialog.input.ParsedInputs;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.MinecraftKey;
import org.geysermc.mcprotocollib.protocol.data.game.Holder;
@@ -46,7 +48,6 @@ import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.ToIntFunction;
public abstract class Dialog {
@@ -55,9 +56,9 @@ public abstract class Dialog {
private final String title;
private final String externalTitle;
protected final AfterAction afterAction;
private final AfterAction afterAction;
private final List<String> labels;
private final List<Object> inputs; // TODO
private final List<DialogInput<?>> inputs = new ArrayList<>();
protected Dialog(GeyserSession session, NbtMap map) {
title = MessageTranslator.convertFromNullableNbtTag(session, map.get("title"));
@@ -70,20 +71,22 @@ public abstract class Dialog {
} else if (bodyTag instanceof NbtMap bodyMap) {
labels = readBody(session, bodyMap).map(List::of).orElse(List.of());
} else if (bodyTag instanceof List<?> bodyList) {
List<String> bodies = new ArrayList<>();
labels = new ArrayList<>();
for (Object tag : bodyList) {
if (tag instanceof NbtMap bodyMap) {
readBody(session, bodyMap).ifPresent(bodies::add);
readBody(session, bodyMap).ifPresent(labels::add);
} else {
throw new IllegalStateException("Found non-NBT map in list of bodies, was: " + tag);
}
}
labels = List.copyOf(bodies);
} else {
throw new IllegalStateException("Expected body tag to either be a NBT map or list thereof, was: " + bodyTag);
}
inputs = List.of();
List<NbtMap> inputTag = map.getList("inputs", NbtType.COMPOUND);
for (NbtMap input : inputTag) {
inputs.add(DialogInput.read(session, input));
}
}
private static Optional<String> readBody(GeyserSession session, NbtMap tag) {
@@ -95,7 +98,7 @@ public abstract class Dialog {
return Optional.empty();
}
protected abstract Optional<DialogAction> onCancel();
protected abstract Optional<DialogButton> onCancel();
protected FormBuilder<? extends FormBuilder<?,?,?>, ? extends Form, ? extends FormResponse> createForm(GeyserSession session) {
if (inputs.isEmpty()) {
@@ -103,14 +106,15 @@ public abstract class Dialog {
.title(title);
builder.content(String.join("\n", labels));
builder.closedOrInvalidResultHandler(actionResult(session, onCancel()));
builder.closedOrInvalidResultHandler(() -> runButton(session, onCancel(), ParsedInputs.EMPTY));
addCustomComponents(session, builder);
return builder;
} else {
CustomForm.Builder builder = CustomForm.builder()
.title(title);
builder.closedOrInvalidResultHandler(actionResult(session, onCancel())); // TODO parse input
inputs.forEach(input -> input.addComponent(builder, Optional.empty()));
builder.closedOrInvalidResultHandler(response -> runButton(session, onCancel(), new ParsedInputs(inputs)));
addCustomComponents(session, builder);
return builder;
}
@@ -124,17 +128,12 @@ public abstract class Dialog {
return createForm(session).build();
}
protected Object parseInput(CustomFormResponse response) {
return 0; // TODO
protected ParsedInputs parseInput(CustomFormResponse response) {
return new ParsedInputs(inputs, response);
}
protected Runnable actionResult(GeyserSession session, Optional<DialogAction> action) {
return () -> action.ifPresent(present -> present.run(session, afterAction));
}
protected Consumer<CustomFormResponse> validResultAction(GeyserSession session, Optional<DialogAction> action) {
Runnable runnable = actionResult(session, action);
return response -> runnable.run();
protected void runButton(GeyserSession session, Optional<DialogButton> button, ParsedInputs inputs) {
button.flatMap(DialogButton::action).ifPresent(action -> action.run(session, inputs));
}
public static Dialog readDialog(RegistryEntryContext context) {
@@ -161,7 +160,7 @@ public abstract class Dialog {
protected void addCustomComponents(GeyserSession session, SimpleForm.Builder builder) {}
@Override
protected Optional<DialogAction> onCancel() {
protected Optional<DialogButton> onCancel() {
return Optional.empty();
}
};

View File

@@ -30,6 +30,7 @@ import org.geysermc.cumulus.component.DropdownComponent;
import org.geysermc.cumulus.form.CustomForm;
import org.geysermc.cumulus.form.SimpleForm;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.dialog.input.ParsedInputs;
import java.util.ArrayList;
import java.util.List;
@@ -54,9 +55,9 @@ public abstract class DialogWithButtons extends Dialog {
builder.dropdown(dropdown);
builder.validResultHandler(response -> {
parseInput(response); // TODO
ParsedInputs inputs = parseInput(response);
int selection = response.asDropdown();
buttons.get(selection).action().ifPresent(action -> action.run(session, afterAction)); // TODO check size?
runButton(session, Optional.of(buttons.get(selection)), inputs);
});
}
@@ -66,9 +67,7 @@ public abstract class DialogWithButtons extends Dialog {
builder.button(button.label());
}
builder.validResultHandler(response -> {
buttons.get(response.clickedButtonId()).action().ifPresent(action -> action.run(session, afterAction)); // TODO maybe button press method
});
builder.validResultHandler(response -> runButton(session, Optional.of(buttons.get(response.clickedButtonId())), ParsedInputs.EMPTY));
}
@SafeVarargs

View File

@@ -29,7 +29,6 @@ import net.kyori.adventure.key.Key;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtType;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.dialog.action.DialogAction;
import org.geysermc.geyser.util.MinecraftKey;
import java.util.Optional;
@@ -46,7 +45,7 @@ public class MultiActionDialog extends DialogWithButtons {
}
@Override
protected Optional<DialogAction> onCancel() {
return exit.flatMap(DialogButton::action);
protected Optional<DialogButton> onCancel() {
return exit;
}
}

View File

@@ -30,7 +30,7 @@ import org.cloudburstmc.nbt.NbtMap;
import org.geysermc.cumulus.form.CustomForm;
import org.geysermc.cumulus.form.SimpleForm;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.dialog.action.DialogAction;
import org.geysermc.geyser.session.dialog.input.ParsedInputs;
import org.geysermc.geyser.util.MinecraftKey;
import java.util.Optional;
@@ -47,17 +47,17 @@ public class NoticeDialog extends Dialog {
}
@Override
protected Optional<DialogAction> onCancel() {
return button.flatMap(DialogButton::action);
protected Optional<DialogButton> onCancel() {
return button;
}
@Override
protected void addCustomComponents(GeyserSession session, CustomForm.Builder builder) {
builder.validResultHandler(validResultAction(session, button.flatMap(DialogButton::action))); // TODO parse input
builder.validResultHandler(response -> runButton(session, button, parseInput(response)));
}
@Override
protected void addCustomComponents(GeyserSession session, SimpleForm.Builder builder) {
builder.validResultHandler(response -> button.flatMap(DialogButton::action).ifPresent(action -> action.run(session, afterAction)));
builder.validResultHandler(response -> runButton(session, button, ParsedInputs.EMPTY));
}
}

View File

@@ -27,7 +27,6 @@ package org.geysermc.geyser.session.dialog;
import org.cloudburstmc.nbt.NbtMap;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.dialog.action.DialogAction;
import java.util.List;
import java.util.Optional;
@@ -39,7 +38,7 @@ public class ServerLinksDialog extends DialogWithButtons {
}
@Override
protected Optional<DialogAction> onCancel() {
protected Optional<DialogButton> onCancel() {
return Optional.empty();
}
}

View File

@@ -31,10 +31,13 @@ import org.cloudburstmc.nbt.NbtMap;
import org.geysermc.cumulus.form.SimpleForm;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.dialog.Dialog;
import org.geysermc.geyser.session.dialog.input.ParsedInputs;
import org.geysermc.geyser.util.MinecraftKey;
import org.geysermc.mcprotocollib.protocol.data.game.Holder;
import org.geysermc.mcprotocollib.protocol.packet.common.serverbound.ServerboundCustomClickActionPacket;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public interface DialogAction {
@@ -50,22 +53,29 @@ public interface DialogAction {
} else if (type.equals(RunCommand.TYPE)) {
return Optional.of(new RunCommand(map.getString("command")));
} else if (type.equals(ShowDialog.TYPE)) {
return Optional.of(ShowDialog.readDialog(map.get("dialog"), idGetter));
return Optional.of(ShowDialog.read(map.get("dialog"), idGetter));
} else if (type.equals(Custom.TYPE)) {
return Optional.of(new Custom(MinecraftKey.key(map.getString("id")), map.getCompound("payload")));
} else if (type.equals(DynamicRunCommand.TYPE)) {
return Optional.of(DynamicRunCommand.read(map.getString("template")));
} else if (type.equals(DynamicCustom.TYPE)) {
return Optional.of(new DynamicCustom(MinecraftKey.key(map.getString("id")), map.getCompound("additions")));
}
// TODO the dynamic types
// Unknown or unsupported type
// Currently unsupported types are: suggest_command, change_page, copy_to_clipboard
// open_file is not supported by Java in dialogs
return Optional.empty();
}
void run(GeyserSession session, Dialog.AfterAction after);
void run(GeyserSession session, ParsedInputs inputs);
record OpenUrl(String url) implements DialogAction {
public static final Key TYPE = MinecraftKey.key("open_url");
@Override
public void run(GeyserSession session, Dialog.AfterAction after) {
public void run(GeyserSession session, ParsedInputs inputs) {
session.sendForm(SimpleForm.builder().title("Open URL").content(url));
}
}
@@ -75,7 +85,7 @@ public interface DialogAction {
public static final Key TYPE = MinecraftKey.key("run_command");
@Override
public void run(GeyserSession session, Dialog.AfterAction after) {
public void run(GeyserSession session, ParsedInputs inputs) {
session.sendCommand(command);
}
}
@@ -84,7 +94,7 @@ public interface DialogAction {
public static final Key TYPE = MinecraftKey.key("show_dialog");
private static ShowDialog readDialog(Object dialog, Dialog.IdGetter idGetter) {
private static ShowDialog read(Object dialog, Dialog.IdGetter idGetter) {
if (dialog instanceof NbtMap map) {
return new ShowDialog(Holder.ofCustom(map));
} else if (dialog instanceof String string) {
@@ -94,7 +104,7 @@ public interface DialogAction {
}
@Override
public void run(GeyserSession session, Dialog.AfterAction after) {
public void run(GeyserSession session, ParsedInputs inputs) {
// TODO figure out parent dialog
Dialog.showDialog(session, dialog);
}
@@ -106,8 +116,83 @@ public interface DialogAction {
public static final Key TYPE = MinecraftKey.key("custom");
@Override
public void run(GeyserSession session, Dialog.AfterAction after) {
public void run(GeyserSession session, ParsedInputs inputs) {
session.sendDownstreamPacket(new ServerboundCustomClickActionPacket(id, tag));
}
}
record DynamicRunCommand(List<String> segments, List<String> variables) implements DialogAction {
public static final Key TYPE = MinecraftKey.key("dynamic/run_command");
private static DynamicRunCommand read(String command) {
// Inspired by StringTemplate in mojmap
// Reads commands with 'macros', variables that are replaced with inputs, in a format like this:
// /say hey everyone, $(your_name) is super cool!
int length = command.length();
int lastVariable = 0;
int nextVariable = command.indexOf('$');
List<String> segments = new ArrayList<>();
List<String> variables = new ArrayList<>();
while (nextVariable != -1) {
if (nextVariable != length - 1 && command.charAt(nextVariable + 1) == '(') {
segments.add(command.substring(lastVariable, nextVariable));
int variableEnd = command.indexOf(')', nextVariable + 1);
if (variableEnd == -1) {
throw new IllegalArgumentException("Command ended with an open variable");
}
variables.add(command.substring(nextVariable + 2, variableEnd));
lastVariable = variableEnd + 1;
nextVariable = command.indexOf('$', lastVariable);
} else {
// If this $ was just an $ without a (, so no variable, which can occur in e.g. text components, just go to the next one
nextVariable = command.indexOf('$', nextVariable + 1);
}
}
if (lastVariable == 0) {
throw new IllegalArgumentException("No variables in command template");
} else {
// Append the remaining segment if there is one
if (lastVariable != length) {
segments.add(command.substring(lastVariable));
}
return new DynamicRunCommand(segments, variables);
}
}
@Override
public void run(GeyserSession session, ParsedInputs inputs) {
StringBuilder command = new StringBuilder();
List<String> parsedVariables = variables.stream().map(inputs::getSubstitution).toList();
for (int i = 0; i < variables.size(); i++) {
command.append(segments.get(i)).append(parsedVariables.get(i));
}
// Append the remaining segment if there is one
if (segments.size() > variables.size()) {
command.append(segments.get(segments.size() - 1));
}
session.sendCommand(command.toString());
}
}
record DynamicCustom(Key id, NbtMap additions) implements DialogAction {
public static final Key TYPE = MinecraftKey.key("dynamic/custom");
@Override
public void run(GeyserSession session, ParsedInputs inputs) {
NbtMap map = inputs.asNbtMap();
map.putAll(additions); // Can be optional on Java. We just read an empty map when it doesn't exist.
session.sendDownstreamPacket(new ServerboundCustomClickActionPacket(id, map));
}
}
}

View File

@@ -0,0 +1,77 @@
/*
* 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 net.kyori.adventure.key.Key;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.geysermc.cumulus.form.CustomForm;
import org.geysermc.cumulus.response.CustomFormResponse;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.MinecraftKey;
import java.util.Optional;
public class BooleanInput extends DialogInput<Boolean> {
public static final Key TYPE = MinecraftKey.key("boolean");
private final boolean initial;
private final String onTrue;
private final String onFalse;
public BooleanInput(GeyserSession session, NbtMap map) {
super(session, map);
initial = map.getBoolean("initial", false);
onTrue = map.getString("on_true", "true");
onFalse = map.getString("on_false", "false");
}
@Override
public void addComponent(CustomForm.Builder builder, Optional<Boolean> restored) {
builder.toggle(label, restored.orElse(initial));
}
@Override
public Boolean read(CustomFormResponse response) {
return response.asToggle();
}
@Override
public String asSubstitution(Boolean value) {
return value ? onTrue : onFalse;
}
@Override
public void addToMap(NbtMapBuilder builder, Boolean value) {
builder.put(key, value);
}
@Override
public Boolean defaultValue() {
return initial;
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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 net.kyori.adventure.key.Key;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.geysermc.cumulus.form.CustomForm;
import org.geysermc.cumulus.response.CustomFormResponse;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.MinecraftKey;
import java.util.Optional;
public abstract class DialogInput<T> {
protected final String key;
protected final String label;
protected DialogInput(GeyserSession session, NbtMap map) {
this.key = map.getString("key");
this.label = MessageTranslator.convertFromNullableNbtTag(session, map.get("label"));
}
public abstract void addComponent(CustomForm.Builder builder, Optional<T> restored);
public abstract T read(CustomFormResponse response);
public abstract String asSubstitution(T value);
public abstract void addToMap(NbtMapBuilder builder, T value);
public abstract T defaultValue();
public static DialogInput<?> read(GeyserSession session, NbtMap tag) {
Key type = MinecraftKey.key(tag.getString("type"));
if (type.equals(BooleanInput.TYPE)) {
return new BooleanInput(session, tag);
}
throw new UnsupportedOperationException("Unknown dialog input type " + type);
}
}

View File

@@ -0,0 +1,78 @@
/*
* 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 org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.geysermc.cumulus.form.CustomForm;
import org.geysermc.cumulus.response.CustomFormResponse;
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);
private final Map<DialogInput<?>, Object> values = new LinkedHashMap<>();
public ParsedInputs(List<DialogInput<?>> inputs, CustomFormResponse response) {
for (DialogInput<?> input : inputs) {
values.put(input, input.read(response));
}
}
public ParsedInputs(List<DialogInput<?>> inputs) {
for (DialogInput<?> input : inputs) {
values.put(input, input.defaultValue());
}
}
public void restore(CustomForm.Builder builder) {
for (Map.Entry<DialogInput<?>, Object> entry : values.entrySet()) {
// Can't be a Geyser update without eclipse dealing with generics
((DialogInput) entry.getKey()).addComponent(builder, Optional.of(entry.getValue()));
}
}
public String getSubstitution(String key) {
for (Map.Entry<DialogInput<?>, Object> entry : values.entrySet()) {
if (entry.getKey().key.equals(key)) {
return ((DialogInput) entry.getKey()).asSubstitution(entry.getValue());
}
}
return ""; // Java defaults to empty strings when a key was not in the inputs
}
public NbtMap asNbtMap() {
NbtMapBuilder builder = NbtMap.builder();
for (Map.Entry<DialogInput<?>, Object> entry : values.entrySet()) {
((DialogInput) entry.getKey()).addToMap(builder, entry.getValue());
}
return builder.build();
}
}