aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/0342-Optimize-Hoppers.patch
diff options
context:
space:
mode:
Diffstat (limited to 'patches/server/0342-Optimize-Hoppers.patch')
-rw-r--r--patches/server/0342-Optimize-Hoppers.patch482
1 files changed, 482 insertions, 0 deletions
diff --git a/patches/server/0342-Optimize-Hoppers.patch b/patches/server/0342-Optimize-Hoppers.patch
new file mode 100644
index 0000000000..66df27dd80
--- /dev/null
+++ b/patches/server/0342-Optimize-Hoppers.patch
@@ -0,0 +1,482 @@
+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
+* 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/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
+index 2f060bc6b108df0c4fb07758005a911d92c09057..73d82e20d7353120a36815dc94be8c2253b2602d 100644
+--- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
++++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
+@@ -506,5 +506,17 @@ public class PaperWorldConfig {
+ private void entitiesTargetWithFollowRange() {
+ entitiesTargetWithFollowRange = getBoolean("entities-target-with-follow-range", entitiesTargetWithFollowRange);
+ }
++
++ public boolean cooldownHopperWhenFull = true;
++ public boolean disableHopperMoveEvents = false;
++ public boolean hoppersIgnoreOccludingBlocks = false;
++ private void hopperOptimizations() {
++ cooldownHopperWhenFull = getBoolean("hopper.cooldown-when-full", cooldownHopperWhenFull);
++ log("Cooldown Hoppers when Full: " + (cooldownHopperWhenFull ? "enabled" : "disabled"));
++ disableHopperMoveEvents = getBoolean("hopper.disable-move-event", disableHopperMoveEvents);
++ log("Hopper Move Item Events: " + (disableHopperMoveEvents ? "disabled" : "enabled"));
++ hoppersIgnoreOccludingBlocks = getBoolean("hopper.ignore-occluding-blocks", hoppersIgnoreOccludingBlocks);
++ log("Hopper Ignore Container Entities inside Occluding Blocks: " + (hoppersIgnoreOccludingBlocks ? "enabled" : "disabled"));
++ }
+ }
+
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index e75ca992a5fc0cc16b5d365287f63215c124759b..8e49a648671b4d4c0d07af35ba6558b63bbf28ee 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -1455,6 +1455,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ while (iterator.hasNext()) {
+ ServerLevel worldserver = (ServerLevel) iterator.next();
+ worldserver.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper
++ net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig.disableHopperMoveEvents || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper
+
+ this.profiler.push(() -> {
+ return worldserver + " " + worldserver.dimension().location();
+diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java
+index f1289d7251783c5203828c2b76785dd22c7e2992..b78769cf6a70fb7513ea8d3302cb2dd4bf242752 100644
+--- a/src/main/java/net/minecraft/world/item/ItemStack.java
++++ b/src/main/java/net/minecraft/world/item/ItemStack.java
+@@ -613,11 +613,12 @@ public final class ItemStack {
+ return this.getItem().interactLivingEntity(this, user, entity, hand);
+ }
+
+- public ItemStack copy() {
+- if (this.isEmpty()) {
++ public ItemStack copy() { return cloneItemStack(false); } // Paper
++ public ItemStack cloneItemStack(boolean origItem) { // Paper
++ if (!origItem && this.isEmpty()) { // Paper
+ return ItemStack.EMPTY;
+ } else {
+- ItemStack itemstack = new ItemStack(this.getItem(), this.count);
++ ItemStack itemstack = new ItemStack(origItem ? this.item : this.getItem(), this.count); // Paper
+
+ itemstack.setPopTime(this.getPopTime());
+ if (this.tag != null) {
+diff --git a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java
+index a69bbc11b9e003aed1630a6f5cdbca521deb6ff5..e838abb6d258a1ab47e95572cf4c30c6ca6144b4 100644
+--- a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java
++++ b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java
+@@ -70,6 +70,7 @@ public abstract class BlockEntity implements io.papermc.paper.util.KeyedObject {
+ getMinecraftKey(); // Try to load if it doesn't exists.
+ return tileEntityKeyString;
+ }
++ static boolean IGNORE_TILE_UPDATES = false;
+ // Paper end
+
+ @Nullable
+@@ -182,6 +183,7 @@ public abstract class BlockEntity implements io.papermc.paper.util.KeyedObject {
+
+ public void setChanged() {
+ if (this.level != null) {
++ if (IGNORE_TILE_UPDATES) return; // Paper
+ BlockEntity.setChanged(this.level, this.worldPosition, this.blockState);
+ }
+
+diff --git a/src/main/java/net/minecraft/world/level/block/entity/Hopper.java b/src/main/java/net/minecraft/world/level/block/entity/Hopper.java
+index a05acf709735b40ca86f978508c63a86065fd405..6a1405a8630e90db3b5a3c9152259ba6f5f0c784 100644
+--- a/src/main/java/net/minecraft/world/level/block/entity/Hopper.java
++++ b/src/main/java/net/minecraft/world/level/block/entity/Hopper.java
+@@ -14,6 +14,8 @@ public interface Hopper extends Container {
+ return SUCK;
+ }
+
++ default net.minecraft.core.BlockPos getBlockPosition() { return new net.minecraft.core.BlockPos(getLevelX(), getLevelY(), getLevelZ()); } // Paper
++
+ double getLevelX();
+
+ double getLevelY();
+diff --git a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java
+index 43b0e6a4c2bcd1d2026add39f64b5993eb614d40..ae680111d01470881e5dc4914cc7e8e82a463aaa 100644
+--- a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java
++++ b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java
+@@ -3,7 +3,6 @@ package net.minecraft.world.level.block.entity;
+ import java.util.Iterator;
+ import java.util.List;
+ import java.util.function.BooleanSupplier;
+-import java.util.stream.Collectors;
+ import java.util.stream.IntStream;
+ import javax.annotation.Nullable;
+ import net.minecraft.core.BlockPos;
+@@ -33,7 +32,6 @@ import net.minecraft.world.level.block.state.BlockState;
+ import net.minecraft.world.phys.AABB;
+ import net.minecraft.world.phys.shapes.BooleanOp;
+ import net.minecraft.world.phys.shapes.Shapes;
+-import org.bukkit.Bukkit;
+ import org.bukkit.craftbukkit.entity.CraftHumanEntity;
+ import org.bukkit.craftbukkit.inventory.CraftItemStack;
+ import org.bukkit.entity.HumanEntity;
+@@ -191,6 +189,158 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+
+ return false;
+ }
++ // Paper start - Optimize Hoppers
++ private static boolean skipPullModeEventFire = false;
++ private static boolean skipPushModeEventFire = false;
++ public static boolean skipHopperEvents = false;
++
++ private static boolean hopperPush(Level level, BlockPos pos, Container destination, Direction enumdirection, HopperBlockEntity hopper) {
++ skipPushModeEventFire = skipHopperEvents;
++ boolean foundItem = false;
++ for (int i = 0; i < hopper.getContainerSize(); ++i) {
++ ItemStack item = hopper.getItem(i);
++ if (!item.isEmpty()) {
++ foundItem = true;
++ ItemStack origItemStack = item;
++ ItemStack itemstack = origItemStack;
++
++ final int origCount = origItemStack.getCount();
++ final int moved = Math.min(level.spigotConfig.hopperAmount, origCount);
++ origItemStack.setCount(moved);
++
++ // 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) {
++ itemstack = callPushMoveEvent(destination, itemstack, hopper);
++ if (itemstack == null) { // cancelled
++ origItemStack.setCount(origCount);
++ return false;
++ }
++ }
++ final ItemStack itemstack2 = addItem(hopper, destination, itemstack, enumdirection);
++ final int remaining = itemstack2.getCount();
++ if (remaining != moved) {
++ origItemStack = origItemStack.cloneItemStack(true);
++ origItemStack.setCount(origCount);
++ if (!origItemStack.isEmpty()) {
++ origItemStack.setCount(origCount - moved + remaining);
++ }
++ hopper.setItem(i, origItemStack);
++ destination.setChanged();
++ return true;
++ }
++ origItemStack.setCount(origCount);
++ }
++ }
++ if (foundItem && level.paperConfig.cooldownHopperWhenFull) { // Inventory was full - cooldown
++ hopper.setCooldown(level.spigotConfig.hopperTransfer);
++ }
++ return false;
++ }
++
++ private static boolean hopperPull(Level level, Hopper ihopper, Container iinventory, ItemStack origItemStack, int i) {
++ ItemStack itemstack = origItemStack;
++ final int origCount = origItemStack.getCount();
++ final int moved = Math.min(level.spigotConfig.hopperAmount, origCount);
++ itemstack.setCount(moved);
++
++ if (!skipPullModeEventFire) {
++ itemstack = callPullMoveEvent(ihopper, iinventory, itemstack);
++ if (itemstack == null) { // cancelled
++ origItemStack.setCount(origCount);
++ // 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 itemstack2 = addItem(iinventory, ihopper, itemstack, null);
++ final int remaining = itemstack2.getCount();
++ if (remaining != moved) {
++ origItemStack = origItemStack.cloneItemStack(true);
++ origItemStack.setCount(origCount);
++ if (!origItemStack.isEmpty()) {
++ origItemStack.setCount(origCount - moved + remaining);
++ }
++ IGNORE_TILE_UPDATES = true;
++ iinventory.setItem(i, origItemStack);
++ IGNORE_TILE_UPDATES = false;
++ iinventory.setChanged();
++ return true;
++ }
++ origItemStack.setCount(origCount);
++
++ if (level.paperConfig.cooldownHopperWhenFull) {
++ cooldownHopper(ihopper);
++ }
++
++ return false;
++ }
++
++ private static ItemStack callPushMoveEvent(Container iinventory, ItemStack itemstack, HopperBlockEntity hopper) {
++ Inventory destinationInventory = getInventory(iinventory);
++ InventoryMoveItemEvent event = new InventoryMoveItemEvent(hopper.getOwner(false).getInventory(),
++ CraftItemStack.asCraftMirror(itemstack), destinationInventory, true);
++ 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;
++ }
++ }
++
++ private static ItemStack callPullMoveEvent(Hopper hopper, Container iinventory, ItemStack itemstack) {
++ Inventory sourceInventory = getInventory(iinventory);
++ Inventory destination = getInventory(hopper);
++
++ InventoryMoveItemEvent event = new InventoryMoveItemEvent(sourceInventory,
++ // Mirror is safe as we no plugins ever use this item
++ CraftItemStack.asCraftMirror(itemstack), destination, false);
++ 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(Container iinventory) {
++ 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 instanceof BlockEntity) {
++ sourceInventory = ((BlockEntity) iinventory).getOwner(false).getInventory();
++ } else {
++ sourceInventory = iinventory.getOwner().getInventory();
++ }
++ return sourceInventory;
++ }
++
++ private static void cooldownHopper(Hopper hopper) {
++ if (hopper instanceof HopperBlockEntity blockEntity) {
++ blockEntity.setCooldown(blockEntity.getLevel().spigotConfig.hopperTransfer);
++ } else if (hopper instanceof MinecartHopper blockEntity) {
++ blockEntity.setCooldown(blockEntity.getLevel().spigotConfig.hopperTransfer / 2);
++ }
++ }
++ // Paper end
+
+ private static boolean ejectItems(Level world, BlockPos blockposition, BlockState iblockdata, Container iinventory, HopperBlockEntity hopper) { // CraftBukkit
+ Container iinventory1 = HopperBlockEntity.getAttachedContainer(world, blockposition, iblockdata);
+@@ -203,6 +353,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+ if (HopperBlockEntity.isFullContainer(iinventory1, enumdirection)) {
+ return false;
+ } else {
++ return hopperPush(world, blockposition, iinventory1, enumdirection, hopper); /* // Paper - disable rest
+ for (int i = 0; i < iinventory.getContainerSize(); ++i) {
+ if (!iinventory.getItem(i).isEmpty()) {
+ ItemStack itemstack = iinventory.getItem(i).copy();
+@@ -240,7 +391,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+ }
+ }
+
+- return false;
++ return false;*/ // Paper - end commenting out replaced block for Hopper Optimizations
+ }
+ }
+ }
+@@ -250,27 +401,68 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+ }
+
+ private static boolean isFullContainer(Container inventory, Direction direction) {
+- return HopperBlockEntity.getSlots(inventory, direction).allMatch((i) -> {
+- ItemStack itemstack = inventory.getItem(i);
+-
+- return itemstack.getCount() >= itemstack.getMaxStackSize();
+- });
++ return allMatch(inventory, direction, STACK_SIZE_TEST); // Paper - no streams
+ }
+
+ private static boolean isEmptyContainer(Container inv, Direction facing) {
+- return HopperBlockEntity.getSlots(inv, facing).allMatch((i) -> {
+- return inv.getItem(i).isEmpty();
+- });
++ // Paper start
++ return allMatch(inv, facing, IS_EMPTY_TEST);
++ }
++ 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
++
+ public static boolean suckInItems(Level world, Hopper hopper) {
+ Container iinventory = HopperBlockEntity.getSourceContainer(world, hopper);
+
+ if (iinventory != null) {
+ Direction enumdirection = Direction.DOWN;
+
+- return HopperBlockEntity.isEmptyContainer(iinventory, enumdirection) ? false : HopperBlockEntity.getSlots(iinventory, enumdirection).anyMatch((i) -> {
+- return HopperBlockEntity.a(hopper, iinventory, i, enumdirection, world); // Spigot
++ // Paper start - optimize hoppers and remove streams
++ skipPullModeEventFire = skipHopperEvents;
++ return !HopperBlockEntity.isEmptyContainer(iinventory, enumdirection) && anyMatch(iinventory, enumdirection, (item, i) -> {
++ // Logic copied from below to avoid extra getItem calls
++ if (!item.isEmpty() && canTakeItemFromContainer(iinventory, item, i, enumdirection)) {
++ return hopperPull(world, hopper, iinventory, item, i);
++ } else {
++ return false;
++ }
++ // Paper end
+ });
+ } else {
+ Iterator iterator = HopperBlockEntity.getItemsAtAndAbove(world, hopper).iterator();
+@@ -289,10 +481,12 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+ }
+ }
+
++ // Paper - method unused as logic is inlined above
+ private static boolean a(Hopper ihopper, Container iinventory, int i, Direction enumdirection, Level world) { // Spigot
+ ItemStack itemstack = iinventory.getItem(i);
+
+- if (!itemstack.isEmpty() && HopperBlockEntity.canTakeItemFromContainer(iinventory, itemstack, i, enumdirection)) {
++ if (!itemstack.isEmpty() && HopperBlockEntity.canTakeItemFromContainer(iinventory, itemstack, i, enumdirection)) { // If this logic changes, update above. this is left inused incase reflective plugins
++ return hopperPull(world, ihopper, iinventory, itemstack, i); /* // Paper - disable rest
+ ItemStack itemstack1 = itemstack.copy();
+ // ItemStack itemstack2 = addItem(iinventory, ihopper, iinventory.removeItem(i, 1), (EnumDirection) null);
+ // CraftBukkit start - Call event on collection of items from inventories into the hopper
+@@ -329,7 +523,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+ }
+
+ itemstack1.shrink(origCount - itemstack2.getCount()); // Spigot
+- iinventory.setItem(i, itemstack1);
++ iinventory.setItem(i, itemstack1);*/ // Paper - end commenting out replaced block for Hopper Optimizations
+ }
+
+ return false;
+@@ -338,7 +532,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+ public static boolean addItem(Container inventory, ItemEntity itemEntity) {
+ boolean flag = false;
+ // CraftBukkit start
+- InventoryPickupItemEvent event = new InventoryPickupItemEvent(inventory.getOwner().getInventory(), (org.bukkit.entity.Item) itemEntity.getBukkitEntity());
++ InventoryPickupItemEvent event = new InventoryPickupItemEvent(getInventory(inventory), (org.bukkit.entity.Item) itemEntity.getBukkitEntity()); // Paper - use getInventory() to avoid snapshot creation
+ itemEntity.level.getCraftServer().getPluginManager().callEvent(event);
+ if (event.isCancelled()) {
+ return false;
+@@ -397,7 +591,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+ stack = stack.split(to.getMaxStackSize());
+ }
+ // Spigot end
++ IGNORE_TILE_UPDATES = true; // Paper
+ to.setItem(slot, stack);
++ IGNORE_TILE_UPDATES = false; // Paper
+ stack = ItemStack.EMPTY;
+ flag = true;
+ } else if (HopperBlockEntity.canMergeItems(itemstack1, stack)) {
+@@ -448,18 +644,23 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+ }
+
+ public static List<ItemEntity> getItemsAtAndAbove(Level world, Hopper hopper) {
+- return (List) hopper.getSuckShape().toAabbs().stream().flatMap((axisalignedbb) -> {
+- return world.getEntitiesOfClass(ItemEntity.class, axisalignedbb.move(hopper.getLevelX() - 0.5D, hopper.getLevelY() - 0.5D, hopper.getLevelZ() - 0.5D), EntitySelector.ENTITY_STILL_ALIVE).stream();
+- }).collect(Collectors.toList());
++ // Paper start - Optimize item suck in. remove streams, restore 1.12 checks. Seriously checking the bowl?!
++ double d0 = hopper.getLevelX();
++ double d1 = hopper.getLevelY();
++ double d2 = hopper.getLevelZ();
++ AABB bb = new AABB(d0 - 0.5D, d1, d2 - 0.5D, d0 + 0.5D, d1 + 1.5D, d2 + 0.5D);
++ return world.getEntitiesOfClass(ItemEntity.class, bb, Entity::isAlive);
++ // Paper end
+ }
+
+ @Nullable
+ public static Container getContainerAt(Level world, BlockPos pos) {
+- return HopperBlockEntity.getContainerAt(world, (double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D);
++ return HopperBlockEntity.getContainerAt(world, (double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, true); // Paper
+ }
+
++ public static Container getContainerAt(Level world, double x, double y, double z) { return getContainerAt(world, x, y, z, false); } // Paper - overload to default false
+ @Nullable
+- private static Container getContainerAt(Level world, double x, double y, double z) {
++ private static Container getContainerAt(Level world, double x, double y, double z, boolean optimizeEntities) {
+ Object object = null;
+ BlockPos blockposition = new BlockPos(x, y, z);
+ if ( !world.hasChunkAt( blockposition ) ) return null; // Spigot
+@@ -479,7 +680,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
+ }
+ }
+
+- if (object == null) {
++ if (object == null && (!optimizeEntities || !world.paperConfig.hoppersIgnoreOccludingBlocks || !org.bukkit.craftbukkit.util.CraftMagicNumbers.getMaterial(block).isOccluding())) { // Paper
+ List<Entity> list = world.getEntities((Entity) null, new AABB(x - 0.5D, y - 0.5D, z - 0.5D, x + 0.5D, y + 0.5D, z + 0.5D), EntitySelector.CONTAINER_ENTITY_SELECTOR);
+
+ if (!list.isEmpty()) {
+diff --git a/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java
+index f23fff80d07ac7d06715efe67cb49ebbe704967b..ed3518fe7c841d9e1a9c97626acaa3d765a6d76f 100644
+--- a/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java
++++ b/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java
+@@ -95,12 +95,19 @@ public abstract class RandomizableContainerBlockEntity extends BaseContainerBloc
+ @Override
+ public boolean isEmpty() {
+ this.unpackLootTable((Player)null);
+- return this.getItems().stream().allMatch(ItemStack::isEmpty);
++ // Paper start
++ for (ItemStack itemStack : this.getItems()) {
++ if (!itemStack.isEmpty()) {
++ return false;
++ }
++ }
++ // Paper end
++ return true;
+ }
+
+ @Override
+ public ItemStack getItem(int slot) {
+- this.unpackLootTable((Player)null);
++ if (slot == 0) this.unpackLootTable((Player) null); // Paper
+ return this.getItems().get(slot);
+ }
+