--- a/net/minecraft/world/level/block/entity/SignBlockEntity.java
+++ b/net/minecraft/world/level/block/entity/SignBlockEntity.java
@@ -55,10 +_,15 @@
     }
 
     public boolean isFacingFrontText(Player player) {
+        // Paper start - More Sign Block API
+        return this.isFacingFrontText(player.getX(), player.getZ());
+    }
+    public boolean isFacingFrontText(double x, double z) {
+        // Paper end - More Sign Block API
         if (this.getBlockState().getBlock() instanceof SignBlock signBlock) {
             Vec3 signHitboxCenterPosition = signBlock.getSignHitboxCenterPosition(this.getBlockState());
-            double d = player.getX() - (this.getBlockPos().getX() + signHitboxCenterPosition.x);
-            double d1 = player.getZ() - (this.getBlockPos().getZ() + signHitboxCenterPosition.z);
+            double d = x - (this.getBlockPos().getX() + signHitboxCenterPosition.x); // Paper - More Sign Block API
+            double d1 = z - (this.getBlockPos().getZ() + signHitboxCenterPosition.z); // Paper - More Sign Block AP
             float yRotationDegrees = signBlock.getYRotationDegrees(this.getBlockState());
             float f = (float)(Mth.atan2(d1, d) * 180.0F / (float)Math.PI) - 90.0F;
             return Mth.degreesDifferenceAbs(yRotationDegrees, f) <= 90.0F;
@@ -143,11 +_,13 @@
 
     public void updateSignText(Player player, boolean isFrontText, List<FilteredText> filteredText) {
         if (!this.isWaxed() && player.getUUID().equals(this.getPlayerWhoMayEdit()) && this.level != null) {
-            this.updateText(signText -> this.setMessages(player, filteredText, signText), isFrontText);
+            this.updateText(signText -> this.setMessages(player, filteredText, signText, isFrontText), isFrontText); // CraftBukkit
             this.setAllowedPlayerEditor(null);
             this.level.sendBlockUpdated(this.getBlockPos(), this.getBlockState(), this.getBlockState(), 3);
         } else {
             LOGGER.warn("Player {} just tried to change non-editable sign", player.getName().getString());
+            if (player.distanceToSqr(this.getBlockPos().getX(), this.getBlockPos().getY(), this.getBlockPos().getZ()) < Mth.square(32)) // Paper - Don't send far away sign update
+            ((net.minecraft.server.level.ServerPlayer) player).connection.send(this.getUpdatePacket()); // CraftBukkit
         }
     }
 
@@ -156,18 +_,40 @@
         return this.setText(updater.apply(text), isFrontText);
     }
 
-    private SignText setMessages(Player player, List<FilteredText> filteredText, SignText text) {
+    private SignText setMessages(Player player, List<FilteredText> filteredText, SignText text, boolean front) { // CraftBukkit
+        SignText originalText = text; // CraftBukkit
         for (int i = 0; i < filteredText.size(); i++) {
             FilteredText filteredText1 = filteredText.get(i);
             Style style = text.getMessage(i, player.isTextFilteringEnabled()).getStyle();
             if (player.isTextFilteringEnabled()) {
-                text = text.setMessage(i, Component.literal(filteredText1.filteredOrEmpty()).setStyle(style));
+                text = text.setMessage(i, Component.literal(net.minecraft.util.StringUtil.filterText(filteredText1.filteredOrEmpty())).setStyle(style)); // Paper - filter sign text to chat only
             } else {
                 text = text.setMessage(
-                    i, Component.literal(filteredText1.raw()).setStyle(style), Component.literal(filteredText1.filteredOrEmpty()).setStyle(style)
+                    i, Component.literal(filteredText1.raw()).setStyle(style), Component.literal(net.minecraft.util.StringUtil.filterText(filteredText1.filteredOrEmpty())).setStyle(style) // Paper - filter sign text to chat only
                 );
             }
         }
+
+        // CraftBukkit start
+        org.bukkit.entity.Player apiPlayer = ((net.minecraft.server.level.ServerPlayer) player).getBukkitEntity();
+        List<net.kyori.adventure.text.Component> lines = new java.util.ArrayList<>(); // Paper - adventure
+
+        for (int i = 0; i < filteredText.size(); ++i) {
+            lines.add(io.papermc.paper.adventure.PaperAdventure.asAdventure(text.getMessage(i, player.isTextFilteringEnabled()))); // Paper - Adventure
+        }
+
+        org.bukkit.event.block.SignChangeEvent event = new org.bukkit.event.block.SignChangeEvent(org.bukkit.craftbukkit.block.CraftBlock.at(this.level, this.worldPosition), apiPlayer, new java.util.ArrayList<>(lines), (front) ? org.bukkit.block.sign.Side.FRONT : org.bukkit.block.sign.Side.BACK); // Paper - Adventure
+        if (!event.callEvent()) {
+            return originalText;
+        }
+
+        Component[] components = org.bukkit.craftbukkit.block.CraftSign.sanitizeLines(event.lines()); // Paper - Adventure
+        for (int i = 0; i < components.length; i++) {
+            if (!java.util.Objects.equals(lines.get(i), event.line(i))) { // Paper - Adventure
+                text = text.setMessage(i, components[i]);
+            }
+        }
+        // CraftBukkit end
 
         return text;
     }
@@ -207,7 +_,23 @@
             Style style = component.getStyle();
             ClickEvent clickEvent = style.getClickEvent();
             if (clickEvent != null && clickEvent.getAction() == ClickEvent.Action.RUN_COMMAND) {
-                player.getServer().getCommands().performPrefixedCommand(createCommandSourceStack(player, level, pos), clickEvent.getValue());
+                // Paper start - Fix commands from signs not firing command events
+                String command = clickEvent.getValue().startsWith("/") ? clickEvent.getValue() : "/" + clickEvent.getValue();
+                if (org.spigotmc.SpigotConfig.logCommands)  {
+                    LOGGER.info("{} issued server command: {}", player.getScoreboardName(), command);
+                }
+                io.papermc.paper.event.player.PlayerSignCommandPreprocessEvent event = new io.papermc.paper.event.player.PlayerSignCommandPreprocessEvent(
+                    (org.bukkit.entity.Player) player.getBukkitEntity(),
+                    command,
+                    new org.bukkit.craftbukkit.util.LazyPlayerSet(player.getServer()),
+                    (org.bukkit.block.Sign) org.bukkit.craftbukkit.block.CraftBlock.at(this.level, this.worldPosition).getState(),
+                    frontText ? org.bukkit.block.sign.Side.FRONT : org.bukkit.block.sign.Side.BACK
+                );
+                if (!event.callEvent()) {
+                    return false;
+                }
+                player.getServer().getCommands().performPrefixedCommand(this.createCommandSourceStack(((org.bukkit.craftbukkit.entity.CraftPlayer) event.getPlayer()).getHandle(), level, pos), event.getMessage());
+                // Paper end - Fix commands from signs not firing command events
                 flag = true;
             }
         }
@@ -215,10 +_,55 @@
         return flag;
     }
 
