--- a/net/minecraft/world/level/block/entity/TileEntitySign.java
+++ b/net/minecraft/world/level/block/entity/TileEntitySign.java
@@ -36,6 +36,16 @@
 import net.minecraft.world.phys.Vec3D;
 import org.slf4j.Logger;
 
+// CraftBukkit start
+import net.minecraft.nbt.NBTBase;
+import net.minecraft.server.level.EntityPlayer;
+import org.bukkit.block.sign.Side;
+import org.bukkit.craftbukkit.block.CraftBlock;
+import org.bukkit.craftbukkit.util.CraftChatMessage;
+import org.bukkit.entity.Player;
+import org.bukkit.event.block.SignChangeEvent;
+// CraftBukkit end
+
 public class TileEntitySign extends TileEntity {
 
     private static final Logger LOGGER = LogUtils.getLogger();
@@ -101,7 +111,7 @@
     protected void saveAdditional(NBTTagCompound nbttagcompound, HolderLookup.a holderlookup_a) {
         super.saveAdditional(nbttagcompound, holderlookup_a);
         DynamicOps<NBTBase> dynamicops = holderlookup_a.createSerializationContext(DynamicOpsNBT.INSTANCE);
-        DataResult dataresult = SignText.DIRECT_CODEC.encodeStart(dynamicops, this.frontText);
+        DataResult<NBTBase> dataresult = SignText.DIRECT_CODEC.encodeStart(dynamicops, this.frontText); // CraftBukkit - decompile error
         Logger logger = TileEntitySign.LOGGER;
 
         Objects.requireNonNull(logger);
@@ -121,7 +131,7 @@
     protected void loadAdditional(NBTTagCompound nbttagcompound, HolderLookup.a holderlookup_a) {
         super.loadAdditional(nbttagcompound, holderlookup_a);
         DynamicOps<NBTBase> dynamicops = holderlookup_a.createSerializationContext(DynamicOpsNBT.INSTANCE);
-        DataResult dataresult;
+        DataResult<SignText> dataresult; // CraftBukkit - decompile error
         Logger logger;
 
         if (nbttagcompound.contains("front_text")) {
@@ -173,12 +183,13 @@
     public void updateSignText(EntityHuman entityhuman, boolean flag, List<FilteredText> list) {
         if (!this.isWaxed() && entityhuman.getUUID().equals(this.getPlayerWhoMayEdit()) && this.level != null) {
             this.updateText((signtext) -> {
-                return this.setMessages(entityhuman, list, signtext);
+                return this.setMessages(entityhuman, list, signtext, flag); // CraftBukkit
             }, flag);
             this.setAllowedPlayerEditor((UUID) null);
             this.level.sendBlockUpdated(this.getBlockPos(), this.getBlockState(), this.getBlockState(), 3);
         } else {
             TileEntitySign.LOGGER.warn("Player {} just tried to change non-editable sign", entityhuman.getName().getString());
+            ((EntityPlayer) entityhuman).connection.send(this.getUpdatePacket()); // CraftBukkit
         }
     }
 
@@ -188,7 +199,8 @@
         return this.setText((SignText) unaryoperator.apply(signtext), flag);
     }
 
-    private SignText setMessages(EntityHuman entityhuman, List<FilteredText> list, SignText signtext) {
+    private SignText setMessages(EntityHuman entityhuman, List<FilteredText> list, SignText signtext, boolean front) { // CraftBukkit
+        SignText originalText = signtext; // CraftBukkit
         for (int i = 0; i < list.size(); ++i) {
             FilteredText filteredtext = (FilteredText) list.get(i);
             ChatModifier chatmodifier = signtext.getMessage(i, entityhuman.isTextFilteringEnabled()).getStyle();
@@ -200,6 +212,29 @@
             }
         }
 
+        // CraftBukkit start
+        Player player = ((EntityPlayer) entityhuman).getBukkitEntity();
+        String[] lines = new String[4];
+
+        for (int i = 0; i < list.size(); ++i) {
+            lines[i] = CraftChatMessage.fromComponent(signtext.getMessage(i, entityhuman.isTextFilteringEnabled()));
+        }
+
+        SignChangeEvent event = new SignChangeEvent(CraftBlock.at(this.level, this.worldPosition), player, lines.clone(), (front) ? Side.FRONT : Side.BACK);
+        entityhuman.level().getCraftServer().getPluginManager().callEvent(event);
+
+        if (event.isCancelled()) {
+            return originalText;
+        }
+
+        IChatBaseComponent[] components = org.bukkit.craftbukkit.block.CraftSign.sanitizeLines(event.getLines());
+        for (int i = 0; i < components.length; i++) {
+            if (!Objects.equals(lines[i], event.getLine(i))) {
+                signtext = signtext.setMessage(i, components[i]);
+            }
+        }
+        // CraftBukkit end
+
         return signtext;
     }
 
@@ -250,11 +285,40 @@
         return flag1;
     }
 
-    private static CommandListenerWrapper createCommandSourceStack(@Nullable EntityHuman entityhuman, World world, BlockPosition blockposition) {
+    // CraftBukkit start
+    private final ICommandListener commandSource = new ICommandListener() {
+
+        @Override
+        public void sendSystemMessage(IChatBaseComponent ichatbasecomponent) {}
+
+        @Override
+        public org.bukkit.command.CommandSender getBukkitSender(CommandListenerWrapper wrapper) {
+            return wrapper.getEntity() != null ? wrapper.getEntity().getBukkitEntity() : new org.bukkit.craftbukkit.command.CraftBlockCommandSender(wrapper, TileEntitySign.this);
+        }
+
+        @Override
+        public boolean acceptsSuccess() {
+            return false;
+        }
+
+        @Override
+        public boolean acceptsFailure() {
+            return false;
+        }
+
+        @Override
+        public boolean shouldInformAdmins() {
+            return false;
+        }
+    };
+
+    private CommandListenerWrapper createCommandSourceStack(@Nullable EntityHuman entityhuman, World world, BlockPosition blockposition) {
+        // CraftBukkit end
         String s = entityhuman == null ? "Sign" : entityhuman.getName().getString();
         Object object = entityhuman == null ? IChatBaseComponent.literal("Sign") : entityhuman.getDisplayName();
 
-        return new CommandListenerWrapper(ICommandListener.NULL, Vec3D.atCenterOf(blockposition), Vec2F.ZERO, (WorldServer) world, 2, s, (IChatBaseComponent) object, world.getServer(), entityhuman);
+        // CraftBukkit - commandSource
+        return new CommandListenerWrapper(commandSource, Vec3D.atCenterOf(blockposition), Vec2F.ZERO, (WorldServer) world, 2, s, (IChatBaseComponent) object, world.getServer(), entityhuman);
     }
 
     @Override
@@ -273,12 +337,17 @@
 
     @Nullable
     public UUID getPlayerWhoMayEdit() {
+        // CraftBukkit start - unnecessary sign ticking removed, so do this lazily
+        if (this.level != null && this.playerWhoMayEdit != null) {
+            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() {
