--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java
+++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java
@@ -1,5 +1,6 @@
 package net.minecraft.world.level.block.entity;
 
+import java.util.Iterator;
 import java.util.List;
 import java.util.function.BooleanSupplier;
 import javax.annotation.Nullable;
@@ -10,6 +11,7 @@
 import net.minecraft.nbt.CompoundTag;
 import net.minecraft.network.chat.Component;
 import net.minecraft.tags.BlockTags;
+import net.minecraft.world.CompoundContainer;
 import net.minecraft.world.Container;
 import net.minecraft.world.ContainerHelper;
 import net.minecraft.world.WorldlyContainer;
@@ -17,7 +19,7 @@
 import net.minecraft.world.entity.Entity;
 import net.minecraft.world.entity.EntitySelector;
 import net.minecraft.world.entity.item.ItemEntity;
-import net.minecraft.world.entity.player.Inventory;
+import net.minecraft.world.entity.player.Player;
 import net.minecraft.world.inventory.AbstractContainerMenu;
 import net.minecraft.world.inventory.HopperMenu;
 import net.minecraft.world.item.ItemStack;
@@ -27,19 +29,64 @@
 import net.minecraft.world.level.block.HopperBlock;
 import net.minecraft.world.level.block.state.BlockState;
 import net.minecraft.world.phys.AABB;
+import org.bukkit.Bukkit;
+import org.bukkit.craftbukkit.block.CraftBlock;
+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
+import org.bukkit.craftbukkit.inventory.CraftInventory;
+import org.bukkit.craftbukkit.inventory.CraftItemStack;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.event.entity.EntityRemoveEvent;
+import org.bukkit.event.inventory.HopperInventorySearchEvent;
+import org.bukkit.event.inventory.InventoryMoveItemEvent;
+import org.bukkit.event.inventory.InventoryPickupItemEvent;
+import org.bukkit.inventory.Inventory;
+// CraftBukkit end
 
 public class HopperBlockEntity extends RandomizableContainerBlockEntity implements Hopper {
+
     public static final int MOVE_ITEM_SPEED = 8;
     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);
-    public int cooldownTime = -1;
+    private NonNullList<ItemStack> items;
+    public int cooldownTime;
     private long tickedGameTime;
     private Direction facing;
 
+    // CraftBukkit start - add fields and methods
+    public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
+    private int maxStack = MAX_STACK;
+
+    public List<ItemStack> getContents() {
+        return this.items;
+    }
+
+    public void onOpen(CraftHumanEntity who) {
+        this.transaction.add(who);
+    }
+
+    public void onClose(CraftHumanEntity who) {
+        this.transaction.remove(who);
+    }
+
+    public List<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 state) {
         super(BlockEntityType.HOPPER, pos, state);
-        this.facing = state.getValue(HopperBlock.FACING);
+        this.items = NonNullList.withSize(5, ItemStack.EMPTY);
+        this.cooldownTime = -1;
+        this.facing = (Direction) state.getValue(HopperBlock.FACING);
     }
 
     @Override
@@ -70,13 +117,13 @@
 
     @Override
     public ItemStack removeItem(int slot, int amount) {
-        this.unpackLootTable(null);
+        this.unpackLootTable((Player) null);
         return ContainerHelper.removeItem(this.getItems(), slot, amount);
     }
 
     @Override
     public void setItem(int slot, ItemStack stack) {
-        this.unpackLootTable(null);
+        this.unpackLootTable((Player) null);
         this.getItems().set(slot, stack);
         stack.limitSize(this.getMaxStackSize(stack));
     }
@@ -84,7 +131,7 @@
     @Override
     public void setBlockState(BlockState state) {
         super.setBlockState(state);
-        this.facing = state.getValue(HopperBlock.FACING);
+        this.facing = (Direction) state.getValue(HopperBlock.FACING);
     }
 
     @Override
@@ -93,30 +140,77 @@
     }
 
     public static void pushItemsTick(Level world, BlockPos pos, BlockState state, HopperBlockEntity blockEntity) {
-        blockEntity.cooldownTime--;
+        --blockEntity.cooldownTime;
         blockEntity.tickedGameTime = world.getGameTime();
         if (!blockEntity.isOnCooldown()) {
             blockEntity.setCooldown(0);
-            tryMoveItems(world, pos, state, blockEntity, () -> suckInItems(world, blockEntity));
+            // Spigot start
+            boolean result = HopperBlockEntity.tryMoveItems(world, pos, state, blockEntity, () -> {
+                return HopperBlockEntity.suckInItems(world, blockEntity);
+            });
+            if (!result && blockEntity.level.spigotConfig.hopperCheck > 1) {
+                blockEntity.setCooldown(blockEntity.level.spigotConfig.hopperCheck);
+            }
+            // Spigot end
         }
+
     }
 
