diff --git a/paper-api/src/main/java/io/papermc/paper/event/connection/configuration/PlayerDialogsReceiveEvent.java b/paper-api/src/main/java/io/papermc/paper/event/connection/configuration/PlayerDialogsReceiveEvent.java new file mode 100644 index 000000000000..8ff0a3d95c6e --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/event/connection/configuration/PlayerDialogsReceiveEvent.java @@ -0,0 +1,87 @@ +package io.papermc.paper.event.connection.configuration; + +import io.papermc.paper.connection.PlayerConfigurationConnection; +import java.util.Optional; +import net.kyori.adventure.dialog.DialogLike; +import org.bukkit.Bukkit; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +/** + * This Event allows for setting a custom {@code quick_action} and {@code pause_menu_additions} + * {@link io.papermc.paper.dialog.Dialog} for each player. + *

+ * This is executed once, when the player joins. + */ +public class PlayerDialogsReceiveEvent extends Event { + + private static final HandlerList HANDLER_LIST = new HandlerList(); + + private final PlayerConfigurationConnection connection; + private @Nullable DialogLike quickAction; + private @Nullable DialogLike pauseMenuAdditions; + + @ApiStatus.Internal + public PlayerDialogsReceiveEvent(final PlayerConfigurationConnection connection) { + super(!Bukkit.isPrimaryThread()); + this.connection = connection; + } + + /** + * Gets the players Connection who will receive these Dialogs + * + * @return The ConfigurationConnection + */ + public PlayerConfigurationConnection getConnection() { + return this.connection; + } + + /** + * Sets the Quick Action Dialog, standard button to open this is G + * + * @param quickAction The dialog, or null to unset it + */ + public void setQuickAction(DialogLike quickAction) { + this.quickAction = quickAction; + } + + + /** + * Sets the Pause Menu Additions Dialog, which is viewable in the Esc menu + * + * @param pauseMenuAdditions The dialog, or null to unset it + */ + public void setPauseMenuAdditions(DialogLike pauseMenuAdditions) { + this.pauseMenuAdditions = pauseMenuAdditions; + } + + /** + * Gets the currently set Quick Actions Dialog + * + * @return the Dialog, or Optional.empty() if its not set + */ + public Optional getQuickAction() { + return Optional.ofNullable(quickAction); + } + + + /** + * Gets the currently set Pause Menu Additions Dialog + * + * @return the Dialog, or Optional.empty() if its not set + */ + public Optional getPauseMenuAdditions() { + return Optional.ofNullable(pauseMenuAdditions); + } + + @Override + public HandlerList getHandlers() { + return HANDLER_LIST; + } + + public static HandlerList getHandlerList() { + return HANDLER_LIST; + } +} diff --git a/paper-server/patches/sources/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch b/paper-server/patches/sources/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch index cd94a1842b79..29fd442dd80f 100644 --- a/paper-server/patches/sources/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch @@ -87,6 +87,108 @@ this.finishCurrentTask(ServerResourcePackConfigurationTask.TYPE); } } +@@ -146,11 +_,100 @@ + if (this.synchronizeRegistriesTask == null) { + throw new IllegalStateException("Unexpected response from client: received pack selection, but no negotiation ongoing"); + } ++ // Paper start - Per Player Quick Action & Pause Menu Dialogs ++ var dialogEvent = new io.papermc.paper.event.connection.configuration.PlayerDialogsReceiveEvent(paperConnection); ++ dialogEvent.callEvent(); ++ if (dialogEvent.getPauseMenuAdditions().isPresent() || dialogEvent.getQuickAction().isPresent()) { ++ this.synchronizeRegistriesTask.handleResponse(packet.knownPacks(), p -> spoofPacketForPerPlayerDialogs(p, dialogEvent)); ++ } else { ++ // Vanilla ++ this.synchronizeRegistriesTask.handleResponse(packet.knownPacks(), this::send); ++ } ++ // Paper end - Per Player Quick Action & Pause Menu Dialogs + +- this.synchronizeRegistriesTask.handleResponse(packet.knownPacks(), this::send); + this.finishCurrentTask(SynchronizeRegistriesTask.TYPE); + } + ++ // Paper start - Per Player Quick Action & Pause Menu Dialogs ++ private void spoofPacketForPerPlayerDialogs(final net.minecraft.network.protocol.Packet packet, io.papermc.paper.event.connection.configuration.PlayerDialogsReceiveEvent dialogEvent) { ++ switch (packet) { ++ case net.minecraft.network.protocol.configuration.ClientboundRegistryDataPacket dataPacket -> { ++ // spoof the dialog quick_actions and custom_options for quick actions and pause menu add ++ // I'd rather use a new custom dialog but I couldn't find out how to add a new one. ++ var dialogRegistry = net.minecraft.core.registries.Registries.DIALOG; ++ if (dataPacket.registry().equals(dialogRegistry)) { ++ dialogEvent.getQuickAction().ifPresent(spoofDialog(net.minecraft.server.dialog.Dialogs.QUICK_ACTIONS, dataPacket)); ++ dialogEvent.getPauseMenuAdditions().ifPresent(spoofDialog(net.minecraft.server.dialog.Dialogs.CUSTOM_OPTIONS, dataPacket)); ++ } ++ } ++ case net.minecraft.network.protocol.common.ClientboundUpdateTagsPacket tagsPacket -> { ++ // spoof the quick_actions and pause_menu_additions Tag so it includes the dialog ++ var registry = net.minecraft.core.registries.Registries.DIALOG; ++ tagsPacket.getTags().compute(registry, (_, old) -> { ++ var registryAccess = this.server.registries().compositeAccess().lookup(registry).get(); ++ var map = new java.util.HashMap(); ++ // if old data exists keep it ++ if (old != null) { ++ var tags = old.resolve(registryAccess).tags(); ++ tags.forEach((key, val) -> { ++ map.put(key.location(), val.stream().map(holder -> registryAccess.getId(holder.value())).collect(java.util.stream.Collectors.toCollection(it.unimi.dsi.fastutil.ints.IntArrayList::new))); ++ }); ++ } ++ // replace the Tags with the dialogs we want ++ if (dialogEvent.getQuickAction().isPresent()) { ++ map.put(net.minecraft.tags.DialogTags.QUICK_ACTIONS.location(), dialogToIdList(registryAccess, net.minecraft.server.dialog.Dialogs.QUICK_ACTIONS.identifier())); ++ } ++ if (dialogEvent.getPauseMenuAdditions().isPresent()) { ++ map.put(net.minecraft.tags.DialogTags.PAUSE_SCREEN_ADDITIONS.location(), dialogToIdList(registryAccess, net.minecraft.server.dialog.Dialogs.CUSTOM_OPTIONS.identifier())); ++ } ++ return new net.minecraft.tags.TagNetworkSerialization.NetworkPayload(map); ++ }); ++ } ++ default -> { ++ } ++ } ++ this.send(packet); ++ } ++ ++ private static it.unimi.dsi.fastutil.ints.IntList dialogToIdList( ++ net.minecraft.core.Registry registryAccess, ++ net.minecraft.resources.Identifier identifier ++ ) { ++ return it.unimi.dsi.fastutil.ints.IntList.of(registryAccess.getId(registryAccess.getValue(identifier))); ++ } ++ ++ private java.util.function.Consumer spoofDialog( ++ net.minecraft.resources.ResourceKey dialogKey, ++ net.minecraft.network.protocol.configuration.ClientboundRegistryDataPacket dataPacket ++ ) { ++ var registry = net.minecraft.core.registries.Registries.DIALOG; ++ var ops = this.server.registries().compositeAccess().createSerializationContext(net.minecraft.nbt.NbtOps.INSTANCE); ++ var registryData = new net.minecraft.resources.RegistryDataLoader.RegistryData<>(registry, net.minecraft.server.dialog.Dialog.DIRECT_CODEC, net.minecraft.resources.RegistryValidator.none()); ++ return dialog -> { ++ // get index so that the network Id stays the same ++ var identifier = dialogKey.identifier(); ++ int index = 0; ++ for (var entry : dataPacket.entries()) { ++ if (entry.id().equals(identifier)) { ++ break; ++ } ++ ++index; ++ } ++ // remove the element ++ dataPacket.entries().remove(index); ++ ++ var paperDialog = io.papermc.paper.dialog.PaperDialog.bukkitToMinecraftHolder((io.papermc.paper.dialog.Dialog) dialog); ++ // parse dialog to, see net.minecraft.core.RegistrySynchronization#packRegistry ++ net.minecraft.nbt.Tag encodedElement = registryData.elementCodec() ++ .encodeStart(ops, paperDialog.value()) ++ .getOrThrow(s -> new IllegalArgumentException("Failed to serialize " + identifier + ": " + s)); ++ var optional = java.util.Optional.of(encodedElement); ++ // readd the element to the list at the same position ++ dataPacket.entries().add(index, new net.minecraft.core.RegistrySynchronization.PackedRegistryEntry(identifier, optional)); ++ }; ++ } ++ // Paper end - Per Player Quick Action & Pause Menu Dialogs ++ + @Override + public void handleAcceptCodeOfConduct(final ServerboundAcceptCodeOfConductPacket packet) { + this.finishCurrentTask(ServerCodeOfConductConfigurationTask.TYPE); @@ -169,7 +_,7 @@ return; } diff --git a/paper-server/patches/sources/net/minecraft/tags/TagNetworkSerialization.java.patch b/paper-server/patches/sources/net/minecraft/tags/TagNetworkSerialization.java.patch new file mode 100644 index 000000000000..e219e8d0a114 --- /dev/null +++ b/paper-server/patches/sources/net/minecraft/tags/TagNetworkSerialization.java.patch @@ -0,0 +1,12 @@ +--- a/net/minecraft/tags/TagNetworkSerialization.java ++++ b/net/minecraft/tags/TagNetworkSerialization.java +@@ -60,7 +_,8 @@ + public static final TagNetworkSerialization.NetworkPayload EMPTY = new TagNetworkSerialization.NetworkPayload(Map.of()); + private final Map tags; + +- NetworkPayload(final Map tags) { ++ // Paper public for Per Player Quick Action & Pause Menu Dialogs ++ public NetworkPayload(final Map tags) { + this.tags = tags; + } +