--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java
+++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java
@@ -33,10 +_,41 @@
     public static final int HOPPER_CONTAINER_SIZE = 5;
     private static final int[][] CACHED_SLOTS = new int[54][];
     private NonNullList<ItemStack> items = NonNullList.withSize(5, ItemStack.EMPTY);
-    private int cooldownTime = -1;
+    public int cooldownTime = -1;
     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,9 +_,53 @@
         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
+        }
+    }
+
+    // Paper start - Perf: Optimize Hoppers
+    private static final int HOPPER_EMPTY = 0;
+    private static final int HOPPER_HAS_ITEMS = 1;
+    private static final int HOPPER_IS_FULL = 2;
+
+    private static int getFullState(final HopperBlockEntity hopper) {
+        hopper.unpackLootTable(null);
+
+        final List<ItemStack> hopperItems = hopper.items;
+
+        boolean empty = true;
+        boolean full = true;
+
+        for (int i = 0, len = hopperItems.size(); i < len; ++i) {
+            final ItemStack stack = hopperItems.get(i);
+            if (stack.isEmpty()) {
+                full = false;
+                continue;
+            }
+
+            if (!full) {
+                // can't be full
+                return HOPPER_HAS_ITEMS;
+            }
+
+            empty = false;
+
+            if (stack.getCount() != stack.getMaxStackSize()) {
+                // can't be full or empty
+                return HOPPER_HAS_ITEMS;
+            }
+        }
+
+        return empty ? HOPPER_EMPTY : (full ? HOPPER_IS_FULL : HOPPER_HAS_ITEMS);
+    }
+    // Paper end - Perf: Optimize Hoppers
 
     private static boolean tryMoveItems(Level level, BlockPos pos, BlockState state, HopperBlockEntity blockEntity, BooleanSupplier validator) {
         if (level.isClientSide) {
@@ -107,16 +_,17 @@
         } else {
             if (!blockEntity.isOnCooldown() && state.getValue(HopperBlock.ENABLED)) {
                 boolean flag = false;
-                if (!blockEntity.isEmpty()) {
+                final int fullState = getFullState(blockEntity); // Paper - Perf: Optimize Hoppers
+                if (fullState != HOPPER_EMPTY) { // Paper - Perf: Optimize Hoppers
                     flag = ejectItems(level, pos, blockEntity);
                 }
 
-                if (!blockEntity.inventoryFull()) {
-                    flag |= validator.getAsBoolean();
+                if (fullState != HOPPER_IS_FULL || flag) { // Paper - Perf: Optimize Hoppers
+                    flag |= validator.getAsBoolean(); // Paper - note: this is not a validator, it's what adds/sucks in items
                 }
 
                 if (flag) {
-                    blockEntity.setCooldown(8);
+                    blockEntity.setCooldown(level.spigotConfig.hopperTransfer); // Spigot
                     setChanged(level, pos, state);
                     return true;
                 }
@@ -136,6 +_,206 @@
         return true;
     }
 
+    // Paper start - Perf: Optimize Hoppers
+    public static boolean skipHopperEvents;
+    private static boolean skipPullModeEventFire;
+    private static boolean skipPushModeEventFire;
+
+    private static boolean hopperPush(final Level level, final Container destination, final Direction direction, final HopperBlockEntity hopper) {
+        skipPushModeEventFire = skipHopperEvents;
+        boolean foundItem = false;
+        for (int i = 0; i < hopper.getContainerSize(); ++i) {
+            final ItemStack item = hopper.getItem(i);
+            if (!item.isEmpty()) {
+                foundItem = true;
+                ItemStack origItemStack = item;
+                ItemStack movedItem = origItemStack;
+
+                final int originalItemCount = origItemStack.getCount();
+                final int movedItemCount = Math.min(level.spigotConfig.hopperAmount, originalItemCount);
+                origItemStack.setCount(movedItemCount);
+
+                // We only need to fire the event once to give protection plugins a chance to cancel this event
+                // Because nothing uses getItem, every event call should end up the same result.
+                if (!skipPushModeEventFire) {
+                    movedItem = callPushMoveEvent(destination, movedItem, hopper);
+                    if (movedItem == null) { // cancelled
+                        origItemStack.setCount(originalItemCount);
+                        return false;
+                    }
+                }
+
+                final ItemStack remainingItem = addItem(hopper, destination, movedItem, direction);
+                final int remainingItemCount = remainingItem.getCount();
+                if (remainingItemCount != movedItemCount) {
+                    origItemStack = origItemStack.copy(true);
+                    origItemStack.setCount(originalItemCount);
+                    if (!origItemStack.isEmpty()) {
+                        origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount);
+                    }
+                    hopper.setItem(i, origItemStack);
+                    destination.setChanged();
+                    return true;
+                }
+                origItemStack.setCount(originalItemCount);
+            }
+        }
+        if (foundItem && level.paperConfig().hopper.cooldownWhenFull) { // Inventory was full - cooldown
+            hopper.setCooldown(level.spigotConfig.hopperTransfer);
+        }
+        return false;
+    }
+
+    private static boolean hopperPull(final Level level, final Hopper hopper, final Container container, ItemStack origItemStack, final int i) {
+        ItemStack movedItem = origItemStack;
+        final int originalItemCount = origItemStack.getCount();
+        final int movedItemCount = Math.min(level.spigotConfig.hopperAmount, originalItemCount);
+        container.setChanged(); // original logic always marks source inv as changed even if no move happens.
+        movedItem.setCount(movedItemCount);
+
+        if (!skipPullModeEventFire) {
+            movedItem = callPullMoveEvent(hopper, container, movedItem);
+            if (movedItem == null) { // cancelled
+                origItemStack.setCount(originalItemCount);
+                // Drastically improve performance by returning true.
+                // No plugin could have relied on the behavior of false as the other call
+                // site for IMIE did not exhibit the same behavior
+                return true;
+            }
+        }
+
+        final ItemStack remainingItem = addItem(container, hopper, movedItem, null);
+        final int remainingItemCount = remainingItem.getCount();
+        if (remainingItemCount != movedItemCount) {
+            origItemStack = origItemStack.copy(true);
+            origItemStack.setCount(originalItemCount);
+            if (!origItemStack.isEmpty()) {
+                origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount);
+            }
+
+            ignoreBlockEntityUpdates = true;
+            container.setItem(i, origItemStack);
+            ignoreBlockEntityUpdates = false;
+            container.setChanged();
+            return true;
+        }
+        origItemStack.setCount(originalItemCount);
+
+        if (level.paperConfig().hopper.cooldownWhenFull) {
+            applyCooldown(hopper);
+        }
+
+        return false;
+    }
+
+    @Nullable
+    private static ItemStack callPushMoveEvent(Container destination, ItemStack itemStack, HopperBlockEntity hopper) {
+        final org.bukkit.inventory.Inventory destinationInventory = getInventory(destination);
+        final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(
+            hopper.getOwner(false).getInventory(),
+            org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack),
+            destinationInventory,
+            true
+        );
+        final boolean result = event.callEvent();
+        if (!event.calledGetItem && !event.calledSetItem) {
+            skipPushModeEventFire = true;
+        }
+        if (!result) {
+            applyCooldown(hopper);
+            return null;
+        }
+
+        if (event.calledSetItem) {
+            return org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
+        } else {
+            return itemStack;
+        }
+    }
+
+    @Nullable
+    private static ItemStack callPullMoveEvent(final Hopper hopper, final Container container, final ItemStack itemstack) {
+        final org.bukkit.inventory.Inventory sourceInventory = getInventory(container);
+        final org.bukkit.inventory.Inventory destination = getInventory(hopper);
+
+        // Mirror is safe as no plugins ever use this item
+        final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(sourceInventory, org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack), destination, false);
+        final boolean result = event.callEvent();
+        if (!event.calledGetItem && !event.calledSetItem) {
+            skipPullModeEventFire = true;
+        }
+        if (!result) {
+            applyCooldown(hopper);
+            return null;
+        }
+
+        if (event.calledSetItem) {
+            return org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItem());
+        } else {
+            return itemstack;
+        }
+    }
+
+    private static org.bukkit.inventory.Inventory getInventory(final Container container) {
+        final org.bukkit.inventory.Inventory sourceInventory;
+        if (container instanceof net.minecraft.world.CompoundContainer compoundContainer) {
+            // Have to special-case large chests as they work oddly
+            sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest(compoundContainer);
+        } else if (container instanceof BlockEntity blockEntity) {
+            sourceInventory = blockEntity.getOwner(false).getInventory();
+        } else if (container.getOwner() != null) {
+            sourceInventory = container.getOwner().getInventory();
+        } else {
+            sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventory(container);
+        }
+        return sourceInventory;
+    }
+
+    private static void applyCooldown(final Hopper hopper) {
+        if (hopper instanceof HopperBlockEntity blockEntity && blockEntity.getLevel() != null) {
+            blockEntity.setCooldown(blockEntity.getLevel().spigotConfig.hopperTransfer);
+        }
+    }
+
+    private static boolean allMatch(Container container, Direction direction, java.util.function.BiPredicate<ItemStack, Integer> test) {
+        if (container instanceof WorldlyContainer) {
+            for (int slot : ((WorldlyContainer) container).getSlotsForFace(direction)) {
+                if (!test.test(container.getItem(slot), slot)) {
+                    return false;
+                }
+            }
+        } else {
+            int size = container.getContainerSize();
+            for (int slot = 0; slot < size; slot++) {
+                if (!test.test(container.getItem(slot), slot)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    private static boolean anyMatch(Container container, Direction direction, java.util.function.BiPredicate<ItemStack, Integer> test) {
+        if (container instanceof WorldlyContainer) {
+            for (int slot : ((WorldlyContainer) container).getSlotsForFace(direction)) {
+                if (test.test(container.getItem(slot), slot)) {
+                    return true;
+                }
+            }
+        } else {
+            int size = container.getContainerSize();
+            for (int slot = 0; slot < size; slot++) {
+                if (test.test(container.getItem(slot), slot)) {
+                    return true;
+                }
+            }
+        }
+        return true;
+    }
+    private static final java.util.function.BiPredicate<ItemStack, Integer> STACK_SIZE_TEST = (itemStack, i) -> itemStack.getCount() >= itemStack.getMaxStackSize();
+    private static final java.util.function.BiPredicate<ItemStack, Integer> IS_EMPTY_TEST = (itemStack, i) -> itemStack.isEmpty();
+    // Paper end - Perf: Optimize Hoppers
+
     private static boolean ejectItems(Level level, BlockPos pos, HopperBlockEntity blockEntity) {
         Container attachedContainer = getAttachedContainer(level, pos, blockEntity);
         if (attachedContainer == null) {
@@ -145,24 +_,60 @@
             if (isFullContainer(attachedContainer, opposite)) {
                 return false;
             } else {
-                for (int i = 0; i < blockEntity.getContainerSize(); i++) {
-                    ItemStack item = blockEntity.getItem(i);
-                    if (!item.isEmpty()) {
-                        int count = item.getCount();
-                        ItemStack itemStack = addItem(blockEntity, attachedContainer, blockEntity.removeItem(i, 1), opposite);
-                        if (itemStack.isEmpty()) {
-                            attachedContainer.setChanged();
-                            return true;
-                        }
-
-                        item.setCount(count);
-                        if (count == 1) {
-                            blockEntity.setItem(i, item);
-                        }
-                    }
-                }
-
-                return false;
+                // Paper start - Perf: Optimize Hoppers
+                return hopperPush(level, attachedContainer, opposite, blockEntity);
+                //for (int i = 0; i < blockEntity.getContainerSize(); i++) {
+                //    ItemStack item = blockEntity.getItem(i);
+                //    if (!item.isEmpty()) {
+                //        int count = item.getCount();
+                //        // 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);
+                //        // Spigot start
+                //        item.shrink(origCount - itemStack.getCount());
+                //        if (count <= level.spigotConfig.hopperAmount) {
+                //            // Spigot end
+                //            blockEntity.setItem(i, item);
+                //        }
+                //    }
+                //}
+
+                //return false;
+                // Paper end - Perf: Optimize Hoppers
             }
         }
     }
@@ -217,9 +_,10 @@
         Container sourceContainer = getSourceContainer(level, hopper, blockPos, blockState);
         if (sourceContainer != null) {
             Direction direction = Direction.DOWN;
+            skipPullModeEventFire = skipHopperEvents; // Paper - Perf: Optimize Hoppers
 
             for (int i : getSlots(sourceContainer, direction)) {
-                if (tryTakeInItemFromSlot(hopper, sourceContainer, i, direction)) {
+                if (tryTakeInItemFromSlot(hopper, sourceContainer, i, direction, level)) { // Spigot
                     return true;
                 }
             }
@@ -239,20 +_,61 @@
         }
     }
 
-    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);
-            if (itemStack.isEmpty()) {
-                container.setChanged();
-                return true;
-            }
-
-            item.setCount(count);
-            if (count == 1) {
-                container.setItem(slot, item);
-            }
+            // Paper start - Perf: Optimize Hoppers
+            return hopperPull(level, hopper, container, item, slot);
+            //int count = item.getCount();
+            //// 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);
+            //// Spigot start
+            //item.shrink(origCount - itemStack.getCount());
+            //if (count <= level.spigotConfig.hopperAmount) {
+            //    // Spigot end
+            //    container.setItem(slot, item);
+            //}
+            // Paper end - Perf: Optimize Hoppers
         }
 
         return false;
@@ -260,12 +_,22 @@
 
     public static boolean addItem(Container container, ItemEntity item) {
         boolean flag = false;
+        // CraftBukkit start
+        if (org.bukkit.event.inventory.InventoryPickupItemEvent.getHandlerList().getRegisteredListeners().length > 0) { // Paper - optimize hoppers
+        org.bukkit.event.inventory.InventoryPickupItemEvent event = new org.bukkit.event.inventory.InventoryPickupItemEvent(
+            getInventory(container), (org.bukkit.entity.Item) item.getBukkitEntity() // Paper - Perf: Optimize Hoppers; use getInventory() to avoid snapshot creation
+        );
+        if (!event.callEvent()) {
+            return false;
+        }
+        // CraftBukkit end
+        } // Paper - Perf: Optimize Hoppers
         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 +_,20 @@
             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
+                ignoreBlockEntityUpdates = true; // Paper - Perf: Optimize Hoppers
                 destination.setItem(slot, stack);
-                stack = ItemStack.EMPTY;
+                ignoreBlockEntityUpdates = false; // Paper - Perf: Optimize Hoppers
+                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) {
@@ -352,13 +_,19 @@
 
     @Nullable
     public static Container getContainerAt(Level level, BlockPos pos) {
-        return getContainerAt(level, pos, level.getBlockState(pos), pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
+        return getContainerAt(level, pos, level.getBlockState(pos), pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, true); // Paper - Optimize hoppers
     }
 
     @Nullable
     private static Container getContainerAt(Level level, BlockPos pos, BlockState state, double x, double y, double z) {
+        // Paper start - Perf: Optimize Hoppers
+        return HopperBlockEntity.getContainerAt(level, pos, state, x, y, z, false);
+    }
+    @Nullable
+    private static Container getContainerAt(Level level, BlockPos pos, BlockState state, double x, double y, double z, final boolean optimizeEntities) {
+        // Paper end - Perf: Optimize Hoppers
         Container blockContainer = getBlockContainer(level, pos, state);
-        if (blockContainer == null) {
+        if (blockContainer == null && (!optimizeEntities || !level.paperConfig().hopper.ignoreOccludingBlocks || !state.getBukkitMaterial().isOccluding())) { // Paper - Perf: Optimize Hoppers
             blockContainer = getEntityContainer(level, x, y, z);
         }
 
@@ -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);
@@ -383,14 +_,14 @@
 
     @Nullable
     private static Container getEntityContainer(Level level, double x, double y, double z) {
-        List<Entity> entities = level.getEntities(
-            (Entity)null, new AABB(x - 0.5, y - 0.5, z - 0.5, x + 0.5, y + 0.5, z + 0.5), EntitySelector.CONTAINER_ENTITY_SELECTOR
+        List<Entity> entities = level.getEntitiesOfClass(
+            (Class) Container.class, new AABB(x - 0.5, y - 0.5, z - 0.5, x + 0.5, y + 0.5, z + 0.5), EntitySelector.CONTAINER_ENTITY_SELECTOR // Paper - Perf: Optimize hoppers
         );
         return !entities.isEmpty() ? (Container)entities.get(level.random.nextInt(entities.size())) : null;
     }
 
     private static boolean canMergeItems(ItemStack stack1, ItemStack stack2) {
-        return stack1.getCount() <= stack1.getMaxStackSize() && ItemStack.isSameItemSameComponents(stack1, stack2);
+        return stack1.getCount() < stack1.getMaxStackSize() && ItemStack.isSameItemSameComponents(stack1, stack2); // Paper - Perf: Optimize Hoppers; used to return true for full itemstacks?!
     }
 
     @Override
@@ -413,7 +_,7 @@
         return true;
     }
 
-    private void setCooldown(int cooldownTime) {
+    public void setCooldown(int cooldownTime) {
         this.cooldownTime = cooldownTime;
     }
 
