--- a/net/minecraft/world/inventory/Container.java
+++ b/net/minecraft/world/inventory/Container.java
@@ -33,6 +33,20 @@
 import net.minecraft.world.level.block.entity.TileEntity;
 import org.slf4j.Logger;
 
+// CraftBukkit start
+import com.google.common.base.Preconditions;
+import java.util.HashMap;
+import java.util.Map;
+import net.minecraft.network.chat.IChatBaseComponent;
+import net.minecraft.network.protocol.game.PacketPlayOutSetSlot;
+import org.bukkit.craftbukkit.inventory.CraftInventory;
+import org.bukkit.craftbukkit.inventory.CraftItemStack;
+import org.bukkit.event.Event.Result;
+import org.bukkit.event.inventory.InventoryDragEvent;
+import org.bukkit.event.inventory.InventoryType;
+import org.bukkit.inventory.InventoryView;
+// CraftBukkit end
+
 public abstract class Container {
 
     private static final Logger LOGGER = LogUtils.getLogger();
@@ -63,6 +77,27 @@
     private ContainerSynchronizer synchronizer;
     private boolean suppressRemoteUpdates;
 
+    // CraftBukkit start
+    public boolean checkReachable = true;
+    public abstract InventoryView getBukkitView();
+    public void transferTo(Container other, org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
+        InventoryView source = this.getBukkitView(), destination = other.getBukkitView();
+        ((CraftInventory) source.getTopInventory()).getInventory().onClose(player);
+        ((CraftInventory) source.getBottomInventory()).getInventory().onClose(player);
+        ((CraftInventory) destination.getTopInventory()).getInventory().onOpen(player);
+        ((CraftInventory) destination.getBottomInventory()).getInventory().onOpen(player);
+    }
+    private IChatBaseComponent title;
+    public final IChatBaseComponent getTitle() {
+        Preconditions.checkState(this.title != null, "Title not set");
+        return this.title;
+    }
+    public final void setTitle(IChatBaseComponent title) {
+        Preconditions.checkState(this.title == null, "Title already set");
+        this.title = title;
+    }
+    // CraftBukkit end
+
     protected Container(@Nullable Containers<?> containers, int i) {
         this.carried = ItemStack.EMPTY;
         this.remoteSlots = NonNullList.create();
@@ -164,6 +199,15 @@
 
     }
 
+    // CraftBukkit start
+    public void broadcastCarriedItem() {
+        this.remoteCarried = this.getCarried().copy();
+        if (this.synchronizer != null) {
+            this.synchronizer.sendCarriedChange(this, this.remoteCarried);
+        }
+    }
+    // CraftBukkit end
+
     public void removeSlotListener(ICrafting icrafting) {
         this.containerListeners.remove(icrafting);
     }
@@ -380,7 +424,7 @@
                 }
             } else if (this.quickcraftStatus == 2) {
                 if (!this.quickcraftSlots.isEmpty()) {
-                    if (this.quickcraftSlots.size() == 1) {
+                    if (false && this.quickcraftSlots.size() == 1) { // CraftBukkit - treat everything as a drag since we are unable to easily call InventoryClickEvent instead
                         k = ((Slot) this.quickcraftSlots.iterator().next()).index;
                         this.resetQuickCraft();
                         this.doClick(k, this.quickcraftType, InventoryClickType.PICKUP, entityhuman);
@@ -396,6 +440,7 @@
                     l = this.getCarried().getCount();
                     Iterator iterator = this.quickcraftSlots.iterator();
 
+                    Map<Integer, ItemStack> draggedSlots = new HashMap<Integer, ItemStack>(); // CraftBukkit - Store slots from drag in map (raw slot id -> new stack)
                     while (iterator.hasNext()) {
                         Slot slot1 = (Slot) iterator.next();
                         ItemStack itemstack2 = this.getCarried();
@@ -406,12 +451,48 @@
                             int l1 = Math.min(getQuickCraftPlaceCount(this.quickcraftSlots, this.quickcraftType, itemstack1) + j1, k1);
 
                             l -= l1 - j1;
-                            slot1.setByPlayer(itemstack1.copyWithCount(l1));
+                            // slot1.setByPlayer(itemstack1.copyWithCount(l1));
+                            draggedSlots.put(slot1.index, itemstack1.copyWithCount(l1)); // CraftBukkit - Put in map instead of setting
                         }
                     }
 
-                    itemstack1.setCount(l);
-                    this.setCarried(itemstack1);
+                    // CraftBukkit start - InventoryDragEvent
+                    InventoryView view = getBukkitView();
+                    org.bukkit.inventory.ItemStack newcursor = CraftItemStack.asCraftMirror(itemstack1);
+                    newcursor.setAmount(l);
+                    Map<Integer, org.bukkit.inventory.ItemStack> eventmap = new HashMap<Integer, org.bukkit.inventory.ItemStack>();
+                    for (Map.Entry<Integer, ItemStack> ditem : draggedSlots.entrySet()) {
+                        eventmap.put(ditem.getKey(), 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(CraftItemStack.asNMSCopy(newcursor));
+
+                    InventoryDragEvent event = new InventoryDragEvent(view, (newcursor.getType() != org.bukkit.Material.AIR ? newcursor : null), CraftItemStack.asBukkitCopy(oldCursor), this.quickcraftType == 1, eventmap);
+                    entityhuman.level().getCraftServer().getPluginManager().callEvent(event);
+
+                    // Whether or not a change was made to the inventory that requires an update.
+                    boolean needsUpdate = event.getResult() != Result.DEFAULT;
+
+                    if (event.getResult() != Result.DENY) {
+                        for (Map.Entry<Integer, ItemStack> dslot : draggedSlots.entrySet()) {
+                            view.setItem(dslot.getKey(), 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(CraftItemStack.asNMSCopy(event.getCursor()));
+                            needsUpdate = true;
+                        }
+                    } else {
+                        this.setCarried(oldCursor);
+                    }
+
+                    if (needsUpdate && entityhuman instanceof EntityPlayer) {
+                        this.sendAllDataToRemote();
+                    }
+                    // CraftBukkit end
                 }
 
                 this.resetQuickCraft();
@@ -429,8 +510,11 @@
                 if (i == -999) {
                     if (!this.getCarried().isEmpty()) {
                         if (clickaction == ClickAction.PRIMARY) {
-                            entityhuman.drop(this.getCarried(), true);
+                            // CraftBukkit start
+                            ItemStack carried = this.getCarried();
                             this.setCarried(ItemStack.EMPTY);
+                            entityhuman.drop(carried, true);
+                            // CraftBukkit start
                         } else {
                             entityhuman.drop(this.getCarried().split(1), true);
                         }
@@ -493,6 +577,15 @@
                     }
 
                     slot.setChanged();
+                    // CraftBukkit start - Make sure the client has the right slot contents
+                    if (entityhuman instanceof EntityPlayer && slot.getMaxStackSize() != 64) {
+                        ((EntityPlayer) entityhuman).connection.send(new PacketPlayOutSetSlot(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() == InventoryType.WORKBENCH || this.getBukkitView().getType() == InventoryType.CRAFTING) {
+                            ((EntityPlayer) entityhuman).connection.send(new PacketPlayOutSetSlot(this.containerId, this.incrementStateId(), 0, this.getSlot(0).getItem()));
+                        }
+                    }
+                    // CraftBukkit end
                 }
             } else {
                 Slot slot2;
@@ -605,13 +698,14 @@
             ItemStack itemstack = this.getCarried();
 
             if (!itemstack.isEmpty()) {
+                this.setCarried(ItemStack.EMPTY); // CraftBukkit - SPIGOT-4556 - from below
                 if (entityhuman.isAlive() && !((EntityPlayer) entityhuman).hasDisconnected()) {
                     entityhuman.getInventory().placeItemBackInInventory(itemstack);
                 } else {
                     entityhuman.drop(itemstack, false);
                 }
 
-                this.setCarried(ItemStack.EMPTY);
+                // this.setCarried(ItemStack.EMPTY); // CraftBukkit - moved up
             }
         }
 
@@ -830,6 +924,11 @@
     }
 
     public ItemStack getCarried() {
+        // CraftBukkit start
+        if (this.carried.isEmpty()) {
+            this.setCarried(ItemStack.EMPTY);
+        }
+        // CraftBukkit end
         return this.carried;
     }
 
