1
0
mirror of https://github.com/GeyserMC/Geyser.git synced 2025-12-30 20:29:19 +00:00

Fix various things relating to closing and switching of dialogs; implement 2 after-actions; implement exit button on button list dialogs; some cleanup

This commit is contained in:
Eclipse
2025-06-04 16:49:21 +00:00
parent 71cf2ce254
commit 2bb9cf664d
12 changed files with 270 additions and 61 deletions

View File

@@ -176,6 +176,7 @@ import org.geysermc.geyser.session.cache.TeleportCache;
import org.geysermc.geyser.session.cache.WorldBorder;
import org.geysermc.geyser.session.cache.WorldCache;
import org.geysermc.geyser.session.cache.registry.JavaRegistries;
import org.geysermc.geyser.session.dialog.DialogManager;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
import org.geysermc.geyser.translator.text.MessageTranslator;
@@ -307,6 +308,9 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter
private @Nullable InventoryHolder<? extends Inventory> inventoryHolder;
@Getter
private final DialogManager dialogManager = new DialogManager(this);
/**
* Whether the client is currently closing an inventory.
* Used to open new inventories while another one is currently open.
@@ -1515,6 +1519,19 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Override
public boolean sendForm(@NonNull Form form) {
// First close any dialogs that are open. This won't execute the dialog's closing action.
dialogManager.close();
return doSendForm(form);
}
/**
* Sends a form without first closing any open dialog. This should only be used by {@link org.geysermc.geyser.session.dialog.Dialog}s.
*/
public boolean sendDialogForm(@NonNull Form form) {
return doSendForm(form);
}
private boolean doSendForm(@NonNull Form form) {
formCache.showForm(form);
return true;
}

View File

@@ -37,11 +37,6 @@ public class ConfirmationDialog extends DialogWithButtons {
public static final Key TYPE = MinecraftKey.key("confirmation");
public ConfirmationDialog(GeyserSession session, NbtMap map, IdGetter idGetter) {
super(session, map, parseOptionalList(DialogButton.read(session, map.get("yes"), idGetter), DialogButton.read(session, map.get("no"), idGetter)));
}
@Override
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
super(session, map, parseOptionalList(DialogButton.read(session, map.get("yes"), idGetter), DialogButton.read(session, map.get("no"), idGetter)), Optional.empty());
}
}

View File