-    private static CommandSourceStack createCommandSourceStack(@Nullable Player player, Level level, BlockPos pos) {
+    // CraftBukkit start
+    private final CommandSource commandSource = new CommandSource() {
+
+        @Override
+        public void sendSystemMessage(Component message) {}
+
+        @Override
+        public org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack commandSourceStack) {
+            return commandSourceStack.getEntity() != null ? commandSourceStack.getEntity().getBukkitEntity() : new org.bukkit.craftbukkit.command.CraftBlockCommandSender(commandSourceStack, SignBlockEntity.this);
+        }
+
+        @Override
+        public boolean acceptsSuccess() {
+            return false;
+        }
+
+        @Override
+        public boolean acceptsFailure() {
+            return false;
+        }
+
+        @Override
+        public boolean shouldInformAdmins() {
+            return false;
+        }
+    };
+
+    private CommandSourceStack createCommandSourceStack(@Nullable Player player, Level level, BlockPos pos) {
+        // CraftBukkit end
         String string = player == null ? "Sign" : player.getName().getString();
         Component component = (Component)(player == null ? Component.literal("Sign") : player.getDisplayName());
-        return new CommandSourceStack(CommandSource.NULL, Vec3.atCenterOf(pos), Vec2.ZERO, (ServerLevel)level, 2, string, component, level.getServer(), player);
+
+        // Paper start - Fix commands from signs not firing command events
+        CommandSource commandSource = level.paperConfig().misc.showSignClickCommandFailureMsgsToPlayer ? new io.papermc.paper.commands.DelegatingCommandSource(this.commandSource) {
+            @Override
+            public void sendSystemMessage(Component message) {
+                if (player instanceof final net.minecraft.server.level.ServerPlayer serverPlayer) {
+                    serverPlayer.sendSystemMessage(message);
+                }
+            }
+
+            @Override
+            public boolean acceptsFailure() {
+                return true;
+            }
+        } : this.commandSource;
+        // Paper end - Fix commands from signs not firing command events
+        // CraftBukkit - this
+        return new CommandSourceStack(commandSource, Vec3.atCenterOf(pos), Vec2.ZERO, (ServerLevel)level, 2, string, component, level.getServer(), player); // Paper - Fix commands from signs not firing command events
     }
 
     @Override
@@ -237,12 +_,17 @@
 
     @Nullable
     public UUID getPlayerWhoMayEdit() {
+        // CraftBukkit start - unnecessary sign ticking removed, so do this lazily
+        if (this.level != null && this.playerWhoMayEdit != null) {
+            this.clearInvalidPlayerWhoMayEdit(this, this.level, this.playerWhoMayEdit);
+        }
+        // CraftBukkit end
         return this.playerWhoMayEdit;
     }
 
     private void markUpdated() {
         this.setChanged();
-        this.level.sendBlockUpdated(this.getBlockPos(), this.getBlockState(), this.getBlockState(), 3);
+        if (this.level != null) this.level.sendBlockUpdated(this.getBlockPos(), this.getBlockState(), this.getBlockState(), 3); // CraftBukkit - skip notify if world is null (SPIGOT-5122)
     }
 
     public boolean isWaxed() {
