From 3efaf477c4aa4012e10a8a1943ddc0ca89e36dae Mon Sep 17 00:00:00 2001 From: Md5Lukas Date: Sat, 24 May 2025 19:16:23 +0000 Subject: [PATCH] Add API for client-side signs (#11903) --- .../packet/UncheckedSignChangeEvent.java | 94 +++++++++++++++++++ .../main/java/org/bukkit/entity/Player.java | 17 +++- .../ServerGamePacketListenerImpl.java.patch | 16 +++- .../craftbukkit/entity/CraftPlayer.java | 10 ++ 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 paper-api/src/main/java/io/papermc/paper/event/packet/UncheckedSignChangeEvent.java diff --git a/paper-api/src/main/java/io/papermc/paper/event/packet/UncheckedSignChangeEvent.java b/paper-api/src/main/java/io/papermc/paper/event/packet/UncheckedSignChangeEvent.java new file mode 100644 index 0000000000..e36aeb1967 --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/event/packet/UncheckedSignChangeEvent.java @@ -0,0 +1,94 @@ +package io.papermc.paper.event.packet; + +import io.papermc.paper.math.BlockPosition; +import io.papermc.paper.math.Position; +import java.util.Collections; +import java.util.List; +import net.kyori.adventure.text.Component; +import org.bukkit.block.sign.Side; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; +import org.bukkit.event.player.PlayerEvent; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Unmodifiable; +import org.jspecify.annotations.NullMarked; + +/** + * Called when a client attempts to modify a sign, but the location at which the sign should be edited + * has not yet been checked for the existence of a real sign. + *

+ * Cancelling this event will prevent further processing of the sign change, but needs further handling + * by the plugin as the client's local world might be in an inconsistent state. + * + * @see Player#openVirtualSign(Position, Side) + */ +@NullMarked +@ApiStatus.Experimental +public class UncheckedSignChangeEvent extends PlayerEvent implements Cancellable { + + private static final HandlerList HANDLER_LIST = new HandlerList(); + private boolean cancel = false; + private final BlockPosition editedBlockPosition; + private final Side side; + private final List lines; + + @ApiStatus.Internal + public UncheckedSignChangeEvent( + final Player editor, + final BlockPosition editedBlockPosition, + final Side side, + final List lines + ) { + super(editor); + this.editedBlockPosition = editedBlockPosition; + this.side = side; + this.lines = lines; + } + + /** + * Gets the location at which a potential sign was edited. + * + * @return location where the change happened + */ + public BlockPosition getEditedBlockPosition() { + return editedBlockPosition; + } + + /** + * Gets which side of the sign was edited. + * + * @return {@link Side} that was edited + */ + public Side getSide() { + return side; + } + + /** + * Gets the lines that the player has entered. + * + * @return the lines + */ + public @Unmodifiable List lines() { + return Collections.unmodifiableList(lines); + } + + @Override + public boolean isCancelled() { + return cancel; + } + + @Override + public void setCancelled(final boolean cancel) { + this.cancel = cancel; + } + + @Override + public HandlerList getHandlers() { + return HANDLER_LIST; + } + + public static HandlerList getHandlerList() { + return HANDLER_LIST; + } +} diff --git a/paper-api/src/main/java/org/bukkit/entity/Player.java b/paper-api/src/main/java/org/bukkit/entity/Player.java index c3dfe3471d..d34419693f 100644 --- a/paper-api/src/main/java/org/bukkit/entity/Player.java +++ b/paper-api/src/main/java/org/bukkit/entity/Player.java @@ -12,6 +12,7 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; import io.papermc.paper.entity.LookAnchor; import io.papermc.paper.entity.PlayerGiveResult; +import io.papermc.paper.math.Position; import org.bukkit.BanEntry; import org.bukkit.DyeColor; import org.bukkit.Effect; @@ -52,7 +53,6 @@ import org.bukkit.plugin.Plugin; import org.bukkit.plugin.messaging.PluginMessageRecipient; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; -import org.bukkit.profile.PlayerProfile; import org.bukkit.scoreboard.Scoreboard; import org.jetbrains.annotations.ApiStatus; import org.jspecify.annotations.NullMarked; @@ -3449,6 +3449,21 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM */ public void openSign(Sign sign, Side side); + /** + * Open a sign for editing by the player. + *

+ * The sign must only be placed locally for the player, which can be done with {@link #sendBlockChange(Location, BlockData)} and {@link #sendBlockUpdate(Location, TileState)}. + * A side-effect of this is that normal events, like {@link org.bukkit.event.block.SignChangeEvent} will not be called (unless there is an actual sign in the world). + * Additionally, the client may enforce distance limits to the opened position. + *

+ * + * @param block The block where the client has a sign placed + * @param side The side to edit + * @see io.papermc.paper.event.packet.UncheckedSignChangeEvent + */ + @ApiStatus.Experimental + void openVirtualSign(Position block, Side side); + /** * Shows the demo screen to the player, this screen is normally only seen in * the demo version of the game. diff --git a/paper-server/patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch b/paper-server/patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch index 0b712a82ba..4ae0a3e926 100644 --- a/paper-server/patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch @@ -2486,7 +2486,7 @@ } else if (flag && flag2) { if (this.dropSpamThrottler.isUnderThreshold()) { this.dropSpamThrottler.increment(); -@@ -1895,11 +_,24 @@ +@@ -1895,15 +_,38 @@ @Override public void handleSignUpdate(ServerboundSignUpdatePacket packet) { @@ -2512,6 +2512,20 @@ this.player.resetLastActionTime(); ServerLevel serverLevel = this.player.serverLevel(); BlockPos pos = packet.getPos(); + if (serverLevel.hasChunkAt(pos)) { ++ // Paper start - Add API for client-side signs ++ if (!new io.papermc.paper.event.packet.UncheckedSignChangeEvent( ++ this.player.getBukkitEntity(), ++ io.papermc.paper.util.MCUtil.toPosition(pos), ++ packet.isFrontText() ? org.bukkit.block.sign.Side.FRONT : org.bukkit.block.sign.Side.BACK, ++ filteredText.stream().map(line -> net.kyori.adventure.text.Component.text(line.raw())).toList()) ++ .callEvent()) { ++ return; ++ } ++ // Paper end - Add API for client-side signs + if (!(serverLevel.getBlockEntity(pos) instanceof SignBlockEntity signBlockEntity)) { + return; + } @@ -1915,14 +_,32 @@ @Override public void handlePlayerAbilities(ServerboundPlayerAbilitiesPacket packet) { diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java index 8a1e2785d4..1db6276ae7 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -12,6 +12,8 @@ import io.papermc.paper.configuration.GlobalConfiguration; import io.papermc.paper.entity.LookAnchor; import io.papermc.paper.entity.PaperPlayerGiveResult; import io.papermc.paper.entity.PlayerGiveResult; +import io.papermc.paper.math.Position; +import io.papermc.paper.util.MCUtil; import it.unimi.dsi.fastutil.shorts.ShortArraySet; import it.unimi.dsi.fastutil.shorts.ShortSet; import java.io.ByteArrayOutputStream; @@ -73,6 +75,7 @@ import net.minecraft.network.protocol.game.ClientboundHurtAnimationPacket; import net.minecraft.network.protocol.game.ClientboundLevelEventPacket; import net.minecraft.network.protocol.game.ClientboundLevelParticlesPacket; import net.minecraft.network.protocol.game.ClientboundMapItemDataPacket; +import net.minecraft.network.protocol.game.ClientboundOpenSignEditorPacket; import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; import net.minecraft.network.protocol.game.ClientboundRemoveMobEffectPacket; @@ -3037,6 +3040,13 @@ public class CraftPlayer extends CraftHumanEntity implements Player { CraftSign.openSign(sign, this, side); } + @Override + public void openVirtualSign(Position block, Side side) { + if (this.getHandle().connection == null) return; + + this.getHandle().connection.send(new ClientboundOpenSignEditorPacket(MCUtil.toBlockPos(block), side == Side.FRONT)); + } + @Override public void showDemoScreen() { if (this.getHandle().connection == null) return;