aboutsummaryrefslogtreecommitdiffhomepage
path: root/paper-server/patches/features/0029-Optimize-Hoppers.patch
diff options
context:
space:
mode:
Diffstat (limited to 'paper-server/patches/features/0029-Optimize-Hoppers.patch')
-rw-r--r--paper-server/patches/features/0029-Optimize-Hoppers.patch683
1 files changed, 683 insertions, 0 deletions
diff --git a/paper-server/patches/features/0029-Optimize-Hoppers.patch b/paper-server/patches/features/0029-Optimize-Hoppers.patch
new file mode 100644
index 0000000000..c81544a817
--- /dev/null
+++ b/paper-server/patches/features/0029-Optimize-Hoppers.patch
@@ -0,0 +1,683 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+Date: Wed, 27 Apr 2016 22:09:52 -0400
+Subject: [PATCH] Optimize Hoppers
+
+* Removes unnecessary extra calls to .update() that are very expensive
+* Lots of itemstack cloning removed. Only clone if the item is actually moved
+* Return true when a plugin cancels inventory move item event instead of false, as false causes pulls to cycle through all items.
+ However, pushes do not exhibit the same behavior, so this is not something plugins could of been relying on.
+* Add option (Default on) to cooldown hoppers when they fail to move an item due to full inventory
+* Skip subsequent InventoryMoveItemEvents if a plugin does not use the item after first event fire for an iteration by tracking changes to the event via an internal event implementation.
+* Don't check for Entities with Inventories if the block above us is also occluding (not just Inventoried)
+* Remove Streams from Item Suck In and restore restore 1.12 AABB checks which is simpler and no voxel allocations (was doing TWO Item Suck ins)
+
+diff --git a/io/papermc/paper/event/inventory/PaperInventoryMoveItemEvent.java b/io/papermc/paper/event/inventory/PaperInventoryMoveItemEvent.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..24a2090e068ad3c0d08705050944abdfe19136a2
+--- /dev/null
++++ b/io/papermc/paper/event/inventory/PaperInventoryMoveItemEvent.java
+@@ -0,0 +1,29 @@
++package io.papermc.paper.event.inventory;
++
++import org.bukkit.event.inventory.InventoryMoveItemEvent;
++import org.bukkit.inventory.Inventory;
++import org.bukkit.inventory.ItemStack;
++import org.jspecify.annotations.NullMarked;
++
++@NullMarked
++public class PaperInventoryMoveItemEvent extends InventoryMoveItemEvent {
++
++ public boolean calledSetItem;
++ public boolean calledGetItem;
++
++ public PaperInventoryMoveItemEvent(final Inventory sourceInventory, final ItemStack itemStack, final Inventory destinationInventory, final boolean didSourceInitiate) {
++ super(sourceInventory, itemStack, destinationInventory, didSourceInitiate);
++ }
++
++ @Override
++ public ItemStack getItem() {
++ this.calledGetItem = true;
++ return super.getItem();
++ }
++
++ @Override
++ public void setItem(final ItemStack itemStack) {
++ super.setItem(itemStack);
++ this.calledSetItem = true;
++ }
++}
+diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
+index 6dbae12bbfd47cd4e75bc3089561e8e226e9e604..9c859025302ddb2c20cf6457fa4e4eaf7fbafdd7 100644
+--- a/net/minecraft/server/MinecraftServer.java
++++ b/net/minecraft/server/MinecraftServer.java
+@@ -1707,6 +1707,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ serverLevel.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - BlockPhysicsEvent
+ serverLevel.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent
+ serverLevel.updateLagCompensationTick(); // Paper - lag compensation
++ net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = serverLevel.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers
+ profilerFiller.push(() -> serverLevel + " " + serverLevel.dimension().location());
+ /* Drop global time updates
+ if (this.tickCount % 20 == 0) {
+diff --git a/net/minecraft/world/item/ItemStack.java b/net/minecraft/world/item/ItemStack.java
+index c255e11cb0981bd7e0456d4fd401beb5257be597..d6361863d6a1e364de262d6199373cbd68d1c699 100644
+--- a/net/minecraft/world/item/ItemStack.java
++++ b/net/minecraft/world/item/ItemStack.java
+@@ -808,10 +808,16 @@ public final class ItemStack implements DataComponentHolder {
+ }
+
+ public ItemStack copy() {
+- if (this.isEmpty()) {
++ // Paper start - Perf: Optimize Hoppers
++ return this.copy(false);
++ }
++
++ public ItemStack copy(final boolean originalItem) {
++ if (!originalItem && this.isEmpty()) {
++ // Paper end - Perf: Optimize Hoppers
+ return EMPTY;
+ } else {
+- ItemStack itemStack = new ItemStack(this.getItem(), this.count, this.components.copy());
++ ItemStack itemStack = new ItemStack(originalItem ? this.item : this.getItem(), this.count, this.components.copy()); // Paper - Perf: Optimize Hoppers
+ itemStack.setPopTime(this.getPopTime());
+ return itemStack;
+ }
+diff --git a/net/minecraft/world/level/block/entity/BlockEntity.java b/net/minecraft/world/level/block/entity/BlockEntity.java
+index 2ebdf1ad323bb53dfe9eed319e25856b35a1443c..77618757c0e678532dbab814aceed83f7f1cd892 100644
+--- a/net/minecraft/world/level/block/entity/BlockEntity.java
++++ b/net/minecraft/world/level/block/entity/BlockEntity.java
+@@ -26,6 +26,7 @@ import net.minecraft.world.level.block.state.BlockState;
+ import org.slf4j.Logger;
+
+ public abstract class BlockEntity {
++ static boolean ignoreBlockEntityUpdates; // Paper - Perf: Optimize Hoppers
+ // CraftBukkit start - data containers
+ private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry();
+ public org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer persistentDataContainer;
+@@ -196,6 +197,7 @@ public abstract class BlockEntity {
+
+ public void setChanged() {
+ if (this.level != null) {
++ if (ignoreBlockEntityUpdates) return; // Paper - Perf: Optimize Hoppers
+ setChanged(this.level, this.worldPosition, this.blockState);
+ }
+ }
+diff --git a/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/net/minecraft/world/level/block/entity/HopperBlockEntity.java
+index e58a32593e8b42bfc534d13457240860293dd3f4..5cd1326ad5d046c88b2b3449d610a78fa880b4cd 100644
+--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java
+@@ -139,18 +139,56 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+ }
+ }
+
++ // 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) {
+ return false;
+ } 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) {
+@@ -174,6 +212,206 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+ 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) {
+@@ -183,57 +421,60 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+ 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();
+- // 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 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
+ }
+ }
+ }
+@@ -288,6 +529,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+ 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, level)) { // Spigot
+@@ -313,55 +555,58 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+ 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();
+- // 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 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;
+@@ -370,13 +615,15 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+ 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(
+- container.getOwner().getInventory(), (org.bukkit.entity.Item) item.getBukkitEntity()
++ 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()) {
+@@ -431,7 +678,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+ stack = stack.split(destination.getMaxStackSize());
+ }
+ // Spigot end
++ ignoreBlockEntityUpdates = true; // Paper - Perf: Optimize Hoppers
+ destination.setItem(slot, stack);
++ ignoreBlockEntityUpdates = false; // Paper - Perf: Optimize Hoppers
+ stack = leftover; // Paper - Make hoppers respect inventory max stack size
+ flag = true;
+ } else if (canMergeItems(item, stack)) {
+@@ -519,13 +768,19 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+
+ @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);
+ }
+
+@@ -551,14 +806,14 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+
+ @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
+diff --git a/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java b/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java
+index 73b3ddb120d6b6f89e478960e78bed415baea205..f9c31da81d84033abfc1179fc643bceffe35da17 100644
+--- a/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java
+@@ -53,7 +53,7 @@ public abstract class RandomizableContainerBlockEntity extends BaseContainerBloc
+
+ @Override
+ public ItemStack getItem(int index) {
+- this.unpackLootTable(null);
++ if (index == 0) this.unpackLootTable(null); // Paper - Perf: Optimize Hoppers
+ return super.getItem(index);
+ }
+