Paper/Spigot-Server-Patches/0143-LootTable-API-Replenishable-Lootables-Feature.patch
Aikar 3faaaab75d Optimize isInvalidYLocation, getType and getBlockData
Some pretty micro optimizations, but this is the hottest method in the server....

This will drastically reduce number of operations to perform getType

the 2 previous patches was squashed into 1
2016-06-22 22:43:02 -04:00

757 lines
28 KiB
Diff

From 0e07bd7fe9f2f38d0339008bd2793aa6a24a73cd Mon Sep 17 00:00:00 2001
From: Aikar <aikar@aikar.co>
Date: Sun, 1 May 2016 21:19:14 -0400
Subject: [PATCH] LootTable API & Replenishable Lootables Feature
Provides an API to control the loot table for an object.
Also provides a feature that any Lootable Inventory (Chests in Structures)
can automatically replenish after a given time.
This feature is good for long term worlds so that newer players
do not suffer with "Every chest has been looted"
diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
index 31b6dce..a1ccdf1 100644
--- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
+++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
@@ -333,4 +333,26 @@ public class PaperWorldConfig {
this.frostedIceDelayMax = this.getInt("frosted-ice.delay.max", this.frostedIceDelayMax);
this.log("Frosted Ice: " + (this.frostedIceEnabled ? "enabled" : "disabled") + " / delay: min=" + this.frostedIceDelayMin + ", max=" + this.frostedIceDelayMax);
}
+
+ public boolean autoReplenishLootables;
+ public boolean restrictPlayerReloot;
+ public boolean changeLootTableSeedOnFill;
+ public int maxLootableRefills;
+ public int lootableRegenMin;
+ public int lootableRegenMax;
+ private void enhancedLootables() {
+ autoReplenishLootables = getBoolean("lootables.auto-replenish", false);
+ restrictPlayerReloot = getBoolean("lootables.restrict-player-reloot", true);
+ changeLootTableSeedOnFill = getBoolean("lootables.reset-seed-on-fill", true);
+ maxLootableRefills = getInt("lootables.max-refills", -1);
+ lootableRegenMin = PaperConfig.getSeconds(getString("lootables.refresh-min", "12h"));
+ lootableRegenMax = PaperConfig.getSeconds(getString("lootables.refresh-max", "2d"));
+ if (autoReplenishLootables) {
+ log("Lootables: Replenishing every " +
+ PaperConfig.timeSummary(lootableRegenMin) + " to " +
+ PaperConfig.timeSummary(lootableRegenMax) +
+ (restrictPlayerReloot ? " (restricting reloot)" : "")
+ );
+ }
+ }
}
diff --git a/src/main/java/com/destroystokyo/paper/loottable/CraftLootable.java b/src/main/java/com/destroystokyo/paper/loottable/CraftLootable.java
new file mode 100644
index 0000000..36c36d1
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/loottable/CraftLootable.java
@@ -0,0 +1,12 @@
+package com.destroystokyo.paper.loottable;
+
+import net.minecraft.server.World;
+
+interface CraftLootable extends Lootable {
+
+ World getNMSWorld();
+
+ default org.bukkit.World getBukkitWorld() {
+ return getNMSWorld().getWorld();
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/loottable/CraftLootableBlockInventory.java b/src/main/java/com/destroystokyo/paper/loottable/CraftLootableBlockInventory.java
new file mode 100644
index 0000000..20d236c
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/loottable/CraftLootableBlockInventory.java
@@ -0,0 +1,33 @@
+package com.destroystokyo.paper.loottable;
+
+import net.minecraft.server.BlockPosition;
+import net.minecraft.server.TileEntityLootable;
+import net.minecraft.server.World;
+import org.bukkit.Chunk;
+import org.bukkit.block.Block;
+
+public interface CraftLootableBlockInventory extends LootableBlockInventory, CraftLootableInventory {
+
+ TileEntityLootable getTileEntity();
+
+ @Override
+ default LootableInventory getAPILootableInventory() {
+ return this;
+ }
+
+ @Override
+ default World getNMSWorld() {
+ return getTileEntity().getWorld();
+ }
+
+ default Block getBlock() {
+ final BlockPosition position = getTileEntity().getPosition();
+ final Chunk bukkitChunk = getTileEntity().getWorld().getChunkAtWorldCoords(position).bukkitChunk;
+ return bukkitChunk.getBlock(position.getX(), position.getY(), position.getZ());
+ }
+
+ @Override
+ default CraftLootableInventoryData getLootableData() {
+ return getTileEntity().getLootableData();
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/loottable/CraftLootableEntityInventory.java b/src/main/java/com/destroystokyo/paper/loottable/CraftLootableEntityInventory.java
new file mode 100644
index 0000000..1150dee
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/loottable/CraftLootableEntityInventory.java
@@ -0,0 +1,31 @@
+package com.destroystokyo.paper.loottable;
+
+import net.minecraft.server.World;
+import org.bukkit.entity.Entity;
+
+public interface CraftLootableEntityInventory extends LootableEntityInventory, CraftLootableInventory {
+
+ net.minecraft.server.Entity getHandle();
+
+ @Override
+ default LootableInventory getAPILootableInventory() {
+ return this;
+ }
+
+ default Entity getEntity() {
+ return getHandle().getBukkitEntity();
+ }
+
+ @Override
+ default World getNMSWorld() {
+ return getHandle().getWorld();
+ }
+
+ @Override
+ default CraftLootableInventoryData getLootableData() {
+ if (getHandle() instanceof CraftLootableInventory) {
+ return ((CraftLootableInventory) getHandle()).getLootableData();
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/loottable/CraftLootableInventory.java b/src/main/java/com/destroystokyo/paper/loottable/CraftLootableInventory.java
new file mode 100644
index 0000000..6680976
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/loottable/CraftLootableInventory.java
@@ -0,0 +1,88 @@
+package com.destroystokyo.paper.loottable;
+
+import org.apache.commons.lang.Validate;
+
+import java.util.UUID;
+
+public interface CraftLootableInventory extends CraftLootable, LootableInventory {
+
+ CraftLootableInventoryData getLootableData();
+ LootableInventory getAPILootableInventory();
+
+ @Override
+ default boolean isRefillEnabled() {
+ return getNMSWorld().paperConfig.autoReplenishLootables;
+ }
+
+ @Override
+ default boolean hasBeenFilled() {
+ return getLastFilled() != -1;
+ }
+
+ @Override
+ default String getLootTableName() {
+ return getLootableData().getLootable().getLootTableName();
+ }
+
+ @Override
+ default String setLootTable(String name, long seed) {
+ Validate.notNull(name);
+
+ String prevLootTable = getLootTableName();
+ getLootableData().getLootable().setLootTable(name, seed);
+ return prevLootTable;
+ }
+
+ @Override
+ default long getLootTableSeed() {
+ return getLootableData().getLootable().getLootTableSeed();
+ }
+
+ @Override
+ default void clearLootTable() {
+ getLootableData().getLootable().clearLootTable();
+ }
+
+ @Override
+ default boolean hasPlayerLooted(UUID player) {
+ return getLootableData().hasPlayerLooted(player);
+ }
+
+ @Override
+ default Long getLastLooted(UUID player) {
+ return getLootableData().getLastLooted(player);
+ }
+
+ @Override
+ default boolean setHasPlayerLooted(UUID player, boolean looted) {
+ final boolean hasLooted = hasPlayerLooted(player);
+ if (hasLooted != looted) {
+ getLootableData().setPlayerLootedState(player, looted);
+ }
+ return hasLooted;
+ }
+
+ @Override
+ default boolean hasPendingRefill() {
+ long nextRefill = getLootableData().getNextRefill();
+ return nextRefill != -1 && nextRefill > getLootableData().getLastFill();
+ }
+
+ @Override
+ default long getLastFilled() {
+ return getLootableData().getLastFill();
+ }
+
+ @Override
+ default long getNextRefill() {
+ return getLootableData().getNextRefill();
+ }
+
+ @Override
+ default long setNextRefill(long refillAt) {
+ if (refillAt < -1) {
+ refillAt = -1;
+ }
+ return getLootableData().setNextRefill(refillAt);
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/loottable/CraftLootableInventoryData.java b/src/main/java/com/destroystokyo/paper/loottable/CraftLootableInventoryData.java
new file mode 100644
index 0000000..01c2713
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/loottable/CraftLootableInventoryData.java
@@ -0,0 +1,182 @@
+package com.destroystokyo.paper.loottable;
+
+import com.destroystokyo.paper.PaperWorldConfig;
+import net.minecraft.server.*;
+import org.bukkit.entity.Player;
+
+import javax.annotation.Nullable;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.UUID;
+
+public class CraftLootableInventoryData {
+
+ private static final Random RANDOM = new Random();
+
+ private long lastFill = -1;
+ private long nextRefill = -1;
+ private int numRefills = 0;
+ private Map<UUID, Long> lootedPlayers;
+ private final CraftLootableInventory lootable;
+
+ public CraftLootableInventoryData(CraftLootableInventory lootable) {
+ this.lootable = lootable;
+ }
+
+ long getLastFill() {
+ return this.lastFill;
+ }
+
+ long getNextRefill() {
+ return this.nextRefill;
+ }
+
+ long setNextRefill(long nextRefill) {
+ long prev = this.nextRefill;
+ this.nextRefill = nextRefill;
+ return prev;
+ }
+
+ CraftLootableInventory getLootable() {
+ return lootable;
+ }
+
+ public boolean shouldReplenish(@Nullable EntityHuman player) {
+ String tableName = this.lootable.getLootTableName();
+
+ // No Loot Table associated
+ if (tableName == null) {
+ return false;
+ }
+
+ // ALWAYS process the first fill
+ if (this.lastFill == -1) {
+ return true;
+ }
+
+ // Only process refills when a player is set
+ if (player == null) {
+ return false;
+ }
+
+ // Chest is not scheduled for refill
+ if (this.nextRefill == -1) {
+ return false;
+ }
+
+ final PaperWorldConfig paperConfig = this.lootable.getNMSWorld().paperConfig;
+
+ // Check if max refills has been hit
+ if (paperConfig.maxLootableRefills != -1 && this.numRefills >= paperConfig.maxLootableRefills) {
+ return false;
+ }
+
+ // Refill has not been reached
+ if (this.nextRefill > System.currentTimeMillis()) {
+ return false;
+ }
+
+
+ final Player bukkitPlayer = (Player) player.getBukkitEntity();
+ LootableInventoryReplenishEvent event = new LootableInventoryReplenishEvent(bukkitPlayer, lootable.getAPILootableInventory());
+ if (paperConfig.restrictPlayerReloot && hasPlayerLooted(player.getUniqueID())) {
+ event.setCancelled(true);
+ }
+ return event.callEvent();
+ }
+ public void processRefill(@Nullable EntityHuman player) {
+ this.lastFill = System.currentTimeMillis();
+ final PaperWorldConfig paperConfig = this.lootable.getNMSWorld().paperConfig;
+ if (paperConfig.autoReplenishLootables) {
+ int min = paperConfig.lootableRegenMin * 1000;
+ int max = paperConfig.lootableRegenMax * 1000;
+ this.nextRefill = this.lastFill + min + RANDOM.nextInt(max - min + 1);
+ this.numRefills++;
+ if (paperConfig.changeLootTableSeedOnFill) {
+ this.lootable.setLootTableSeed(0);
+ }
+ if (player != null) { // This means that numRefills can be incremented without a player being in the lootedPlayers list - Seems to be EntityMinecartChest specific
+ this.setPlayerLootedState(player.getUniqueID(), true);
+ }
+ } else {
+ this.lootable.clearLootTable();
+ }
+ }
+
+
+ public void loadNbt(NBTTagCompound base) {
+ if (!base.hasKeyOfType("Paper.LootableData", 10)) { // 10 = compound
+ return;
+ }
+ NBTTagCompound comp = base.getCompound("Paper.LootableData");
+ if (comp.hasKey("lastFill")) {
+ this.lastFill = comp.getLong("lastFill");
+ }
+ if (comp.hasKey("nextRefill")) {
+ this.nextRefill = comp.getLong("nextRefill");
+ }
+
+ if (comp.hasKey("numRefills")) {
+ this.numRefills = comp.getInt("numRefills");
+ }
+ if (comp.hasKeyOfType("lootedPlayers", 9)) { // 9 = list
+ NBTTagList list = comp.getList("lootedPlayers", 10); // 10 = compound
+ final int size = list.size();
+ if (size > 0) {
+ this.lootedPlayers = new HashMap<>(list.size());
+ }
+ for (int i = 0; i < size; i++) {
+ final NBTTagCompound cmp = list.get(i);
+ lootedPlayers.put(cmp.getUUID("UUID"), cmp.getLong("Time"));
+ }
+ }
+ }
+ public void saveNbt(NBTTagCompound base) {
+ NBTTagCompound comp = new NBTTagCompound();
+ if (this.nextRefill != -1) {
+ comp.setLong("nextRefill", this.nextRefill);
+ }
+ if (this.lastFill != -1) {
+ comp.setLong("lastFill", this.lastFill);
+ }
+ if (this.numRefills != 0) {
+ comp.setInt("numRefills", this.numRefills);
+ }
+ if (this.lootedPlayers != null && !this.lootedPlayers.isEmpty()) {
+ NBTTagList list = new NBTTagList();
+ for (Map.Entry<UUID, Long> entry : this.lootedPlayers.entrySet()) {
+ NBTTagCompound cmp = new NBTTagCompound();
+ cmp.setUUID("UUID", entry.getKey());
+ cmp.setLong("Time", entry.getValue());
+ list.add(cmp);
+ }
+ comp.set("lootedPlayers", list);
+ }
+
+ if (!comp.isEmpty()) {
+ base.set("Paper.LootableData", comp);
+ }
+ }
+
+ void setPlayerLootedState(UUID player, boolean looted) {
+ if (looted && this.lootedPlayers == null) {
+ this.lootedPlayers = new HashMap<>();
+ }
+ if (looted) {
+ if (!this.lootedPlayers.containsKey(player)) {
+ this.lootedPlayers.put(player, System.currentTimeMillis());
+ }
+ } else if (this.lootedPlayers != null) {
+ this.lootedPlayers.remove(player);
+ }
+ }
+
+ boolean hasPlayerLooted(UUID player) {
+ return this.lootedPlayers != null && this.lootedPlayers.containsKey(player);
+ }
+
+ Long getLastLooted(UUID player) {
+ return lootedPlayers != null ? lootedPlayers.get(player) : null;
+ }
+}
diff --git a/src/main/java/net/minecraft/server/EntityMinecartContainer.java b/src/main/java/net/minecraft/server/EntityMinecartContainer.java
index d0c577d..b808716 100644
--- a/src/main/java/net/minecraft/server/EntityMinecartContainer.java
+++ b/src/main/java/net/minecraft/server/EntityMinecartContainer.java
@@ -5,17 +5,21 @@ import javax.annotation.Nullable;
// CraftBukkit start
import java.util.List;
import org.bukkit.Location;
+
+import com.destroystokyo.paper.loottable.CraftLootableInventoryData; // Paper
+import com.destroystokyo.paper.loottable.CraftLootableInventory; // Paper
+import com.destroystokyo.paper.loottable.LootableInventory; // Paper
import org.bukkit.craftbukkit.entity.CraftHumanEntity;
import org.bukkit.entity.HumanEntity;
import org.bukkit.inventory.InventoryHolder;
// CraftBukkit end
-public abstract class EntityMinecartContainer extends EntityMinecartAbstract implements ITileInventory, ILootable {
+public abstract class EntityMinecartContainer extends EntityMinecartAbstract implements ITileInventory, ILootable, CraftLootableInventory { // Paper
private ItemStack[] items = new ItemStack[27]; // CraftBukkit - 36 -> 27
private boolean b = true;
private MinecraftKey c;
- private long d;
+ private long d;public long getLootTableSeed() { return d; } // Paper // OBFHELPER
// CraftBukkit start
public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
@@ -146,12 +150,13 @@ public abstract class EntityMinecartContainer extends EntityMinecartAbstract imp
protected void b(NBTTagCompound nbttagcompound) {
super.b(nbttagcompound);
+ lootableData.saveNbt(nbttagcompound); // Paper
if (this.c != null) {
nbttagcompound.setString("LootTable", this.c.toString());
if (this.d != 0L) {
nbttagcompound.setLong("LootTableSeed", this.d);
}
- } else {
+ } if (true) { // Paper - Always save the items, Table may stick around
NBTTagList nbttaglist = new NBTTagList();
for (int i = 0; i < this.items.length; ++i) {
@@ -172,10 +177,11 @@ public abstract class EntityMinecartContainer extends EntityMinecartAbstract imp
protected void a(NBTTagCompound nbttagcompound) {
super.a(nbttagcompound);
this.items = new ItemStack[this.getSize()];
+ lootableData.loadNbt(nbttagcompound); // Paper
if (nbttagcompound.hasKeyOfType("LootTable", 8)) {
this.c = new MinecraftKey(nbttagcompound.getString("LootTable"));
this.d = nbttagcompound.getLong("LootTableSeed");
- } else {
+ } if (true) { // Paper - always load the items, table may still remain
NBTTagList nbttaglist = nbttagcompound.getList("Items", 10);
for (int i = 0; i < nbttaglist.size(); ++i) {
@@ -233,10 +239,10 @@ public abstract class EntityMinecartContainer extends EntityMinecartAbstract imp
}
public void f(@Nullable EntityHuman entityhuman) {
- if (this.c != null) {
+ if (lootableData.shouldReplenish(entityhuman)) { // Paper
LootTable loottable = this.world.ak().a(this.c);
- this.c = null;
+ lootableData.processRefill(entityhuman); // Paper
Random random;
if (this.d == 0L) {
@@ -265,12 +271,52 @@ public abstract class EntityMinecartContainer extends EntityMinecartAbstract imp
}
+ public void setLootTable(MinecraftKey key, long seed) { a(key, seed);} // Paper // OBFHELPER
public void a(MinecraftKey minecraftkey, long i) {
this.c = minecraftkey;
this.d = i;
}
+
+ public MinecraftKey getLootTableKey() { return b(); } // Paper // OBFHELPER
public MinecraftKey b() {
return this.c;
}
+
+ // Paper start
+ private final CraftLootableInventoryData lootableData = new CraftLootableInventoryData(this);
+
+ @Override
+ public CraftLootableInventoryData getLootableData() {
+ return lootableData;
+ }
+
+ @Override
+ public LootableInventory getAPILootableInventory() {
+ return (LootableInventory) this.getBukkitEntity();
+ }
+
+ @Override
+ public World getNMSWorld() {
+ return this.world;
+ }
+
+ public String getLootTableName() {
+ final MinecraftKey key = getLootTableKey();
+ return key != null ? key.toString() : null;
+ }
+
+ @Override
+ public String setLootTable(String name, long seed) {
+ String prev = getLootTableName();
+ setLootTable(new MinecraftKey(name), seed);
+ return prev;
+ }
+
+ @Override
+ public void clearLootTable() {
+ //noinspection RedundantCast
+ this.c = (MinecraftKey) null;
+ }
+ // Paper end
}
diff --git a/src/main/java/net/minecraft/server/TileEntityLootable.java b/src/main/java/net/minecraft/server/TileEntityLootable.java
index ef81867..3b42152 100644
--- a/src/main/java/net/minecraft/server/TileEntityLootable.java
+++ b/src/main/java/net/minecraft/server/TileEntityLootable.java
@@ -1,43 +1,49 @@
package net.minecraft.server;
+import com.destroystokyo.paper.loottable.CraftLootableInventoryData; // Paper
+import com.destroystokyo.paper.loottable.CraftLootableInventory; // Paper
+import com.destroystokyo.paper.loottable.LootableInventory; // Paper
+
import java.util.Random;
import javax.annotation.Nullable;
-public abstract class TileEntityLootable extends TileEntityContainer implements ILootable {
+public abstract class TileEntityLootable extends TileEntityContainer implements ILootable, CraftLootableInventory { // Paper
protected MinecraftKey m;
- protected long n;
+ protected long n; public long getLootTableSeed() { return n; } // Paper // OBFHELPER
public TileEntityLootable() {}
protected boolean c(NBTTagCompound nbttagcompound) {
+ lootableData.loadNbt(nbttagcompound); // Paper
if (nbttagcompound.hasKeyOfType("LootTable", 8)) {
this.m = new MinecraftKey(nbttagcompound.getString("LootTable"));
this.n = nbttagcompound.getLong("LootTableSeed");
- return true;
+ return false; // Paper - always load the items, table may still remain
} else {
return false;
}
}
protected boolean d(NBTTagCompound nbttagcompound) {
+ lootableData.saveNbt(nbttagcompound); // Paper
if (this.m != null) {
nbttagcompound.setString("LootTable", this.m.toString());
if (this.n != 0L) {
nbttagcompound.setLong("LootTableSeed", this.n);
}
- return true;
+ return false; // Paper - always save the items, table may still remain
} else {
return false;
}
}
protected void d(@Nullable EntityHuman entityhuman) {
- if (this.m != null) {
+ if (lootableData.shouldReplenish(entityhuman)) { // Paper
LootTable loottable = this.world.ak().a(this.m);
- this.m = null;
+ lootableData.processRefill(entityhuman); // Paper
Random random;
if (this.n == 0L) {
@@ -57,12 +63,51 @@ public abstract class TileEntityLootable extends TileEntityContainer implements
}
+ public MinecraftKey getLootTableKey() { return b(); } // Paper // OBFHELPER
public MinecraftKey b() {
return this.m;
}
+ public void setLootTable(MinecraftKey key, long seed) { a(key, seed);} // Paper // OBFHELPER
public void a(MinecraftKey minecraftkey, long i) {
this.m = minecraftkey;
this.n = i;
}
+
+ // Paper start - LootTable API
+ private final CraftLootableInventoryData lootableData = new CraftLootableInventoryData(this);
+
+ @Override
+ public CraftLootableInventoryData getLootableData() {
+ return lootableData;
+ }
+
+ @Override
+ public LootableInventory getAPILootableInventory() {
+ return (LootableInventory) getBukkitWorld().getBlockAt(MCUtil.toLocation(world, getPosition())).getState();
+ }
+
+ @Override
+ public World getNMSWorld() {
+ return world;
+ }
+
+ public String getLootTableName() {
+ final MinecraftKey key = getLootTableKey();
+ return key != null ? key.toString() : null;
+ }
+
+ @Override
+ public String setLootTable(String name, long seed) {
+ String prev = getLootTableName();
+ setLootTable(new MinecraftKey(name), seed);
+ return prev;
+ }
+
+ @Override
+ public void clearLootTable() {
+ //noinspection RedundantCast
+ this.m = (MinecraftKey) null;
+ }
+ // Paper end
}
diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftChest.java b/src/main/java/org/bukkit/craftbukkit/block/CraftChest.java
index ce36ee4..99b039b 100644
--- a/src/main/java/org/bukkit/craftbukkit/block/CraftChest.java
+++ b/src/main/java/org/bukkit/craftbukkit/block/CraftChest.java
@@ -1,5 +1,6 @@
package org.bukkit.craftbukkit.block;
+import com.destroystokyo.paper.loottable.CraftLootableBlockInventory; // Paper
import net.minecraft.server.BlockPosition;
import net.minecraft.server.TileEntityChest;
@@ -11,7 +12,7 @@ import org.bukkit.craftbukkit.inventory.CraftInventory;
import org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest;
import org.bukkit.inventory.Inventory;
-public class CraftChest extends CraftBlockState implements Chest {
+public class CraftChest extends CraftBlockState implements Chest, CraftLootableBlockInventory { // Paper
private final CraftWorld world;
private final TileEntityChest chest;
diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftDispenser.java b/src/main/java/org/bukkit/craftbukkit/block/CraftDispenser.java
index a3ca3a5..cd7e65d 100644
--- a/src/main/java/org/bukkit/craftbukkit/block/CraftDispenser.java
+++ b/src/main/java/org/bukkit/craftbukkit/block/CraftDispenser.java
@@ -1,5 +1,6 @@
package org.bukkit.craftbukkit.block;
+import com.destroystokyo.paper.loottable.CraftLootableBlockInventory; // Paper
import net.minecraft.server.BlockDispenser;
import net.minecraft.server.BlockPosition;
import net.minecraft.server.Blocks;
@@ -14,7 +15,7 @@ import org.bukkit.craftbukkit.projectiles.CraftBlockProjectileSource;
import org.bukkit.inventory.Inventory;
import org.bukkit.projectiles.BlockProjectileSource;
-public class CraftDispenser extends CraftBlockState implements Dispenser {
+public class CraftDispenser extends CraftBlockState implements Dispenser, CraftLootableBlockInventory { // Paper
private final CraftWorld world;
private final TileEntityDispenser dispenser;
diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftHopper.java b/src/main/java/org/bukkit/craftbukkit/block/CraftHopper.java
index db844a3..1e805a7 100644
--- a/src/main/java/org/bukkit/craftbukkit/block/CraftHopper.java
+++ b/src/main/java/org/bukkit/craftbukkit/block/CraftHopper.java
@@ -1,5 +1,6 @@
package org.bukkit.craftbukkit.block;
+import com.destroystokyo.paper.loottable.CraftLootableBlockInventory; // Paper
import net.minecraft.server.TileEntityHopper;
import org.bukkit.Material;
import org.bukkit.block.Block;
@@ -8,7 +9,7 @@ import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.inventory.CraftInventory;
import org.bukkit.inventory.Inventory;
-public class CraftHopper extends CraftBlockState implements Hopper {
+public class CraftHopper extends CraftBlockState implements Hopper, CraftLootableBlockInventory { // Paper
private final TileEntityHopper hopper;
public CraftHopper(final Block block) {
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java
index f5a1875..b2c5679 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java
@@ -1,5 +1,6 @@
package org.bukkit.craftbukkit.entity;
+import com.destroystokyo.paper.loottable.CraftLootableEntityInventory; // Paper
import net.minecraft.server.EntityMinecartChest;
import org.bukkit.craftbukkit.CraftServer;
@@ -9,7 +10,7 @@ import org.bukkit.entity.StorageMinecart;
import org.bukkit.inventory.Inventory;
@SuppressWarnings("deprecation")
-public class CraftMinecartChest extends CraftMinecart implements StorageMinecart {
+public class CraftMinecartChest extends CraftMinecart implements StorageMinecart, CraftLootableEntityInventory { // Paper
private final CraftInventory inventory;
public CraftMinecartChest(CraftServer server, EntityMinecartChest entity) {
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java
index e9963e2..acb4dee 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java
@@ -1,5 +1,6 @@
package org.bukkit.craftbukkit.entity;
+import com.destroystokyo.paper.loottable.CraftLootableEntityInventory; // Paper
import net.minecraft.server.EntityMinecartHopper;
import org.bukkit.craftbukkit.CraftServer;
@@ -8,7 +9,7 @@ import org.bukkit.entity.EntityType;
import org.bukkit.entity.minecart.HopperMinecart;
import org.bukkit.inventory.Inventory;
-final class CraftMinecartHopper extends CraftMinecart implements HopperMinecart {
+final class CraftMinecartHopper extends CraftMinecart implements HopperMinecart, CraftLootableEntityInventory { // Paper
private final CraftInventory inventory;
CraftMinecartHopper(CraftServer server, EntityMinecartHopper entity) {
--
2.9.0