aboutsummaryrefslogtreecommitdiffhomepage
path: root/Spigot-Server-Patches/0399-Optimize-Hoppers.patch
blob: b7851eecda405778db918f72d4ddc82b88c7ee30 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Aikar <aikar@aikar.co>
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 f0a28d787e06ceae71034f757637b4c90b6f3c04..9bd64034587217743157a81c04fc20751474e21d 100644
--- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
+++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
@@ -582,4 +582,13 @@ public class PaperWorldConfig {
     private void entitiesTargetWithFollowRange() {
         entitiesTargetWithFollowRange = getBoolean("entities-target-with-follow-range", entitiesTargetWithFollowRange);
     }
+
+    public boolean cooldownHopperWhenFull = true;
+    public boolean disableHopperMoveEvents = 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"));
+    }
 }
diff --git a/src/main/java/net/minecraft/server/IHopper.java b/src/main/java/net/minecraft/server/IHopper.java
index d891be4115f35c7b9685faa34ce90380e989fb18..54552d181912133577cd2c16c0d54e47d90f59d3 100644
--- a/src/main/java/net/minecraft/server/IHopper.java
+++ b/src/main/java/net/minecraft/server/IHopper.java
@@ -12,12 +12,13 @@ public interface IHopper extends IInventory {
         return IHopper.c;
     }
 
-    @Nullable
+    //@Nullable // Paper - it's annoying
     World getWorld();
+    default BlockPosition getBlockPosition() { return new BlockPosition(getX(), getY(), getZ()); } // Paper
 
-    double x();
+    double x(); default double getX() { return this.x(); } // Paper - OBFHELPER
 
-    double z();
+    double z(); default double getY() { return this.z(); } // Paper - OBFHELPER
 
-    double A();
+    double A(); default double getZ() { return this.A(); } // Paper - OBFHELPER
 }
diff --git a/src/main/java/net/minecraft/server/ItemStack.java b/src/main/java/net/minecraft/server/ItemStack.java
index dea478d4790be0c0d6e14cd7673d161abfc46e08..d1e4ee64aa395b38cad22f316308c4687ea56b9f 100644
--- a/src/main/java/net/minecraft/server/ItemStack.java
+++ b/src/main/java/net/minecraft/server/ItemStack.java
@@ -486,11 +486,12 @@ public final class ItemStack {
         return this.getItem().a(this, entityhuman, entityliving, enumhand);
     }
 