-    private static boolean tryMoveItems(Level world, BlockPos pos, BlockState state, HopperBlockEntity blockEntity, BooleanSupplier booleanSupplier) {
+    // 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 tileEntity) {
+        tileEntity.unpackLootTable(null);
+
+        final List<ItemStack> hopperItems = tileEntity.getItems();
+
+        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 world, BlockPos pos, BlockState state, HopperBlockEntity blockEntity, BooleanSupplier booleansupplier) {
         if (world.isClientSide) {
             return false;
         } else {
-            if (!blockEntity.isOnCooldown() && state.getValue(HopperBlock.ENABLED)) {
-                boolean bl = false;
-                if (!blockEntity.isEmpty()) {
-                    bl = ejectItems(world, pos, blockEntity);
+            if (!blockEntity.isOnCooldown() && (Boolean) state.getValue(HopperBlock.ENABLED)) {
+                boolean flag = false;
+
+                final int fullState = getFullState(blockEntity); // Paper - Perf: Optimize Hoppers
+                if (fullState != HOPPER_EMPTY) { // Paper - Perf: Optimize Hoppers
+                    flag = HopperBlockEntity.ejectItems(world, pos, blockEntity);
                 }
 
-                if (!blockEntity.inventoryFull()) {
-                    bl |= booleanSupplier.getAsBoolean();
+                if (fullState != HOPPER_IS_FULL || flag) { // Paper - Perf: Optimize Hoppers
+                    flag |= booleansupplier.getAsBoolean();
                 }
 
-                if (bl) {
-                    blockEntity.setCooldown(8);
+                if (flag) {
+                    blockEntity.setCooldown(world.spigotConfig.hopperTransfer); // Spigot
                     setChanged(world, pos, state);
                     return true;
                 }
@@ -127,109 +221,355 @@
     }
 
     private boolean inventoryFull() {
-        for (ItemStack itemStack : this.items) {
-            if (itemStack.isEmpty() || itemStack.getCount() != itemStack.getMaxStackSize()) {
-                return false;
+        Iterator iterator = this.items.iterator();
+
+        ItemStack itemstack;
+
+        do {
+            if (!iterator.hasNext()) {
+                return true;
+            }
+
+            itemstack = (ItemStack) iterator.next();
+        } while (!itemstack.isEmpty() && itemstack.getCount() == itemstack.getMaxStackSize());
+
+        return false;
+    }
+
+    // Paper start - Perf: Optimize Hoppers
+    private static boolean skipPullModeEventFire;
+    private static boolean skipPushModeEventFire;
+    public static boolean skipHopperEvents;
+
+    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 of 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);
+            }
+
+            ignoreTileUpdates = true;
+            container.setItem(i, origItemStack);
+            ignoreTileUpdates = false;
+            container.setChanged();
+            return true;
+        }
+        origItemStack.setCount(originalItemCount);
+
+        if (level.paperConfig().hopper.cooldownWhenFull) {
+            cooldownHopper(hopper);
+        }
+
+        return false;
+    }
+
+    @Nullable
+    private static ItemStack callPushMoveEvent(Container iinventory, ItemStack itemstack, HopperBlockEntity hopper) {
+        final Inventory destinationInventory = getInventory(iinventory);
+        final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(hopper.getOwner(false).getInventory(),
+            CraftItemStack.asCraftMirror(itemstack), destinationInventory, true);
+        final boolean result = event.callEvent();
+        if (!event.calledGetItem && !event.calledSetItem) {
+            skipPushModeEventFire = true;
+        }
+        if (!result) {
+            cooldownHopper(hopper);
+            return null;
+        }
+
+        if (event.calledSetItem) {
+            return CraftItemStack.asNMSCopy(event.getItem());
+        } else {
+            return itemstack;
+        }
+    }
+
+    @Nullable
+    private static ItemStack callPullMoveEvent(final Hopper hopper, final Container container, final ItemStack itemstack) {
+        final Inventory sourceInventory = getInventory(container);
+        final 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, CraftItemStack.asCraftMirror(itemstack), destination, false);
+        final boolean result = event.callEvent();
+        if (!event.calledGetItem && !event.calledSetItem) {
+            skipPullModeEventFire = true;
+        }
+        if (!result) {
+            cooldownHopper(hopper);
+            return null;
+        }
+
+        if (event.calledSetItem) {
+            return CraftItemStack.asNMSCopy(event.getItem());
+        } else {
+            return itemstack;
+        }
+    }
+
+    private static Inventory getInventory(final Container container) {
+        final Inventory sourceInventory;
+        if (container instanceof 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 CraftInventory(container);
+        }
+        return sourceInventory;
+    }
+
+    private static void cooldownHopper(final Hopper hopper) {
+        if (hopper instanceof HopperBlockEntity blockEntity && blockEntity.getLevel() != null) {
+            blockEntity.setCooldown(blockEntity.getLevel().spigotConfig.hopperTransfer);
+        }
+    }
+
+    private static boolean allMatch(Container iinventory, Direction enumdirection, java.util.function.BiPredicate<ItemStack, Integer> test) {
+        if (iinventory instanceof WorldlyContainer) {
+            for (int i : ((WorldlyContainer) iinventory).getSlotsForFace(enumdirection)) {
+                if (!test.test(iinventory.getItem(i), i)) {
+                    return false;
+                }
+            }
+        } else {
+            int size = iinventory.getContainerSize();
+            for (int i = 0; i < size; i++) {
+                if (!test.test(iinventory.getItem(i), i)) {
+                    return false;
+                }
             }
         }
+        return true;
+    }
 
+    private static boolean anyMatch(Container iinventory, Direction enumdirection, java.util.function.BiPredicate<ItemStack, Integer> test) {
+        if (iinventory instanceof WorldlyContainer) {
+            for (int i : ((WorldlyContainer) iinventory).getSlotsForFace(enumdirection)) {
+                if (test.test(iinventory.getItem(i), i)) {
+                    return true;
+                }
+            }
+        } else {
+            int size = iinventory.getContainerSize();
+            for (int i = 0; i < size; i++) {
+                if (test.test(iinventory.getItem(i), i)) {
+                    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 world, BlockPos pos, HopperBlockEntity blockEntity) {
-        Container container = getAttachedContainer(world, pos, blockEntity);
-        if (container == null) {
+        Container iinventory = HopperBlockEntity.getAttachedContainer(world, pos, blockEntity);
+
+        if (iinventory == null) {
             return false;
         } else {
-            Direction direction = blockEntity.facing.getOpposite();
-            if (isFullContainer(container, direction)) {
+            Direction enumdirection = blockEntity.facing.getOpposite();
+
+            if (HopperBlockEntity.isFullContainer(iinventory, enumdirection)) {
                 return false;
             } else {
-                for (int i = 0; i < blockEntity.getContainerSize(); i++) {
-                    ItemStack itemStack = blockEntity.getItem(i);
-                    if (!itemStack.isEmpty()) {
-                        int j = itemStack.getCount();
-                        ItemStack itemStack2 = addItem(blockEntity, container, blockEntity.removeItem(i, 1), direction);
-                        if (itemStack2.isEmpty()) {
-                            container.setChanged();
-                            return true;
-                        }
+                // Paper start - Perf: Optimize Hoppers
+                return hopperPush(world, iinventory, enumdirection, blockEntity);
+                //for (int i = 0; i < blockEntity.getContainerSize(); ++i) {
+                //    ItemStack itemstack = blockEntity.getItem(i);
 
-                        itemStack.setCount(j);
-                        if (j == 1) {
-                            blockEntity.setItem(i, itemStack);
-                        }
-                    }
-                }
+                //    if (!itemstack.isEmpty()) {
+                //        int j = itemstack.getCount();
+                //        // CraftBukkit start - Call event when pushing items into other inventories
+                //        CraftItemStack oitemstack = CraftItemStack.asCraftMirror(blockEntity.removeItem(i, world.spigotConfig.hopperAmount)); // Spigot
 
-                return false;
+                //        Inventory destinationInventory;
+                //        // Have to special case large chests as they work oddly
+                //        if (iinventory instanceof CompoundContainer) {
+                //            destinationInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) iinventory);
+                //        } else if (iinventory.getOwner() != null) {
+                //            destinationInventory = iinventory.getOwner().getInventory();
+                //        } else {
+                //            destinationInventory = new CraftInventory(iinventory);
+                //        }
+
+                //        InventoryMoveItemEvent event = new InventoryMoveItemEvent(tileentityhopper.getOwner().getInventory(), oitemstack, destinationInventory, true);
+                //        world.getCraftServer().getPluginManager().callEvent(event);
+                //        if (event.isCancelled()) {
+                //            blockEntity.setItem(i, itemstack);
+                //            blockEntity.setCooldown(world.spigotConfig.hopperTransfer); // Delay hopper checks // Spigot
+                //            return false;
+                //        }
+                //        ItemStack itemstack1 = HopperBlockEntity.addItem(blockEntity, iinventory, CraftItemStack.asNMSCopy(event.getItem()), enumdirection);
+                //        // CraftBukkit end
+
+                //        if (itemstack1.isEmpty()) {
+                //            iinventory.setChanged();
+                //            return true;
+                //        }
+
+                //        itemstack.setCount(j);
+                //        if (j == 1) {
+                //            blockEntity.setItem(i, itemstack);
+                //        }
+                //    }
+                //}
+
+                // return false;
+                // Paper end - Perf: Optimize Hoppers
             }
         }
     }
 
     private static int[] getSlots(Container inventory, Direction side) {
-        if (inventory instanceof WorldlyContainer worldlyContainer) {
-            return worldlyContainer.getSlotsForFace(side);
+        if (inventory instanceof WorldlyContainer iworldinventory) {
+            return iworldinventory.getSlotsForFace(side);
         } else {
             int i = inventory.getContainerSize();
-            if (i < CACHED_SLOTS.length) {
-                int[] is = CACHED_SLOTS[i];
-                if (is != null) {
-                    return is;
+
+            if (i < HopperBlockEntity.CACHED_SLOTS.length) {
+                int[] aint = HopperBlockEntity.CACHED_SLOTS[i];
+
+                if (aint != null) {
+                    return aint;
                 } else {
-                    int[] js = createFlatSlots(i);
-                    CACHED_SLOTS[i] = js;
-                    return js;
+                    int[] aint1 = HopperBlockEntity.createFlatSlots(i);
+
+                    HopperBlockEntity.CACHED_SLOTS[i] = aint1;
+                    return aint1;
                 }
             } else {
-                return createFlatSlots(i);
+                return HopperBlockEntity.createFlatSlots(i);
             }
         }
     }
 
     private static int[] createFlatSlots(int size) {
-        int[] is = new int[size];
-        int i = 0;
+        int[] aint = new int[size];
 
-        while (i < is.length) {
-            is[i] = i++;
+        for (int j = 0; j < aint.length; aint[j] = j++) {
+            ;
         }
 
-        return is;
+        return aint;
     }
 
     private static boolean isFullContainer(Container inventory, Direction direction) {
-        int[] is = getSlots(inventory, direction);
+        int[] aint = HopperBlockEntity.getSlots(inventory, direction);
+        int[] aint1 = aint;
+        int i = aint.length;
 
-        for (int i : is) {
-            ItemStack itemStack = inventory.getItem(i);
-            if (itemStack.getCount() < itemStack.getMaxStackSize()) {
+        for (int j = 0; j < i; ++j) {
+            int k = aint1[j];
+            ItemStack itemstack = inventory.getItem(k);
+
+            if (itemstack.getCount() < itemstack.getMaxStackSize()) {
                 return false;
             }
         }
-
         return true;
     }
 
     public static boolean suckInItems(Level world, Hopper hopper) {
-        BlockPos blockPos = BlockPos.containing(hopper.getLevelX(), hopper.getLevelY() + 1.0, hopper.getLevelZ());
-        BlockState blockState = world.getBlockState(blockPos);
-        Container container = getSourceContainer(world, hopper, blockPos, blockState);
-        if (container != null) {
-            Direction direction = Direction.DOWN;
+        BlockPos blockposition = BlockPos.containing(hopper.getLevelX(), hopper.getLevelY() + 1.0D, hopper.getLevelZ());
+        BlockState iblockdata = world.getBlockState(blockposition);
+        Container iinventory = HopperBlockEntity.getSourceContainer(world, hopper, blockposition, iblockdata);
 
-            for (int i : getSlots(container, direction)) {
-                if (tryTakeInItemFromSlot(hopper, container, i, direction)) {
+        if (iinventory != null) {
+            Direction enumdirection = Direction.DOWN;
+            skipPullModeEventFire = skipHopperEvents; // Paper - Perf: Optimize Hoppers
+            int[] aint = HopperBlockEntity.getSlots(iinventory, enumdirection);
+            int i = aint.length;
+
+            for (int j = 0; j < i; ++j) {
+                int k = aint[j];
+
+                if (HopperBlockEntity.tryTakeInItemFromSlot(hopper, iinventory, k, enumdirection, world)) { // Spigot
                     return true;
                 }
             }
 
             return false;
         } else {
-            boolean bl = hopper.isGridAligned() && blockState.isCollisionShapeFullBlock(world, blockPos) && !blockState.is(BlockTags.DOES_NOT_BLOCK_HOPPERS);
-            if (!bl) {
-                for (ItemEntity itemEntity : getItemsAtAndAbove(world, hopper)) {
-                    if (addItem(hopper, itemEntity)) {
+            boolean flag = hopper.isGridAligned() && iblockdata.isCollisionShapeFullBlock(world, blockposition) && !iblockdata.is(BlockTags.DOES_NOT_BLOCK_HOPPERS);
+
+            if (!flag) {
+                Iterator iterator = HopperBlockEntity.getItemsAtAndAbove(world, hopper).iterator();
+
+                while (iterator.hasNext()) {
+                    ItemEntity entityitem = (ItemEntity) iterator.next();
+
+                    if (HopperBlockEntity.addItem(hopper, entityitem)) {
                         return true;
                     }
                 }
@@ -239,55 +579,100 @@
         }
     }
 
-    private static boolean tryTakeInItemFromSlot(Hopper hopper, Container inventory, int slot, Direction side) {
-        ItemStack itemStack = inventory.getItem(slot);
-        if (!itemStack.isEmpty() && canTakeItemFromContainer(hopper, inventory, itemStack, slot, side)) {
-            int i = itemStack.getCount();
-            ItemStack itemStack2 = addItem(inventory, hopper, inventory.removeItem(slot, 1), null);
-            if (itemStack2.isEmpty()) {
-                inventory.setChanged();
-                return true;
-            }
+    private static boolean tryTakeInItemFromSlot(Hopper ihopper, Container iinventory, int i, Direction enumdirection, Level world) { // Spigot
+        ItemStack itemstack = iinventory.getItem(i);
 
-            itemStack.setCount(i);
-            if (i == 1) {
-                inventory.setItem(slot, itemStack);
-            }
+        if (!itemstack.isEmpty() && HopperBlockEntity.canTakeItemFromContainer(ihopper, iinventory, itemstack, i, enumdirection)) {
+            // Paper start - Perf: Optimize Hoppers
+            return hopperPull(world, ihopper, iinventory, itemstack, i);
+        //    int j = itemstack.getCount();
+        //    // CraftBukkit start - Call event on collection of items from inventories into the hopper
+        //    CraftItemStack oitemstack = CraftItemStack.asCraftMirror(iinventory.removeItem(i, world.spigotConfig.hopperAmount)); // Spigot
+
+        //    Inventory sourceInventory;
+        //    // Have to special case large chests as they work oddly
+        //    if (iinventory instanceof CompoundContainer) {
+        //        sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) iinventory);
+        //    } else if (iinventory.getOwner() != null) {
+        //        sourceInventory = iinventory.getOwner().getInventory();
+        //    } else {
+        //        sourceInventory = new CraftInventory(iinventory);
+        //    }
+
+        //    InventoryMoveItemEvent event = new InventoryMoveItemEvent(sourceInventory, oitemstack, ihopper.getOwner().getInventory(), false);
+
+        //    Bukkit.getServer().getPluginManager().callEvent(event);
+        //    if (event.isCancelled()) {
+        //        iinventory.setItem(i, itemstack);
+
+        //        if (ihopper instanceof HopperBlockEntity) {
+        //            ((HopperBlockEntity) ihopper).setCooldown(world.spigotConfig.hopperTransfer); // Spigot
+        //        }
+
+        //        return false;
+        //    }
+        //    ItemStack itemstack1 = HopperBlockEntity.addItem(iinventory, ihopper, CraftItemStack.asNMSCopy(event.getItem()), null);
+        //    // CraftBukkit end
+
+        //    if (itemstack1.isEmpty()) {
+        //        iinventory.setChanged();
+        //        return true;
+        //    }
+
+        //    itemstack.setCount(j);
+        //    if (j == 1) {
+        //        iinventory.setItem(i, itemstack);
+        //    }
+            // Paper end - Perf: Optimize Hoppers
         }
 
         return false;
     }
 
     public static boolean addItem(Container inventory, ItemEntity itemEntity) {
-        boolean bl = false;
-        ItemStack itemStack = itemEntity.getItem().copy();
-        ItemStack itemStack2 = addItem(null, inventory, itemStack, null);
-        if (itemStack2.isEmpty()) {
-            bl = true;
+        boolean flag = false;
+        // CraftBukkit start
+        if (InventoryPickupItemEvent.getHandlerList().getRegisteredListeners().length > 0) { // Paper - optimize hoppers
+        InventoryPickupItemEvent event = new InventoryPickupItemEvent(getInventory(inventory), (org.bukkit.entity.Item) itemEntity.getBukkitEntity()); // Paper - Perf: Optimize Hoppers; use getInventory() to avoid snapshot creation
+        itemEntity.level().getCraftServer().getPluginManager().callEvent(event);
+        if (event.isCancelled()) {
+            return false;
+        }
+        // CraftBukkit end
+        } // Paper - Perf: Optimize Hoppers
+        ItemStack itemstack = itemEntity.getItem().copy();
+        ItemStack itemstack1 = HopperBlockEntity.addItem((Container) null, inventory, itemstack, (Direction) null);
+
+        if (itemstack1.isEmpty()) {
+            flag = true;
             itemEntity.setItem(ItemStack.EMPTY);
-            itemEntity.discard();
+            itemEntity.discard(EntityRemoveEvent.Cause.PICKUP); // CraftBukkit - add Bukkit remove cause
         } else {
-            itemEntity.setItem(itemStack2);
+            itemEntity.setItem(itemstack1);
         }
 
-        return bl;
+        return flag;
     }
 
     public static ItemStack addItem(@Nullable Container from, Container to, ItemStack stack, @Nullable Direction side) {
-        if (to instanceof WorldlyContainer worldlyContainer && side != null) {
-            int[] is = worldlyContainer.getSlotsForFace(side);
+        int i;
 
-            for (int i = 0; i < is.length && !stack.isEmpty(); i++) {
-                stack = tryMoveInItem(from, to, stack, is[i], side);
-            }
+        if (to instanceof WorldlyContainer iworldinventory) {
+            if (side != null) {
+                int[] aint = iworldinventory.getSlotsForFace(side);
 
-            return stack;
+                for (i = 0; i < aint.length && !stack.isEmpty(); ++i) {
+                    stack = HopperBlockEntity.tryMoveInItem(from, to, stack, aint[i], side);
+                }
+
+                return stack;
+            }
         }
 
         int j = to.getContainerSize();
 
-        for (int k = 0; k < j && !stack.isEmpty(); k++) {
-            stack = tryMoveInItem(from, to, stack, k, side);
+        for (i = 0; i < j && !stack.isEmpty(); ++i) {
+            stack = HopperBlockEntity.tryMoveInItem(from, to, stack, i, side);
         }
 
         return stack;
@@ -297,11 +682,19 @@
         if (!inventory.canPlaceItem(slot, stack)) {
             return false;
         } else {
-            if (inventory instanceof WorldlyContainer worldlyContainer && !worldlyContainer.canPlaceItemThroughFace(slot, stack, side)) {
-                return false;
+            boolean flag;
+
+            if (inventory instanceof WorldlyContainer) {
+                WorldlyContainer iworldinventory = (WorldlyContainer) inventory;
+
+                if (!iworldinventory.canPlaceItemThroughFace(slot, stack, side)) {
+                    flag = false;
+                    return flag;
+                }
             }
 
-            return true;
+            flag = true;
+            return flag;
         }
     }
 
@@ -309,39 +702,68 @@
         if (!fromInventory.canTakeItem(hopperInventory, slot, stack)) {
             return false;
         } else {
-            if (fromInventory instanceof WorldlyContainer worldlyContainer && !worldlyContainer.canTakeItemThroughFace(slot, stack, facing)) {
-                return false;
+            boolean flag;
+
+            if (fromInventory instanceof WorldlyContainer) {
+                WorldlyContainer iworldinventory = (WorldlyContainer) fromInventory;
+
+                if (!iworldinventory.canTakeItemThroughFace(slot, stack, facing)) {
+                    flag = false;
+                    return flag;
+                }
             }
 
-            return true;
+            flag = true;
+            return flag;
         }
     }
 
     private static ItemStack tryMoveInItem(@Nullable Container from, Container to, ItemStack stack, int slot, @Nullable Direction side) {
-        ItemStack itemStack = to.getItem(slot);
-        if (canPlaceItemInContainer(to, stack, slot, side)) {
-            boolean bl = false;
-            boolean bl2 = to.isEmpty();
-            if (itemStack.isEmpty()) {
+        ItemStack itemstack1 = to.getItem(slot);
+
+        if (HopperBlockEntity.canPlaceItemInContainer(to, stack, slot, side)) {
+            boolean flag = false;
+            boolean flag1 = to.isEmpty();
+
+            if (itemstack1.isEmpty()) {
+                // Spigot start - SPIGOT-6693, InventorySubcontainer#setItem
+                ItemStack leftover = ItemStack.EMPTY; // Paper - Make hoppers respect inventory max stack size
+                if (!stack.isEmpty() && stack.getCount() > to.getMaxStackSize()) {
+                    leftover = stack; // Paper - Make hoppers respect inventory max stack size
+                    stack = stack.split(to.getMaxStackSize());
+                }
+                // Spigot end
+                ignoreTileUpdates = true; // Paper - Perf: Optimize Hoppers
                 to.setItem(slot, stack);
-                stack = ItemStack.EMPTY;
-                bl = true;
-            } else if (canMergeItems(itemStack, stack)) {
-                int i = stack.getMaxStackSize() - itemStack.getCount();
-                int j = Math.min(stack.getCount(), i);
-                stack.shrink(j);
-                itemStack.grow(j);
-                bl = j > 0;
+                ignoreTileUpdates = false; // Paper - Perf: Optimize Hoppers
+                stack = leftover; // Paper - Make hoppers respect inventory max stack size
+                flag = true;
+            } else if (HopperBlockEntity.canMergeItems(itemstack1, stack)) {
+                int j = Math.min(stack.getMaxStackSize(), to.getMaxStackSize()) - itemstack1.getCount(); // Paper - Make hoppers respect inventory max stack size
+                int k = Math.min(stack.getCount(), j);
+
+                stack.shrink(k);
+                itemstack1.grow(k);
+                flag = k > 0;
             }
 
-            if (bl) {
-                if (bl2 && to instanceof HopperBlockEntity hopperBlockEntity && !hopperBlockEntity.isOnCustomCooldown()) {
-                    int k = 0;
-                    if (from instanceof HopperBlockEntity hopperBlockEntity2 && hopperBlockEntity.tickedGameTime >= hopperBlockEntity2.tickedGameTime) {
-                        k = 1;
-                    }
+            if (flag) {
+                if (flag1 && to instanceof HopperBlockEntity) {
+                    HopperBlockEntity tileentityhopper = (HopperBlockEntity) to;
 
-                    hopperBlockEntity.setCooldown(8 - k);
+                    if (!tileentityhopper.isOnCustomCooldown()) {
+                        byte b0 = 0;
+
+                        if (from instanceof HopperBlockEntity) {
+                            HopperBlockEntity tileentityhopper1 = (HopperBlockEntity) from;
+
+                            if (tileentityhopper.tickedGameTime >= tileentityhopper1.tickedGameTime) {
+                                b0 = 1;
+                            }
+                        }
+
+                        tileentityhopper.setCooldown(tileentityhopper.level.spigotConfig.hopperTransfer - b0); // Spigot
+                    }
                 }
 
                 to.setChanged();
@@ -351,77 +773,118 @@
         return stack;
     }
 
+    // CraftBukkit start
+    @Nullable
+    private static Container runHopperInventorySearchEvent(Container inventory, CraftBlock hopper, CraftBlock searchLocation, HopperInventorySearchEvent.ContainerType containerType) {
+        HopperInventorySearchEvent event = new HopperInventorySearchEvent((inventory != null) ? new CraftInventory(inventory) : null, containerType, hopper, searchLocation);
+        Bukkit.getServer().getPluginManager().callEvent(event);
+        CraftInventory craftInventory = (CraftInventory) event.getInventory();
+        return (craftInventory != null) ? craftInventory.getInventory() : null;
+    }
+    // CraftBukkit end
+
     @Nullable
     private static Container getAttachedContainer(Level world, BlockPos pos, HopperBlockEntity blockEntity) {
-        return getContainerAt(world, pos.relative(blockEntity.facing));
+        // CraftBukkit start
+        BlockPos searchPosition = pos.relative(blockEntity.facing);
+        Container inventory = HopperBlockEntity.getContainerAt(world, searchPosition);
+
+        CraftBlock hopper = CraftBlock.at(world, pos);
+        CraftBlock searchBlock = CraftBlock.at(world, searchPosition);
+        return HopperBlockEntity.runHopperInventorySearchEvent(inventory, hopper, searchBlock, HopperInventorySearchEvent.ContainerType.DESTINATION);
+        // CraftBukkit end
     }
 
     @Nullable
     private static Container getSourceContainer(Level world, Hopper hopper, BlockPos pos, BlockState state) {
-        return getContainerAt(world, pos, state, hopper.getLevelX(), hopper.getLevelY() + 1.0, hopper.getLevelZ());
+        // CraftBukkit start
+        Container inventory = HopperBlockEntity.getContainerAt(world, pos, state, hopper.getLevelX(), hopper.getLevelY() + 1.0D, hopper.getLevelZ());
+
+        BlockPos blockPosition = BlockPos.containing(hopper.getLevelX(), hopper.getLevelY(), hopper.getLevelZ());
+        CraftBlock hopper1 = CraftBlock.at(world, blockPosition);
+        CraftBlock container = CraftBlock.at(world, blockPosition.above());
+        return HopperBlockEntity.runHopperInventorySearchEvent(inventory, hopper1, container, HopperInventorySearchEvent.ContainerType.SOURCE);
+        // CraftBukkit end
     }
 
     public static List<ItemEntity> getItemsAtAndAbove(Level world, Hopper hopper) {
-        AABB aABB = hopper.getSuckAabb().move(hopper.getLevelX() - 0.5, hopper.getLevelY() - 0.5, hopper.getLevelZ() - 0.5);
-        return world.getEntitiesOfClass(ItemEntity.class, aABB, EntitySelector.ENTITY_STILL_ALIVE);
+        AABB axisalignedbb = hopper.getSuckAabb().move(hopper.getLevelX() - 0.5D, hopper.getLevelY() - 0.5D, hopper.getLevelZ() - 0.5D);
+
+        return world.getEntitiesOfClass(ItemEntity.class, axisalignedbb, EntitySelector.ENTITY_STILL_ALIVE);
     }
 
     @Nullable
     public static Container getContainerAt(Level world, BlockPos pos) {
-        return getContainerAt(world, pos, world.getBlockState(pos), (double)pos.getX() + 0.5, (double)pos.getY() + 0.5, (double)pos.getZ() + 0.5);
+        return HopperBlockEntity.getContainerAt(world, pos, world.getBlockState(pos), (double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, true);
     }
 
     @Nullable
     private static Container getContainerAt(Level world, BlockPos pos, BlockState state, double x, double y, double z) {
-        Container container = getBlockContainer(world, pos, state);
-        if (container == null) {
-            container = getEntityContainer(world, x, y, z);
+        // Paper start - Perf: Optimize Hoppers
+        return HopperBlockEntity.getContainerAt(world, pos, state, x, y, z, false);
+    }
+    @Nullable
+    private static Container getContainerAt(Level world, BlockPos pos, BlockState state, double x, double y, double z, boolean optimizeEntities) {
+        // Paper end - Perf: Optimize Hoppers
+        Container iinventory = HopperBlockEntity.getBlockContainer(world, pos, state);
+
+        if (iinventory == null && (!optimizeEntities || !world.paperConfig().hopper.ignoreOccludingBlocks || !state.getBukkitMaterial().isOccluding())) { // Paper - Perf: Optimize Hoppers
+            iinventory = HopperBlockEntity.getEntityContainer(world, x, y, z);
         }
 
-        return container;
+        return iinventory;
     }
 
     @Nullable
     private static Container getBlockContainer(Level world, BlockPos pos, BlockState state) {
+        if ( !world.spigotConfig.hopperCanLoadChunks && !world.hasChunkAt( pos ) ) return null; // Spigot
         Block block = state.getBlock();
+
         if (block instanceof WorldlyContainerHolder) {
-            return ((WorldlyContainerHolder)block).getContainer(state, world, pos);
-        } else if (state.hasBlockEntity() && world.getBlockEntity(pos) instanceof Container container) {
-            if (container instanceof ChestBlockEntity && block instanceof ChestBlock) {
-                container = ChestBlock.getContainer((ChestBlock)block, state, world, pos, true);
+            return ((WorldlyContainerHolder) block).getContainer(state, world, pos);
+        } else {
+            if (state.hasBlockEntity()) {
+                BlockEntity tileentity = world.getBlockEntity(pos);
+
+                if (tileentity instanceof Container) {
+                    Container iinventory = (Container) tileentity;
+
+                    if (iinventory instanceof ChestBlockEntity && block instanceof ChestBlock) {
+                        iinventory = ChestBlock.getContainer((ChestBlock) block, state, world, pos, true);
+                    }
+
+                    return iinventory;
+                }
             }
 
-            return container;
-        } else {
             return null;
         }
     }
 
     @Nullable
     private static Container getEntityContainer(Level world, double x, double y, double z) {
-        List<Entity> list = world.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
-        );
-        return !list.isEmpty() ? (Container)list.get(world.random.nextInt(list.size())) : null;
+        List<Entity> list = world.getEntitiesOfClass((Class) Container.class, new AABB(x - 0.5D, y - 0.5D, z - 0.5D, x + 0.5D, y + 0.5D, z + 0.5D), EntitySelector.CONTAINER_ENTITY_SELECTOR); // Paper - Perf: Optimize hoppers
+
+        return !list.isEmpty() ? (Container) list.get(world.random.nextInt(list.size())) : null;
     }
 
     private static boolean canMergeItems(ItemStack first, ItemStack second) {
-        return first.getCount() <= first.getMaxStackSize() && ItemStack.isSameItemSameComponents(first, second);
+        return first.getCount() < first.getMaxStackSize() && ItemStack.isSameItemSameComponents(first, second); // Paper - Perf: Optimize Hoppers; used to return true for full itemstacks?!
     }
 
     @Override
     public double getLevelX() {
-        return (double)this.worldPosition.getX() + 0.5;
+        return (double) this.worldPosition.getX() + 0.5D;
     }
 
     @Override
     public double getLevelY() {
-        return (double)this.worldPosition.getY() + 0.5;
+        return (double) this.worldPosition.getY() + 0.5D;
     }
 
     @Override
     public double getLevelZ() {
-        return (double)this.worldPosition.getZ() + 0.5;
+        return (double) this.worldPosition.getZ() + 0.5D;
     }
 
     @Override
@@ -452,15 +915,18 @@
     }
 
     public static void entityInside(Level world, BlockPos pos, BlockState state, Entity entity, HopperBlockEntity blockEntity) {
-        if (entity instanceof ItemEntity itemEntity
-            && !itemEntity.getItem().isEmpty()
-            && entity.getBoundingBox().move((double)(-pos.getX()), (double)(-pos.getY()), (double)(-pos.getZ())).intersects(blockEntity.getSuckAabb())) {
-            tryMoveItems(world, pos, state, blockEntity, () -> addItem(blockEntity, itemEntity));
+        if (entity instanceof ItemEntity entityitem) {
+            if (!entityitem.getItem().isEmpty() && entity.getBoundingBox().move((double) (-pos.getX()), (double) (-pos.getY()), (double) (-pos.getZ())).intersects(blockEntity.getSuckAabb())) {
+                HopperBlockEntity.tryMoveItems(world, pos, state, blockEntity, () -> {
+                    return HopperBlockEntity.addItem(blockEntity, entityitem);
+                });
+            }
         }
+
     }
 
     @Override
-    protected AbstractContainerMenu createMenu(int syncId, Inventory playerInventory) {
+    protected AbstractContainerMenu createMenu(int syncId, net.minecraft.world.entity.player.Inventory playerInventory) {
         return new HopperMenu(syncId, playerInventory, this);
     }
 }
