--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java
+++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java
@@ -37,6 +_,37 @@
     private long tickedGameTime;
     private Direction facing;
 
+    // CraftBukkit start - add fields and methods
+    public List<org.bukkit.entity.HumanEntity> transaction = new java.util.ArrayList<>();
+    private int maxStack = MAX_STACK;
+
+    public List<ItemStack> getContents() {
+        return this.items;
+    }
+
+    public void onOpen(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
+        this.transaction.add(player);
+    }
+
+    public void onClose(org.bukkit.craftbukkit.entity.CraftHumanEntity player) {
+        this.transaction.remove(player);
+    }
+
+    public java.util.List<org.bukkit.entity.HumanEntity> getViewers() {
+        return this.transaction;
+    }
+
+    @Override
+    public int getMaxStackSize() {
+        return this.maxStack;
+    }
+
+    public void setMaxStackSize(int size) {
+        this.maxStack = size;
+    }
+    // CraftBukkit end
+
+
     public HopperBlockEntity(BlockPos pos, BlockState blockState) {
         super(BlockEntityType.HOPPER, pos, blockState);
         this.facing = blockState.getValue(HopperBlock.FACING);
@@ -97,7 +_,14 @@
         blockEntity.tickedGameTime = level.getGameTime();
         if (!blockEntity.isOnCooldown()) {
             blockEntity.setCooldown(0);
-            tryMoveItems(level, pos, state, blockEntity, () -> suckInItems(level, blockEntity));
+            // Spigot start
+            boolean result = tryMoveItems(level, pos, state, blockEntity, () -> {
+                return suckInItems(level, blockEntity);
+            });
+            if (!result && blockEntity.level.spigotConfig.hopperCheck > 1) {
+                blockEntity.setCooldown(blockEntity.level.spigotConfig.hopperCheck);
+            }
+            // Spigot end
         }
     }
 
@@ -116,7 +_,7 @@
                 }
 
                 if (flag) {
-                    blockEntity.setCooldown(8);
+                    blockEntity.setCooldown(level.spigotConfig.hopperTransfer); // Spigot
                     setChanged(level, pos, state);
                     return true;
                 }
@@ -149,14 +_,47 @@
                     ItemStack item = blockEntity.getItem(i);
                     if (!item.isEmpty()) {
                         int count = item.getCount();
-                        ItemStack itemStack = addItem(blockEntity, attachedContainer, blockEntity.removeItem(i, 1), opposite);
+                        // CraftBukkit start - Call event when pushing items into other inventories
+                        ItemStack original = item.copy();
+                        org.bukkit.craftbukkit.inventory.CraftItemStack oitemstack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(
+                            blockEntity.removeItem(i, level.spigotConfig.hopperAmount)
+                        ); // Spigot
+
+                        org.bukkit.inventory.Inventory destinationInventory;
+                        // Have to special case large chests as they work oddly
+                        if (attachedContainer instanceof final net.minecraft.world.CompoundContainer compoundContainer) {
+                            destinationInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest(compoundContainer);
+                        } else if (attachedContainer.getOwner() != null) {
+                            destinationInventory = attachedContainer.getOwner().getInventory();
+                        } else {
+                            destinationInventory = new org.bukkit.craftbukkit.inventory.CraftInventory(attachedContainer);
+                        }
+
+                        org.bukkit.event.inventory.InventoryMoveItemEvent event = new org.bukkit.event.inventory.InventoryMoveItemEvent(
+                            blockEntity.getOwner().getInventory(),
+                            oitemstack,
+                            destinationInventory,
+                            true
+                        );
+                        if (!event.callEvent()) {
+                            blockEntity.setItem(i, original);
+                            blockEntity.setCooldown(level.spigotConfig.hopperTransfer); // Delay hopper checks // Spigot
+                            return false;
+                        }
+                        int origCount = event.getItem().getAmount(); // Spigot
+                        ItemStack itemStack = HopperBlockEntity.addItem(blockEntity, attachedContainer, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem()), opposite);
+                        // CraftBukkit end
+
                         if (itemStack.isEmpty()) {
                             attachedContainer.setChanged();
                             return true;
                         }
 
                         item.setCount(count);
-                        if (count == 1) {
+                        // Spigot start
+                        item.shrink(origCount - itemStack.getCount());
+                        if (count <= level.spigotConfig.hopperAmount) {
+                            // Spigot end
                             blockEntity.setItem(i, item);
                         }
                     }