-    public ItemStack cloneItemStack() {
-        if (this.isEmpty()) {
+    public ItemStack cloneItemStack() { return cloneItemStack(false); } // Paper
+    public ItemStack cloneItemStack(boolean origItem) { // Paper
+        if (!origItem && this.isEmpty()) { // Paper
             return ItemStack.b;
         } else {
-            ItemStack itemstack = new ItemStack(this.getItem(), this.count);
+            ItemStack itemstack = new ItemStack(origItem ? this.item : this.getItem(), this.count); // Paper
 
             itemstack.d(this.D());
             if (this.tag != null) {
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index 6e67b4e1afd1425a99de8b27a9072cde0bbedf69..529721fb2f7da134957e2fee437dc159d40d1747 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -1247,6 +1247,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
         while (iterator.hasNext()) {
             WorldServer worldserver = (WorldServer) iterator.next();
             worldserver.hasPhysicsEvent =  org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper
+            TileEntityHopper.skipHopperEvents = worldserver.paperConfig.disableHopperMoveEvents || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper
 
             this.methodProfiler.a(() -> {
                 return worldserver + " " + worldserver.getDimensionKey().a();
diff --git a/src/main/java/net/minecraft/server/TileEntity.java b/src/main/java/net/minecraft/server/TileEntity.java
index 2b06c95b4fac97513e706ef073fdd7418e1f092c..67fda8bd5a0ad6fea2df0066c61e006c8a49980c 100644
--- a/src/main/java/net/minecraft/server/TileEntity.java
+++ b/src/main/java/net/minecraft/server/TileEntity.java
@@ -62,6 +62,7 @@ public abstract class TileEntity implements KeyedObject { // Paper
     public void setCurrentChunk(Chunk chunk) {
         this.currentChunk = chunk != null ? new java.lang.ref.WeakReference<>(chunk) : null;
     }
+    static boolean IGNORE_TILE_UPDATES = false;
     // Paper end
 
     @Nullable
@@ -140,6 +141,7 @@ public abstract class TileEntity implements KeyedObject { // Paper
 
     public void update() {
         if (this.world != null) {
+            if (IGNORE_TILE_UPDATES) return; // Paper
             this.c = this.world.getType(this.position);
             this.world.b(this.position, this);
             if (!this.c.isAir()) {
diff --git a/src/main/java/net/minecraft/server/TileEntityHopper.java b/src/main/java/net/minecraft/server/TileEntityHopper.java
index a0a3adac3e2bc939b1809c5587929f674f4318a5..20df9bd21d0e4d2579d05d79672da2eb26478044 100644
--- a/src/main/java/net/minecraft/server/TileEntityHopper.java
+++ b/src/main/java/net/minecraft/server/TileEntityHopper.java
@@ -168,6 +168,160 @@ public class TileEntityHopper extends TileEntityLootable implements IHopper, ITi
         return false;
     }
 
+    // Paper start - Optimize Hoppers
+    private static boolean skipPullModeEventFire = false;
+    private static boolean skipPushModeEventFire = false;
+    static boolean skipHopperEvents = false;
+
+    private boolean hopperPush(IInventory iinventory, EnumDirection enumdirection) {
+        skipPushModeEventFire = skipHopperEvents;
+        boolean foundItem = false;
+        for (int i = 0; i < this.getSize(); ++i) {
+            ItemStack item = this.getItem(i);
+            if (!item.isEmpty()) {
+                foundItem = true;
+                ItemStack origItemStack = item;
+                ItemStack itemstack = origItemStack;
+
+                final int origCount = origItemStack.getCount();
+                final int moved = Math.min(world.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(iinventory, itemstack);
+                    if (itemstack == null) { // cancelled
+                        origItemStack.setCount(origCount);
+                        return false;
+                    }
+                }
+                final ItemStack itemstack2 = addItem(this, iinventory, 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);
+                    }
+                    this.setItem(i, origItemStack);
+                    iinventory.update();
+                    return true;
+                }
+                origItemStack.setCount(origCount);
+            }
+        }
+        if (foundItem && world.paperConfig.cooldownHopperWhenFull) { // Inventory was full - cooldown
+            this.setCooldown(world.spigotConfig.hopperTransfer);
+        }
+        return false;
+    }
+
+    private static boolean hopperPull(IHopper ihopper, IInventory iinventory, ItemStack origItemStack, int i) {
+        ItemStack itemstack = origItemStack;
+        final int origCount = origItemStack.getCount();
+        final World world = ihopper.getWorld();
+        final int moved = Math.min(world.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.update();
+            return true;
+        }
+        origItemStack.setCount(origCount);
+
+        if (world.paperConfig.cooldownHopperWhenFull) {
+            cooldownHopper(ihopper);
+        }
+
+        return false;
+    }
+
+    private ItemStack callPushMoveEvent(IInventory iinventory, ItemStack itemstack) {
+        Inventory destinationInventory = getInventory(iinventory);
+        InventoryMoveItemEvent event = new InventoryMoveItemEvent(this.getOwner(false).getInventory(),
+            CraftItemStack.asCraftMirror(itemstack), destinationInventory, true);
+        boolean result = event.callEvent();
+        if (!event.calledGetItem && !event.calledSetItem) {
+            skipPushModeEventFire = true;
+        }
+        if (!result) {
+            cooldownHopper(this);
+            return null;
+        }
+
+        if (event.calledSetItem) {
+            return CraftItemStack.asNMSCopy(event.getItem());
+        } else {
+            return itemstack;
+        }
+    }
+
+    private static ItemStack callPullMoveEvent(IHopper hopper, IInventory 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(IInventory iinventory) {
+        Inventory sourceInventory;// Have to special case large chests as they work oddly
+        if (iinventory instanceof InventoryLargeChest) {
+            sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((InventoryLargeChest) iinventory);
+        } else if (iinventory instanceof TileEntity) {
+            sourceInventory = ((TileEntity) iinventory).getOwner(false).getInventory();
+        } else {
+            sourceInventory = iinventory.getOwner().getInventory();
+        }
+        return sourceInventory;
+    }
+
+    private static void cooldownHopper(IHopper hopper) {
+        if (hopper instanceof TileEntityHopper) {
+            ((TileEntityHopper) hopper).setCooldown(hopper.getWorld().spigotConfig.hopperTransfer);
+        } else if (hopper instanceof EntityMinecartHopper) {
+            ((EntityMinecartHopper) hopper).setCooldown(hopper.getWorld().spigotConfig.hopperTransfer / 2);
+        }
+    }
+    // Paper end
+
     private boolean k() {
         IInventory iinventory = this.l();
 
@@ -179,6 +333,7 @@ public class TileEntityHopper extends TileEntityLootable implements IHopper, ITi
             if (this.b(iinventory, enumdirection)) {
                 return false;
             } else {
+                return hopperPush(iinventory, enumdirection); /* // Paper - disable rest
                 for (int i = 0; i < this.getSize(); ++i) {
                     if (!this.getItem(i).isEmpty()) {
                         ItemStack itemstack = this.getItem(i).cloneItemStack();
@@ -216,7 +371,7 @@ public class TileEntityHopper extends TileEntityLootable implements IHopper, ITi
                     }
                 }
 
-                return false;
+                return false;*/ // Paper - end commenting out replaced block for Hopper Optimizations
             }
         }
     }
@@ -225,18 +380,54 @@ public class TileEntityHopper extends TileEntityLootable implements IHopper, ITi
         return iinventory instanceof IWorldInventory ? IntStream.of(((IWorldInventory) iinventory).getSlotsForFace(enumdirection)) : IntStream.range(0, iinventory.getSize());
     }
 
-    private boolean b(IInventory iinventory, EnumDirection enumdirection) {
-        return a(iinventory, enumdirection).allMatch((i) -> {
-            ItemStack itemstack = iinventory.getItem(i);
+    private static boolean allMatch(IInventory iinventory, EnumDirection enumdirection, java.util.function.BiPredicate<ItemStack, Integer> test) {
+        if (iinventory instanceof IWorldInventory) {
+            for (int i : ((IWorldInventory) iinventory).getSlotsForFace(enumdirection)) {
+                if (!test.test(iinventory.getItem(i), i)) {
+                    return false;
+                }
+            }
+        } else {
+            int size = iinventory.getSize();
+            for (int i = 0; i < size; i++) {
+                if (!test.test(iinventory.getItem(i), i)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
 
-            return itemstack.getCount() >= itemstack.getMaxStackSize();
-        });
+    private static boolean anyMatch(IInventory iinventory, EnumDirection enumdirection, java.util.function.BiPredicate<ItemStack, Integer> test) {
+        if (iinventory instanceof IWorldInventory) {
+            for (int i : ((IWorldInventory) iinventory).getSlotsForFace(enumdirection)) {
+                if (test.test(iinventory.getItem(i), i)) {
+                    return true;
+                }
+            }
+        } else {
+            int size = iinventory.getSize();
+            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
+
+    private boolean b(IInventory iinventory, EnumDirection enumdirection) {
+        // Paper start - no streams
+        return allMatch(iinventory, enumdirection, STACK_SIZE_TEST);
+        // Paper end
     }
 
     private static boolean c(IInventory iinventory, EnumDirection enumdirection) {
-        return a(iinventory, enumdirection).allMatch((i) -> {
-            return iinventory.getItem(i).isEmpty();
-        });
+        return allMatch(iinventory, enumdirection, IS_EMPTY_TEST);
     }
 
     public static boolean a(IHopper ihopper) {
@@ -245,9 +436,17 @@ public class TileEntityHopper extends TileEntityLootable implements IHopper, ITi
         if (iinventory != null) {
             EnumDirection enumdirection = EnumDirection.DOWN;
 
-            return c(iinventory, enumdirection) ? false : a(iinventory, enumdirection).anyMatch((i) -> {
-                return a(ihopper, iinventory, i, enumdirection);
+            // Paper start - optimize hoppers and remove streams
+            skipPullModeEventFire = skipHopperEvents;
+            return !c(iinventory, enumdirection) && anyMatch(iinventory, enumdirection, (item, i) -> {
+                // Logic copied from below to avoid extra getItem calls
+                if (!item.isEmpty() && canTakeItem(iinventory, item, i, enumdirection)) {
+                    return hopperPull(ihopper, iinventory, item, i);
+                } else {
+                    return false;
+                }
             });
+            // Paper end
         } else {
             Iterator iterator = c(ihopper).iterator();
 
@@ -265,10 +464,11 @@ public class TileEntityHopper extends TileEntityLootable implements IHopper, ITi
         }
     }
 
-    private static boolean a(IHopper ihopper, IInventory iinventory, int i, EnumDirection enumdirection) {
+    private static boolean a(IHopper ihopper, IInventory iinventory, int i, EnumDirection enumdirection) {// Paper - method unused as logic is inlined above
         ItemStack itemstack = iinventory.getItem(i);
 
-        if (!itemstack.isEmpty() && b(iinventory, itemstack, i, enumdirection)) {
+        if (!itemstack.isEmpty() && b(iinventory, itemstack, i, enumdirection)) { // If this logic changes, update above. this is left inused incase reflective plugins
+            return hopperPull(ihopper, iinventory, itemstack, i); /* // Paper - disable rest
             ItemStack itemstack1 = itemstack.cloneItemStack();
             // ItemStack itemstack2 = addItem(iinventory, ihopper, iinventory.splitStack(i, 1), (EnumDirection) null);
             // CraftBukkit start - Call event on collection of items from inventories into the hopper
@@ -305,7 +505,7 @@ public class TileEntityHopper extends TileEntityLootable implements IHopper, ITi
             }
 
             itemstack1.subtract(origCount - itemstack2.getCount()); // Spigot
-            iinventory.setItem(i, itemstack1);
+            iinventory.setItem(i, itemstack1);*/ // Paper - end commenting out replaced block for Hopper Optimizations
         }
 
         return false;
@@ -314,7 +514,7 @@ public class TileEntityHopper extends TileEntityLootable implements IHopper, ITi
     public static boolean a(IInventory iinventory, EntityItem entityitem) {
         boolean flag = false;
         // CraftBukkit start
-        InventoryPickupItemEvent event = new InventoryPickupItemEvent(iinventory.getOwner().getInventory(), (org.bukkit.entity.Item) entityitem.getBukkitEntity());
+        InventoryPickupItemEvent event = new InventoryPickupItemEvent(getInventory(iinventory), (org.bukkit.entity.Item) entityitem.getBukkitEntity()); // Paper - use getInventory() to avoid snapshot creation
         entityitem.world.getServer().getPluginManager().callEvent(event);
         if (event.isCancelled()) {
             return false;
@@ -356,6 +556,7 @@ public class TileEntityHopper extends TileEntityLootable implements IHopper, ITi
         return !iinventory.b(i, itemstack) ? false : !(iinventory instanceof IWorldInventory) || ((IWorldInventory) iinventory).canPlaceItemThroughFace(i, itemstack, enumdirection);
     }
 
+    private static boolean canTakeItem(IInventory iinventory, ItemStack itemstack, int i, EnumDirection enumdirection) { return b(iinventory, itemstack, i, enumdirection); } // Paper - OBFHELPER
     private static boolean b(IInventory iinventory, ItemStack itemstack, int i, EnumDirection enumdirection) {
         return !(iinventory instanceof IWorldInventory) || ((IWorldInventory) iinventory).canTakeItemThroughFace(i, itemstack, enumdirection);
     }
@@ -368,7 +569,9 @@ public class TileEntityHopper extends TileEntityLootable implements IHopper, ITi
             boolean flag1 = iinventory1.isEmpty();
 
             if (itemstack1.isEmpty()) {
+                IGNORE_TILE_UPDATES = true; // Paper
                 iinventory1.setItem(i, itemstack);
+                IGNORE_TILE_UPDATES = false; // Paper
                 itemstack = ItemStack.b;
                 flag = true;
             } else if (a(itemstack1, itemstack)) {
@@ -419,18 +622,24 @@ public class TileEntityHopper extends TileEntityLootable implements IHopper, ITi
     }
 
     public static List<EntityItem> c(IHopper ihopper) {
-        return (List) ihopper.ac_().d().stream().flatMap((axisalignedbb) -> {
-            return ihopper.getWorld().a(EntityItem.class, axisalignedbb.d(ihopper.x() - 0.5D, ihopper.z() - 0.5D, ihopper.A() - 0.5D), IEntitySelector.a).stream();
-        }).collect(Collectors.toList());
+        // Paper start - Optimize item suck in. remove streams, restore 1.12 checks. Seriously checking the bowl?!
+        World world = ihopper.getWorld();
+        double d0 = ihopper.getX();
+        double d1 = ihopper.getY();
+        double d2 = ihopper.getZ();
+        AxisAlignedBB bb = new AxisAlignedBB(d0 - 0.5D, d1, d2 - 0.5D, d0 + 0.5D, d1 + 1.5D, d2 + 0.5D);
+        return world.getEntities(EntityItem.class, bb, Entity::isAlive);
+        // Paper end
     }
 
     @Nullable
     public static IInventory b(World world, BlockPosition blockposition) {
-        return a(world, (double) blockposition.getX() + 0.5D, (double) blockposition.getY() + 0.5D, (double) blockposition.getZ() + 0.5D);
+        return a(world, (double) blockposition.getX() + 0.5D, (double) blockposition.getY() + 0.5D, (double) blockposition.getZ() + 0.5D, true); // Paper
     }
 
     @Nullable
-    public static IInventory a(World world, double d0, double d1, double d2) {
+    public static IInventory a(World world, double d0, double d1, double d2) { return a(world, d0, d1, d2, false); } // Paper - overload to default false
+    public static IInventory a(World world, double d0, double d1, double d2, boolean optimizeEntities) { // Paper
         Object object = null;
         BlockPosition blockposition = new BlockPosition(d0, d1, d2);
         if ( !world.isLoaded( blockposition ) ) return null; // Spigot
@@ -450,7 +659,7 @@ public class TileEntityHopper extends TileEntityLootable implements IHopper, ITi
             }
         }
 
-        if (object == null) {
+        if (object == null && (!optimizeEntities || !org.bukkit.craftbukkit.util.CraftMagicNumbers.getMaterial(block).isOccluding())) { // Paper
             List<Entity> list = world.getEntities((Entity) null, new AxisAlignedBB(d0 - 0.5D, d1 - 0.5D, d2 - 0.5D, d0 + 0.5D, d1 + 0.5D, d2 + 0.5D), IEntitySelector.d);
 
             if (!list.isEmpty()) {
diff --git a/src/main/java/net/minecraft/server/TileEntityLootable.java b/src/main/java/net/minecraft/server/TileEntityLootable.java
index 006f8bace83ba1e540399b3f05952fa5edb0681f..badf8e136ec3e45870c29e154cd4a855546c7530 100644
--- a/src/main/java/net/minecraft/server/TileEntityLootable.java
+++ b/src/main/java/net/minecraft/server/TileEntityLootable.java
@@ -77,12 +77,19 @@ public abstract class TileEntityLootable extends TileEntityContainer {
     @Override
     public boolean isEmpty() {
         this.d((EntityHuman) null);
-        return this.f().stream().allMatch(ItemStack::isEmpty);
+        // Paper start
+        for (ItemStack itemStack : this.f()) {
+            if (!itemStack.isEmpty()) {
+                return false;
+            }
+        }
+        // Paper end
+        return true;
     }
 
     @Override
     public ItemStack getItem(int i) {
-        this.d((EntityHuman) null);
+        if (i == 0) this.d((EntityHuman) null); // Paper
         return (ItemStack) this.f().get(i);
     }
 
diff --git a/src/main/java/net/minecraft/server/World.java b/src/main/java/net/minecraft/server/World.java
index 57f544eb6b2e3362b62e8541642bfc88419ab596..325d7a313cfab72978f5c8fada1af6f19cdab239 100644
--- a/src/main/java/net/minecraft/server/World.java
+++ b/src/main/java/net/minecraft/server/World.java
@@ -1134,8 +1134,8 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
         return list;
     }
 
-    @Override
-    public <T extends Entity> List<T> a(Class<? extends T> oclass, AxisAlignedBB axisalignedbb, @Nullable Predicate<? super T> predicate) {
+    public <T extends Entity> List<T> getEntities(Class<? extends T> oclass, AxisAlignedBB axisalignedbb, @Nullable Predicate<? super T> predicate) { return a(oclass, axisalignedbb, predicate); } // Paper - OBFHELPER
+    @Override public <T extends Entity> List<T> a(Class<? extends T> oclass, AxisAlignedBB axisalignedbb, @Nullable Predicate<? super T> predicate) {
         this.getMethodProfiler().c("getEntities");
         int i = MathHelper.floor((axisalignedbb.minX - 2.0D) / 16.0D);
         int j = MathHelper.f((axisalignedbb.maxX + 2.0D) / 16.0D);