--- a/net/minecraft/world/inventory/AbstractContainerMenu.java
+++ b/net/minecraft/world/inventory/AbstractContainerMenu.java
@@ -65,6 +_,36 @@
     @Nullable
     private ContainerSynchronizer synchronizer;
     private boolean suppressRemoteUpdates;
+    // CraftBukkit start
+    public boolean checkReachable = true;
+    public abstract org.bukkit.inventory.InventoryView getBukkitView();
+
+    public void transferTo(AbstractContainerMenu other, org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
+        org.bukkit.inventory.InventoryView source = this.getBukkitView(), destination = other.getBukkitView();
+        ((org.bukkit.craftbukkit.inventory.CraftInventory) source.getTopInventory()).getInventory().onClose(player);
+        ((org.bukkit.craftbukkit.inventory.CraftInventory) source.getBottomInventory()).getInventory().onClose(player);
+        ((org.bukkit.craftbukkit.inventory.CraftInventory) destination.getTopInventory()).getInventory().onOpen(player);
+        ((org.bukkit.craftbukkit.inventory.CraftInventory) destination.getBottomInventory()).getInventory().onOpen(player);
+    }
+
+    @Nullable
+    private net.minecraft.network.chat.Component title;
+    public final net.minecraft.network.chat.Component getTitle() {
+        // Paper start - return chat component with empty text instead of throwing error
+        if (this.title == null) {
+            return net.minecraft.network.chat.Component.empty();
+        }
+        // Paper end - return chat component with empty text instead of throwing error
+        return this.title;
+    }
+
+    public final void setTitle(net.minecraft.network.chat.Component title) {
+        com.google.common.base.Preconditions.checkState(this.title == null, "Title already set");
+        this.title = title;
+    }
+
+    public void startOpen() {}
+    // CraftBukkit end
 
     protected AbstractContainerMenu(@Nullable MenuType<?> menuType, int containerId) {
         this.menuType = menuType;
@@ -176,8 +_,43 @@
 
         if (this.synchronizer != null) {
             this.synchronizer.sendInitialData(this, list, carried.copy(), this.remoteDataSlots.toIntArray());
-        }
-    }
+            this.synchronizer.sendOffHandSlotChange(); // Paper - Sync offhand slot in menus; update player's offhand since the offhand slot is not added to the slots for menus but can be changed by swapping from a menu slot
+        }
+    }
+
+    // Paper start
+    public void forceHeldSlot(final net.minecraft.world.InteractionHand hand) {
+        this.sendAllDataToRemote();
+    }
+
+    public void forceHeldSlotAndArmor(final net.minecraft.world.InteractionHand hand) {
+        this.sendAllDataToRemote();
+    }
+
+    public void forceSlot(final Container container, final int slot) {
+        final int slotsIndex = this.findSlot(container, slot).orElse(-1);
+        if (slotsIndex == -1) {
+            return;
+        }
+
+        final ItemStack item = this.slots.get(slotsIndex).getItem();
+        this.remoteSlots.get(slotsIndex).force(item);
+
+        if (this.synchronizer != null) {
+            this.synchronizer.sendSlotChange(this, slotsIndex, item.copy());
+        }
+    }
+    // Paper end
+
+    // CraftBukkit start - from synchronizeCarriedToRemote
+    public void broadcastCarriedItem() {
+        ItemStack carried = this.getCarried();
+        this.remoteCarried.force(carried);
+        if (this.synchronizer != null) {
+            this.synchronizer.sendCarriedChange(this, carried.copy());
+        }
+    }
+    // CraftBukkit end
 
     public void removeSlotListener(ContainerListener listener) {
         this.containerListeners.remove(listener);
@@ -243,7 +_,7 @@
             this.lastSlots.set(slotIndex, itemStack1);
 
             for (ContainerListener containerListener : this.containerListeners) {
-                containerListener.slotChanged(this, slotIndex, itemStack1);
+                containerListener.slotChanged(this, slotIndex, itemStack, itemStack1); // Paper - Add PlayerInventorySlotChangeEvent
             }
         }
     }