@@ -25,7 +25,10 @@
package org.geysermc.geyser.session.dialog;
import lombok.Getter;
import lombok.experimental.Accessors;
import net.kyori.adventure.key.Key;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtType;
import org.geysermc.cumulus.form.CustomForm;
@@ -50,12 +53,16 @@ import java.util.Objects;
import java.util.Optional;
import java.util.function.ToIntFunction;
@Accessors(fluent = true)
public abstract class Dialog {
private static final Key PLAIN_MESSAGE_BODY = MinecraftKey.key("plain_message");
private final String title;
private final String externalTitle;
@Getter
private final boolean canCloseWithEscape;
@Getter
private final AfterAction afterAction;
private final List<String> labels;
private final List<DialogInput<?>> inputs = new ArrayList<>();
@@ -63,6 +70,7 @@ public abstract class Dialog {
protected Dialog(GeyserSession session, NbtMap map) {
title = MessageTranslator.convertFromNullableNbtTag(session, map.get("title"));
externalTitle = MessageTranslator.convertFromNullableNbtTag(session, map.get("title"));
canCloseWithEscape = map.getBoolean("can_close_with_escape", true);
afterAction = AfterAction.fromString(map.getString("after_action"));
Object bodyTag = map.get("body");
@@ -100,42 +108,42 @@ public abstract class Dialog {
protected abstract Optional<DialogButton> onCancel();
protected FormBuilder<? extends FormBuilder<?,?,?>, ? extends Form, ? extends FormResponse> createForm(GeyserSession session) {
protected FormBuilder<? extends FormBuilder<?,?,?>, ? extends Form, ? extends FormResponse> createForm(GeyserSession session, Optional<ParsedInputs> restored, DialogHolder holder) {
if (inputs.isEmpty()) {
SimpleForm.Builder builder = SimpleForm.builder()
.title(title);
builder.content(String.join("\n", labels));
builder.content(String.join("\n\n", labels));
builder.closedOrInvalidResultHandler(() -> runButton(session, onCancel(), ParsedInputs.EMPTY));
addCustomComponents(session, builder);
builder.closedOrInvalidResultHandler(() -> holder.closeDialog(onCancel()));
addCustomComponents(session, builder, holder);
return builder;
} else {
CustomForm.Builder builder = CustomForm.builder()
.title(title);
inputs.forEach(input -> input.addComponent(builder, Optional.empty()));
builder.closedOrInvalidResultHandler(response -> runButton(session, onCancel(), new ParsedInputs(inputs)));
addCustomComponents(session, builder);
restored.ifPresentOrElse(last -> last.restore(builder), () -> inputs.forEach(input -> input.addComponent(builder)));
builder.closedOrInvalidResultHandler(response -> holder.closeDialog(onCancel()));
addCustomComponents(session, builder, holder);
return builder;
}
}
protected abstract void addCustomComponents(GeyserSession session, CustomForm.Builder builder);
protected abstract void addCustomComponents(GeyserSession session, CustomForm.Builder builder, DialogHolder holder);
protected abstract void addCustomComponents(GeyserSession session, SimpleForm.Builder builder);
protected abstract void addCustomComponents(GeyserSession session, SimpleForm.Builder builder, DialogHolder holder);
public Form buildForm(GeyserSession session) {
return createForm(session).build();
public void sendForm(GeyserSession session, DialogHolder holder) {
session.sendDialogForm(createForm(session, Optional.empty(), holder).build());
}
public void restoreForm(GeyserSession session, @NonNull ParsedInputs inputs, DialogHolder holder) {
session.sendDialogForm(createForm(session, Optional.of(inputs), holder).build());
}
protected ParsedInputs parseInput(CustomFormResponse response) {
return new ParsedInputs(inputs, response);
}
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) {
return readDialogFromNbt(context.session(), context.data(), context::getNetworkId);
}
@@ -154,10 +162,10 @@ public abstract class Dialog {
return new Dialog(session, map) {
@Override
protected void addCustomComponents(GeyserSession session, CustomForm.Builder builder) {}
protected void addCustomComponents(GeyserSession session, CustomForm.Builder builder, DialogHolder holder) {}
@Override
protected void addCustomComponents(GeyserSession session, SimpleForm.Builder builder) {}
protected void addCustomComponents(GeyserSession session, SimpleForm.Builder builder, DialogHolder holder) {}
@Override
protected Optional<DialogButton> onCancel() {
@@ -167,14 +175,12 @@ public abstract class Dialog {
// throw new UnsupportedOperationException("Unable to read unknown dialog type " + type + "!"); // TODO put this here once all types are implemented
}
public static void showDialog(GeyserSession session, Holder<NbtMap> holder) {
Dialog dialog;
public static Dialog getDialogFromHolder(GeyserSession session, Holder<NbtMap> holder) {
if (holder.isId()) {
dialog = JavaRegistries.DIALOG.fromNetworkId(session, holder.id());
return Objects.requireNonNull(JavaRegistries.DIALOG.fromNetworkId(session, holder.id()));
} else {
dialog = Dialog.readDialogFromNbt(session, holder.custom(), key -> JavaRegistries.DIALOG.keyToNetworkId(session, key));
return Dialog.readDialogFromNbt(session, holder.custom(), key -> JavaRegistries.DIALOG.keyToNetworkId(session, key));
}
session.sendForm(Objects.requireNonNull(dialog).buildForm(session));
}
public enum AfterAction {
@@ -188,7 +194,7 @@ public abstract class Dialog {
return action;
}
}
return null;
return CLOSE;
}
}

View File

@@ -0,0 +1,112 @@
/*
* 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;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.session.dialog.action.DialogAction;
import org.geysermc.geyser.session.dialog.input.ParsedInputs;
import java.util.Optional;
public class DialogHolder {
private final DialogManager manager;
private final Dialog dialog;
private ParsedInputs lastInputs;
public DialogHolder(DialogManager manager, Dialog dialog) {
this.manager = manager;
this.dialog = dialog;
}
// Dialog action behaviour:
// When running an action, if the action:
// - opens a new dialog: the new dialog is opened, the old one is closed, its closing action not executed. [ ]
// - executes with "none" as after action: the dialog is kept open. The dialog can only be closed by pressing "escape" (when allowed by the dialog), or on bedrock, the X in the corner of the form. [ ] (TODO note about multi action exit)
// - executes with "close" as after action: the dialog is closed, its closing action not executed.
// A dialogs closing action (onCancel()) is only executed when the dialog is closed by pressing "escape" (when allowed by the dialog). [ ]
// If a new dialog is opened by the server when a dialog is already open, that dialog is closed, its closing action not executed. [ ]
public void runButton(Optional<DialogButton> button, @NonNull ParsedInputs inputs) {
lastInputs = inputs;
boolean stillValid = runAction(button, lastInputs);
switch (dialog.afterAction()) {
case NONE -> {
// If no new dialog was opened, reopen this one
if (stillValid) {
dialog.restoreForm(manager.session(), lastInputs, this);
}
}
case CLOSE -> {
// If no new dialog was opened, tell the manager this one is now closed
if (stillValid) {
manager.close();
}
}
case WAIT_FOR_RESPONSE -> {} // TODO
}
}
// Called when clicking the X in the corner on a form, which we interpret as clicking escape
// Note that this method is called from the "closedOrInvalidResultHandler",
// meaning it can also be called when e.g. the bedrock client opens another form or is unable to open the form sent to it
public void closeDialog(Optional<DialogButton> onCancel) {
if (dialog.canCloseWithEscape()) {
if (runAction(onCancel, lastInputs == null ? ParsedInputs.EMPTY : lastInputs)) {
manager.close();
}
} else if (manager.open() == this) { // Check if this is still the currently open dialog
// If player should not have been able to close the dialog, reopen it with the last inputs
// lastInputs might be null here since it's possible none were sent yet, and bedrock doesn't send them when just closing the form
if (lastInputs == null) {
dialog.sendForm(manager.session(), this);
} else {
dialog.restoreForm(manager.session(), lastInputs, this);
}
}
}
// Returns true if this dialog is still regarded open by the DialogManager
// When returning false, that means a new dialog has been opened, possibly by the action, which takes this dialog's place
private boolean runAction(Optional<DialogButton> button, ParsedInputs inputs) {
// Don't run any action if a new dialog has already been opened
if (manager.open() != this) {
return false;
}
DialogAction action = button.flatMap(DialogButton::action).orElse(null);
if (action != null) {
action.run(manager.session(), inputs);
if (action instanceof DialogAction.ShowDialog) {
return false;
}
// TODO command warning screen
}
return true;
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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;
import lombok.Getter;
import lombok.experimental.Accessors;
import org.cloudburstmc.nbt.NbtMap;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.Holder;
/**
* Small class to manage the currently open dialog.
*/
@Accessors(fluent = true)
public class DialogManager {
@Getter
private final GeyserSession session;
@Getter
private DialogHolder open;
public DialogManager(GeyserSession session) {
this.session = session;
}
public void openDialog(Holder<NbtMap> dialog) {
openDialog(Dialog.getDialogFromHolder(session, dialog));
}
/**
* Opens a new dialog. If a dialog was already open, this one will be closed. Its closing action will not be executed. This matches Java behaviour.
*/
public void openDialog(Dialog dialog) {
open = new DialogHolder(this, dialog);
session.closeForm();
dialog.sendForm(session, open);
}
/**
* Closes the currently open dialog, if any. The dialog's closing action will not be executed.
*/
public void close() {
if (open != null) {
open = null;
// The form could already have been closed by now, but in the case it wasn't, close it anyway
// This won't run a closing dialog action, because the manager already regards the dialog as closed
session.closeForm();
}
}
}

View File

@@ -39,35 +39,55 @@ import java.util.Optional;
public abstract class DialogWithButtons extends Dialog {
protected final List<DialogButton> buttons;
protected final Optional<DialogButton> exitAction;
protected DialogWithButtons(GeyserSession session, NbtMap map, List<DialogButton> buttons) {
protected DialogWithButtons(GeyserSession session, NbtMap map, List<DialogButton> buttons, Optional<DialogButton> exitAction) {
super(session, map);
this.buttons = buttons;
this.exitAction = exitAction;
}
@Override
protected void addCustomComponents(GeyserSession session, CustomForm.Builder builder) {
protected void addCustomComponents(GeyserSession session, CustomForm.Builder builder, DialogHolder holder) {
DropdownComponent.Builder dropdown = DropdownComponent.builder();
dropdown.text("Please select an option:");
for (DialogButton button : buttons) {
dropdown.option(button.label());
}
exitAction.ifPresent(button -> dropdown.option(button.label()));
builder.dropdown(dropdown);
builder.validResultHandler(response -> {
ParsedInputs inputs = parseInput(response);
int selection = response.asDropdown();
runButton(session, Optional.of(buttons.get(selection)), inputs);
if (selection == buttons.size()) {
holder.runButton(exitAction, inputs);
} else {
holder.runButton(Optional.of(buttons.get(selection)), inputs);
}
});
}
@Override
protected void addCustomComponents(GeyserSession session, SimpleForm.Builder builder) {
protected void addCustomComponents(GeyserSession session, SimpleForm.Builder builder, DialogHolder holder) {
for (DialogButton button : buttons) {
builder.button(button.label());
}
exitAction.ifPresent(button -> builder.button(button.label()));
builder.validResultHandler(response -> runButton(session, Optional.of(buttons.get(response.clickedButtonId())), ParsedInputs.EMPTY));
builder.validResultHandler(response -> {
if (response.clickedButtonId() == buttons.size()) {
holder.runButton(exitAction, ParsedInputs.EMPTY);
} else {
holder.runButton(Optional.of(buttons.get(response.clickedButtonId())), ParsedInputs.EMPTY);
}
});
}
@Override
protected Optional<DialogButton> onCancel() {
return exitAction;
}
@SafeVarargs

View File

@@ -31,21 +31,11 @@ import org.cloudburstmc.nbt.NbtType;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.MinecraftKey;
import java.util.Optional;
public class MultiActionDialog extends DialogWithButtons {
public static final Key TYPE = MinecraftKey.key("multi_action");
private final Optional<DialogButton> exit;
protected MultiActionDialog(GeyserSession session, NbtMap map, IdGetter idGetter) {
super(session, map, DialogButton.readList(session, map.getList("actions", NbtType.COMPOUND), idGetter));
exit = DialogButton.read(session, map.get("exit_action"), idGetter);
}
@Override
protected Optional<DialogButton> onCancel() {
return exit;
super(session, map, DialogButton.readList(session, map.getList("actions", NbtType.COMPOUND), idGetter), DialogButton.read(session, map.get("exit_action"), idGetter));
}
}

View File

@@ -52,12 +52,12 @@ public class NoticeDialog extends Dialog {
}
@Override
protected void addCustomComponents(GeyserSession session, CustomForm.Builder builder) {
builder.validResultHandler(response -> runButton(session, button, parseInput(response)));
protected void addCustomComponents(GeyserSession session, CustomForm.Builder builder, DialogHolder holder) {
builder.validResultHandler(response -> holder.runButton(onCancel(), parseInput(response)));
}
@Override
protected void addCustomComponents(GeyserSession session, SimpleForm.Builder builder) {
builder.validResultHandler(response -> runButton(session, button, ParsedInputs.EMPTY));
protected void addCustomComponents(GeyserSession session, SimpleForm.Builder builder, DialogHolder holder) {
builder.validResultHandler(response -> holder.runButton(onCancel(), ParsedInputs.EMPTY));
}
}

View File

@@ -34,11 +34,6 @@ import java.util.Optional;
public class ServerLinksDialog extends DialogWithButtons {
protected ServerLinksDialog(GeyserSession session, NbtMap map, List<DialogButton> buttons) {
super(session, map, buttons);
}
@Override
protected Optional<DialogButton> onCancel() {
return Optional.empty();
super(session, map, buttons, Optional.empty());
}
}

View File

@@ -28,7 +28,6 @@ package org.geysermc.geyser.session.dialog.action;
import net.kyori.adventure.key.Key;
import org.checkerframework.checker.nullness.qual.Nullable;
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;
@@ -76,7 +75,8 @@ public interface DialogAction {
@Override
public void run(GeyserSession session, ParsedInputs inputs) {
session.sendForm(SimpleForm.builder().title("Open URL").content(url));
//session.sendForm(SimpleForm.builder().title("Open URL").content(url));
// TODO if we use form here we need a connection in dialog stack
}
}
@@ -105,8 +105,7 @@ public interface DialogAction {
@Override
public void run(GeyserSession session, ParsedInputs inputs) {
// TODO figure out parent dialog
Dialog.showDialog(session, dialog);
session.getDialogManager().openDialog(dialog);
}
}

View File

@@ -45,6 +45,10 @@ public abstract class DialogInput<T> {
this.label = MessageTranslator.convertFromNullableNbtTag(session, map.get("label"));
}
public void addComponent(CustomForm.Builder builder) {
addComponent(builder, Optional.empty());
}
public abstract void addComponent(CustomForm.Builder builder, Optional<T> restored);
public abstract T read(CustomFormResponse response);

View File

@@ -26,7 +26,6 @@
package org.geysermc.geyser.translator.protocol.java.dialogues;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.dialog.Dialog;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundShowDialogGamePacket;
@@ -36,6 +35,6 @@ public class JavaShowDialogGameTranslator extends PacketTranslator<ClientboundSh
@Override
public void translate(GeyserSession session, ClientboundShowDialogGamePacket packet) {
Dialog.showDialog(session, packet.getDialog());
session.getDialogManager().openDialog(packet.getDialog());
}
}