--- a/net/minecraft/world/inventory/AbstractContainerMenu.java
+++ b/net/minecraft/world/inventory/AbstractContainerMenu.java
@@ -47,24 +_,54 @@
     public static final int CARRIED_SLOT_SIZE = Integer.MAX_VALUE;
     public static final int SLOTS_PER_ROW = 9;
     public static final int SLOT_SIZE = 18;
-    private final NonNullList<ItemStack> lastSlots = NonNullList.create();
-    public final NonNullList<Slot> slots = NonNullList.create();
-    private final List<DataSlot> dataSlots = Lists.newArrayList();
+    public NonNullList<ItemStack> lastSlots = NonNullList.create();
+    public NonNullList<Slot> slots = NonNullList.create();
+    public List<DataSlot> dataSlots = Lists.newArrayList();
     private ItemStack carried = ItemStack.EMPTY;
-    private final NonNullList<RemoteSlot> remoteSlots = NonNullList.create();
-    private final IntList remoteDataSlots = new IntArrayList();
+    public NonNullList<RemoteSlot> remoteSlots = NonNullList.create();
+    public IntList remoteDataSlots = new IntArrayList();
     private RemoteSlot remoteCarried = RemoteSlot.PLACEHOLDER;
     private int stateId;
     @Nullable
-    private final MenuType<?> menuType;
+    public final MenuType<?> menuType;
     public final int containerId;
-    private int quickcraftType = -1;
-    private int quickcraftStatus;
-    private final Set<Slot> quickcraftSlots = Sets.newHashSet();
+    public int quickcraftType = -1;
+    public int quickcraftStatus;
+    public final Set<Slot> quickcraftSlots = Sets.newHashSet();
     private final List<ContainerListener> containerListeners = Lists.newArrayList();
     @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 +_,19 @@
 
         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
+        }
+    }
+
+    // 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();
+                    java.util.Map<Integer, ItemStack> draggedSlots = new java.util.HashMap<>(); // CraftBukkit - Store slots from drag in map (raw slot id -> new stack)
 
                     for (Slot slot1 : this.quickcraftSlots) {
                         ItemStack carried1 = this.getCarried();
@@ -387,12 +_,48 @@
                             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 newcursor = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack);
+                    newcursor.setAmount(count);
+                    java.util.Map<Integer, org.bukkit.inventory.ItemStack> eventmap = new java.util.HashMap<>();
+                    for (java.util.Map.Entry<Integer, ItemStack> ditem : draggedSlots.entrySet()) {
+                        eventmap.put(ditem.getKey(), org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(ditem.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 oldCursor = this.getCarried();
+                    this.setCarried(org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(newcursor));
+
+                    org.bukkit.event.inventory.InventoryDragEvent event = new org.bukkit.event.inventory.InventoryDragEvent(view, (newcursor.getType() != org.bukkit.Material.AIR ? newcursor : null), org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(oldCursor), this.quickcraftType == 1, eventmap);
+                    event.callEvent();
+
+                    // Whether a change was made to the inventory that requires an update.
+                    boolean needsUpdate = event.getResult() != org.bukkit.event.Event.Result.DEFAULT;
+
+                    if (event.getResult() != org.bukkit.event.Event.Result.DENY) {
+                        for (java.util.Map.Entry<Integer, ItemStack> dslot : draggedSlots.entrySet()) {
+                            view.setItem(dslot.getKey(), org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(dslot.getValue()));
+                        }
+                        // The only time the carried item will be set to null 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() != null) {
+                            this.setCarried(org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getCursor()));
+                            needsUpdate = true;
+                        }
+                    } else {
+                        this.setCarried(oldCursor);
+                    }
+
+                    if (needsUpdate && player instanceof ServerPlayer) {
+                        this.sendAllDataToRemote();
+                    }
+                    // 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() != 64) {
+                    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;
                 }
@@ -716,7 +_,7 @@
         return dragMode == 0 || dragMode == 1 || dragMode == 2 && player.hasInfiniteMaterials();
     }
 
-    protected void resetQuickCraft() {
+    public void resetQuickCraft() {
         this.quickcraftStatus = 0;
         this.quickcraftSlots.clear();
     }
@@ -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
 }