@@ -351,6 +_,7 @@
                     this.resetQuickCraft();
                 }
             } else if (this.quickcraftStatus == 1) {
+                if (slotId < 0) return; // Paper - Add slot sanity checks to container clicks
                 Slot slot = this.slots.get(slotId);
                 ItemStack carried = this.getCarried();
                 if (canItemQuickReplace(slot, carried, true)
@@ -375,6 +_,7 @@
                     }
 
                     int count = this.getCarried().getCount();
+                    it.unimi.dsi.fastutil.ints.Int2ObjectMap<net.minecraft.world.item.ItemStack> draggedSlots = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(); // CraftBukkit - Store slots from drag in map (raw slot id -> new stack)
 
                     for (Slot slot1 : this.quickcraftSlots) {
                         ItemStack carried1 = this.getCarried();
@@ -387,12 +_,46 @@
                             int min = Math.min(itemStack.getMaxStackSize(), slot1.getMaxStackSize(itemStack));
                             int min1 = Math.min(getQuickCraftPlaceCount(this.quickcraftSlots, this.quickcraftType, itemStack) + i2, min);
                             count -= min1 - i2;
-                            slot1.setByPlayer(itemStack.copyWithCount(min1));
-                        }
-                    }
-
-                    itemStack.setCount(count);
-                    this.setCarried(itemStack);
+                            // slot1.setByPlayer(itemStack.copyWithCount(min1));
+                            draggedSlots.put(slot1.index, itemStack.copyWithCount(min1)); // CraftBukkit - Put in map instead of setting
+                        }
+                    }
+
+                    // CraftBukkit start - InventoryDragEvent
+                    org.bukkit.inventory.InventoryView view = this.getBukkitView();
+                    org.bukkit.inventory.ItemStack newCarried = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack);
+                    newCarried.setAmount(count);
+                    java.util.Map<Integer, org.bukkit.inventory.ItemStack> eventMap = new java.util.HashMap<>();
+                    for (it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry<net.minecraft.world.item.ItemStack> entry : draggedSlots.int2ObjectEntrySet()) {
+                        eventMap.put(entry.getIntKey(), org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(entry.getValue()));
+                    }
+
+                    // It's essential that we set the cursor to the new value here to prevent item duplication if a plugin closes the inventory.
+                    ItemStack oldCarried = this.getCarried();
+                    this.setCarried(org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(newCarried));
+
+                    org.bukkit.event.inventory.InventoryDragEvent event = new org.bukkit.event.inventory.InventoryDragEvent(
+                        view,
+                        newCarried,
+                        org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(oldCarried),
+                        this.quickcraftType == QUICKCRAFT_TYPE_GREEDY,
+                        eventMap
+                    );
+                    event.callEvent();
+
+                    if (event.getResult() != org.bukkit.event.Event.Result.DENY) {
+                        for (final it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry<net.minecraft.world.item.ItemStack> entry : draggedSlots.int2ObjectEntrySet()) {
+                            view.setItem(entry.getIntKey(), org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(entry.getValue()));
+                        }
+                        // The only time the carried item will be set to empty is if the inventory is closed by the server.
+                        // If the inventory is closed by the server, then the cursor items are dropped.  This is why we change the cursor early.
+                        if (!this.getCarried().isEmpty()) {
+                            this.setCarried(org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getCursor()));
+                        }
+                    } else {
+                        this.setCarried(oldCarried);
+                    }
+                    // CraftBukkit end
                 }
 
                 this.resetQuickCraft();
@@ -406,8 +_,11 @@
             if (slotId == -999) {
                 if (!this.getCarried().isEmpty()) {
                     if (clickAction == ClickAction.PRIMARY) {
-                        player.drop(this.getCarried(), true);
+                        // CraftBukkit start
+                        ItemStack carried = this.getCarried();
                         this.setCarried(ItemStack.EMPTY);
+                        player.drop(carried, true);
+                        // CraftBukkit end
                     } else {
                         player.drop(this.getCarried().split(1), true);
                     }
@@ -469,8 +_,18 @@
                 }
 
                 slot.setChanged();
+                // CraftBukkit start - Make sure the client has the right slot contents
+                if (player instanceof ServerPlayer serverPlayer && slot.getMaxStackSize() != Container.MAX_STACK) {
+                    serverPlayer.connection.send(new net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket(this.containerId, this.incrementStateId(), slot.index, slot.getItem()));
+                    // Updating a crafting inventory makes the client reset the result slot, have to send it again
+                    if (this.getBukkitView().getType() == org.bukkit.event.inventory.InventoryType.WORKBENCH || this.getBukkitView().getType() == org.bukkit.event.inventory.InventoryType.CRAFTING) {
+                        serverPlayer.connection.send(new net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket(this.containerId, this.incrementStateId(), 0, this.getSlot(0).getItem()));
+                    }
+                }
+                // CraftBukkit end
             }
         } else if (clickType == ClickType.SWAP && (button >= 0 && button < 9 || button == 40)) {
+            if (slotId < 0) return; // Paper - Add slot sanity checks to container clicks
             ItemStack item = inventory.getItem(button);
             Slot slot = this.slots.get(slotId);
             ItemStack carried = slot.getItem();
@@ -590,8 +_,9 @@
         if (player instanceof ServerPlayer) {
             ItemStack carried = this.getCarried();
             if (!carried.isEmpty()) {
+                this.setCarried(ItemStack.EMPTY); // CraftBukkit - SPIGOT-4556 - from below
                 dropOrPlaceInInventory(player, carried);
-                this.setCarried(ItemStack.EMPTY);
+                // this.setCarried(ItemStack.EMPTY); // CraftBukkit - moved up
             }
         }
     }