@@ -219,7 +_,7 @@
             Direction direction = Direction.DOWN;
 
             for (int i : getSlots(sourceContainer, direction)) {
-                if (tryTakeInItemFromSlot(hopper, sourceContainer, i, direction)) {
+                if (tryTakeInItemFromSlot(hopper, sourceContainer, i, direction, level)) { // Spigot
                     return true;
                 }
             }
@@ -239,18 +_,56 @@
         }
     }
 
-    private static boolean tryTakeInItemFromSlot(Hopper hopper, Container container, int slot, Direction direction) {
+    private static boolean tryTakeInItemFromSlot(Hopper hopper, Container container, int slot, Direction direction, Level level) { // Spigot
         ItemStack item = container.getItem(slot);
         if (!item.isEmpty() && canTakeItemFromContainer(hopper, container, item, slot, direction)) {
             int count = item.getCount();
-            ItemStack itemStack = addItem(container, hopper, container.removeItem(slot, 1), null);
+            // CraftBukkit start - Call event on collection of items from inventories into the hopper
+            ItemStack original = item.copy();
+            org.bukkit.craftbukkit.inventory.CraftItemStack oitemstack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(
+                container.removeItem(slot, level.spigotConfig.hopperAmount) // Spigot
+            );
+
+            org.bukkit.inventory.Inventory sourceInventory;
+            // Have to special case large chests as they work oddly
+            if (container instanceof final net.minecraft.world.CompoundContainer compoundContainer) {
+                sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest(compoundContainer);
+            } else if (container.getOwner() != null) {
+                sourceInventory = container.getOwner().getInventory();
+            } else {
+                sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventory(container);
+            }
+
+            org.bukkit.event.inventory.InventoryMoveItemEvent event = new org.bukkit.event.inventory.InventoryMoveItemEvent(
+                sourceInventory,
+                oitemstack,
+                hopper.getOwner().getInventory(),
+                false
+            );
+
+            if (!event.callEvent()) {
+                container.setItem(slot, original);
+
+                if (hopper instanceof final HopperBlockEntity hopperBlockEntity) {
+                    hopperBlockEntity.setCooldown(level.spigotConfig.hopperTransfer); // Spigot
+                }
+
+                return false;
+            }
+            int origCount = event.getItem().getAmount(); // Spigot
+            ItemStack itemStack = HopperBlockEntity.addItem(container, hopper, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem()), null);
+            // CraftBukkit end
+
             if (itemStack.isEmpty()) {
                 container.setChanged();
                 return true;
             }
 
             item.setCount(count);
-            if (count == 1) {
+            // Spigot start
+            item.shrink(origCount - itemStack.getCount());
+            if (count <= level.spigotConfig.hopperAmount) {
+                // Spigot end
                 container.setItem(slot, item);
             }
         }
@@ -260,12 +_,20 @@
 
     public static boolean addItem(Container container, ItemEntity item) {
         boolean flag = false;
+        // CraftBukkit start
+        org.bukkit.event.inventory.InventoryPickupItemEvent event = new org.bukkit.event.inventory.InventoryPickupItemEvent(
+            container.getOwner().getInventory(), (org.bukkit.entity.Item) item.getBukkitEntity()
+        );
+        if (!event.callEvent()) {
+            return false;
+        }
+        // CraftBukkit end
         ItemStack itemStack = item.getItem().copy();
         ItemStack itemStack1 = addItem(null, container, itemStack, null);
         if (itemStack1.isEmpty()) {
             flag = true;
             item.setItem(ItemStack.EMPTY);
-            item.discard();
+            item.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
         } else {
             item.setItem(itemStack1);
         }
@@ -307,11 +_,18 @@
             boolean flag = false;
             boolean isEmpty = destination.isEmpty();
             if (item.isEmpty()) {
+                // Spigot start - SPIGOT-6693, SimpleContainer#setItem
+                ItemStack leftover = ItemStack.EMPTY; // Paper - Make hoppers respect inventory max stack size
+                if (!stack.isEmpty() && stack.getCount() > destination.getMaxStackSize()) {
+                    leftover = stack; // Paper - Make hoppers respect inventory max stack size
+                    stack = stack.split(destination.getMaxStackSize());
+                }
+                // Spigot end
                 destination.setItem(slot, stack);
-                stack = ItemStack.EMPTY;
+                stack = leftover; // Paper - Make hoppers respect inventory max stack size
                 flag = true;
             } else if (canMergeItems(item, stack)) {
-                int i = stack.getMaxStackSize() - item.getCount();
+                int i = Math.min(stack.getMaxStackSize(), destination.getMaxStackSize()) - item.getCount(); // Paper - Make hoppers respect inventory max stack size
                 int min = Math.min(stack.getCount(), i);
                 stack.shrink(min);
                 item.grow(min);
@@ -325,7 +_,7 @@
                         min = 1;
                     }
 
-                    hopperBlockEntity.setCooldown(8 - min);
+                    hopperBlockEntity.setCooldown(hopperBlockEntity.level.spigotConfig.hopperTransfer - min); // Spigot
                 }
 
                 destination.setChanged();
@@ -335,14 +_,57 @@
         return stack;
     }
 
+    // CraftBukkit start
+    @Nullable
+    private static Container runHopperInventorySearchEvent(
+        Container container,
+        org.bukkit.craftbukkit.block.CraftBlock hopper,
+        org.bukkit.craftbukkit.block.CraftBlock searchLocation,
+        org.bukkit.event.inventory.HopperInventorySearchEvent.ContainerType containerType
+    ) {
+        org.bukkit.event.inventory.HopperInventorySearchEvent event = new org.bukkit.event.inventory.HopperInventorySearchEvent(
+            (container != null) ? new org.bukkit.craftbukkit.inventory.CraftInventory(container) : null,
+            containerType,
+            hopper,
+            searchLocation
+        );
+        event.callEvent();
+        return (event.getInventory() != null) ? ((org.bukkit.craftbukkit.inventory.CraftInventory) event.getInventory()).getInventory() : null;
+    }
+    // CraftBukkit end
+
     @Nullable
     private static Container getAttachedContainer(Level level, BlockPos pos, HopperBlockEntity blockEntity) {
-        return getContainerAt(level, pos.relative(blockEntity.facing));
+        // CraftBukkit start
+        BlockPos searchPosition = pos.relative(blockEntity.facing);
+        Container inventory = getContainerAt(level, searchPosition);
+
+        org.bukkit.craftbukkit.block.CraftBlock hopper = org.bukkit.craftbukkit.block.CraftBlock.at(level, pos);
+        org.bukkit.craftbukkit.block.CraftBlock searchBlock = org.bukkit.craftbukkit.block.CraftBlock.at(level, searchPosition);
+        return HopperBlockEntity.runHopperInventorySearchEvent(
+            inventory,
+            hopper,
+            searchBlock,
+            org.bukkit.event.inventory.HopperInventorySearchEvent.ContainerType.DESTINATION
+        );
+        // CraftBukkit end
     }
 
     @Nullable
     private static Container getSourceContainer(Level level, Hopper hopper, BlockPos pos, BlockState state) {
-        return getContainerAt(level, pos, state, hopper.getLevelX(), hopper.getLevelY() + 1.0, hopper.getLevelZ());
+        // CraftBukkit start
+        final Container inventory = HopperBlockEntity.getContainerAt(level, pos, state, hopper.getLevelX(), hopper.getLevelY() + 1.0D, hopper.getLevelZ());
+
+        final BlockPos blockPosition = BlockPos.containing(hopper.getLevelX(), hopper.getLevelY(), hopper.getLevelZ());
+        org.bukkit.craftbukkit.block.CraftBlock hopperBlock = org.bukkit.craftbukkit.block.CraftBlock.at(level, blockPosition);
+        org.bukkit.craftbukkit.block.CraftBlock containerBlock = org.bukkit.craftbukkit.block.CraftBlock.at(level, blockPosition.above());
+        return HopperBlockEntity.runHopperInventorySearchEvent(
+            inventory,
+            hopperBlock,
+            containerBlock,
+            org.bukkit.event.inventory.HopperInventorySearchEvent.ContainerType.SOURCE
+        );
+        // CraftBukkit end
     }
 
     public static List<ItemEntity> getItemsAtAndAbove(Level level, Hopper hopper) {
@@ -367,6 +_,7 @@
 
     @Nullable
     private static Container getBlockContainer(Level level, BlockPos pos, BlockState state) {
+        if (!level.spigotConfig.hopperCanLoadChunks && !level.hasChunkAt(pos)) return null; // Spigot
         Block block = state.getBlock();
         if (block instanceof WorldlyContainerHolder) {
             return ((WorldlyContainerHolder)block).getContainer(state, level, pos);