@@ -637,6 +_,14 @@
     public abstract boolean stillValid(Player player);
 
     protected boolean moveItemStackTo(ItemStack stack, int startIndex, int endIndex, boolean reverseDirection) {
+        // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent
+        return this.moveItemStackTo(stack, startIndex, endIndex, reverseDirection, false);
+    }
+    protected boolean moveItemStackTo(ItemStack stack, int startIndex, int endIndex, boolean reverseDirection, boolean isCheck) {
+        if (isCheck) {
+            stack = stack.copy();
+        }
+        // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
         boolean flag = false;
         int i = startIndex;
         if (reverseDirection) {
@@ -647,18 +_,27 @@
             while (!stack.isEmpty() && (reverseDirection ? i >= startIndex : i < endIndex)) {
                 Slot slot = this.slots.get(i);
                 ItemStack item = slot.getItem();
+                // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent; clone if only a check
+                if (isCheck) {
+                    item = item.copy();
+                }
+                // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
                 if (!item.isEmpty() && ItemStack.isSameItemSameComponents(stack, item)) {
                     int i1 = item.getCount() + stack.getCount();
                     int maxStackSize = slot.getMaxStackSize(item);
                     if (i1 <= maxStackSize) {
                         stack.setCount(0);
                         item.setCount(i1);
+                        if (!isCheck) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
                         slot.setChanged();
+                        } // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
                         flag = true;
                     } else if (item.getCount() < maxStackSize) {
                         stack.shrink(maxStackSize - item.getCount());
                         item.setCount(maxStackSize);
+                         if (!isCheck) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
                         slot.setChanged();
+                        } // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
                         flag = true;
                     }
                 }
@@ -681,10 +_,21 @@
             while (reverseDirection ? i >= startIndex : i < endIndex) {
                 Slot slotx = this.slots.get(i);
                 ItemStack itemx = slotx.getItem();
+                // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent
+                if (isCheck) {
+                    itemx = itemx.copy();
+                }
+                // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
                 if (itemx.isEmpty() && slotx.mayPlace(stack)) {
                     int i1 = slotx.getMaxStackSize(stack);
+                    // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent
+                    if (isCheck) {
+                        stack.shrink(Math.min(stack.getCount(), i1));
+                    } else {
+                    // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
                     slotx.setByPlayer(stack.split(Math.min(stack.getCount(), i1)));
                     slotx.setChanged();
+                    } // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
                     flag = true;
                     break;
                 }
@@ -768,6 +_,11 @@
     }
 
     public ItemStack getCarried() {
+        // CraftBukkit start
+        if (this.carried.isEmpty()) {
+            this.setCarried(ItemStack.EMPTY);
+        }
+        // CraftBukkit end
         return this.carried;
     }
 
@@ -820,4 +_,15 @@
         this.stateId = this.stateId + 1 & 32767;
         return this.stateId;
     }
+
+    // Paper start - Add missing InventoryHolders
+    // The reason this is a supplier, is that the createHolder method uses the bukkit InventoryView#getTopInventory to get the inventory in question
+    // and that can't be obtained safely until the AbstractContainerMenu has been fully constructed. Using a supplier lazily
+    // initializes the InventoryHolder safely.
+    protected final Supplier<org.bukkit.inventory.BlockInventoryHolder> createBlockHolder(final ContainerLevelAccess context) {
+        //noinspection ConstantValue
+        com.google.common.base.Preconditions.checkArgument(context != null, "context was null");
+        return () -> context.createBlockHolder(this);
+    }
+    // Paper end - Add missing InventoryHolders
 }
