From 0000000000000000000000000000000000000000 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 9e0c4895403a264f927292db2ac06b00731986e3..6db1312035807c04b98408100fb0a5c04c07aff4 100644
--- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
+++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
@@ -267,4 +267,26 @@ public class PaperWorldConfig {
         this.frostedIceDelayMax = this.getInt("frosted-ice.delay.max", this.frostedIceDelayMax);
         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/PaperLootableBlockInventory.java b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableBlockInventory.java
new file mode 100644
index 0000000000000000000000000000000000000000..d6fce3112ebd8ef208c6fe45e0d887ec778fc092
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableBlockInventory.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 PaperLootableBlockInventory extends LootableBlockInventory, PaperLootableInventory {
+
+    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 PaperLootableInventoryData getLootableData() {
+        return getTileEntity().lootableData;
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/loottable/PaperLootableEntityInventory.java b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableEntityInventory.java
new file mode 100644
index 0000000000000000000000000000000000000000..5e637782d555cd68952e8bdbd47a63a636348ec6
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableEntityInventory.java
@@ -0,0 +1,28 @@
+package com.destroystokyo.paper.loottable;
+
+import net.minecraft.server.World;
+import org.bukkit.entity.Entity;
+
+public interface PaperLootableEntityInventory extends LootableEntityInventory, PaperLootableInventory {
+
+    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 PaperLootableInventoryData getLootableData() {
+        return getHandle().lootableData;
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventory.java b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventory.java
new file mode 100644
index 0000000000000000000000000000000000000000..856843fc917ff6a7252b7900f7772cc9debe3649
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventory.java
@@ -0,0 +1,71 @@
+package com.destroystokyo.paper.loottable;
+
+import net.minecraft.server.World;
+import org.bukkit.loot.Lootable;
+
+import java.util.UUID;
+
+public interface PaperLootableInventory extends LootableInventory, Lootable {
+
+    PaperLootableInventoryData getLootableData();
+    LootableInventory getAPILootableInventory();
+
+    World getNMSWorld();
+
+    default org.bukkit.World getBukkitWorld() {
+        return getNMSWorld().getWorld();
+    }
+
+    @Override
+    default boolean isRefillEnabled() {
+        return getNMSWorld().paperConfig.autoReplenishLootables;
+    }
+
+    @Override
+    default boolean hasBeenFilled() {
+        return getLastFilled() != -1;
+    }
+
+    @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/PaperLootableInventoryData.java b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventoryData.java
new file mode 100644
index 0000000000000000000000000000000000000000..b5401eaf974857455c17c3f9cfdedf2eb4bde321
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventoryData.java
@@ -0,0 +1,179 @@
+package com.destroystokyo.paper.loottable;
+
+import com.destroystokyo.paper.PaperWorldConfig;
+import net.minecraft.server.*;
+import org.bukkit.entity.Player;
+import org.bukkit.loot.LootTable;
+
+import javax.annotation.Nullable;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.UUID;
+
+public class PaperLootableInventoryData {
+
+    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 PaperLootableInventory lootable;
+
+    public PaperLootableInventoryData(PaperLootableInventory 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;
+    }
+
+    public boolean shouldReplenish(@Nullable EntityHuman player) {
+        LootTable table = this.lootable.getLootTable();
+
+        // No Loot Table associated
+        if (table == null) {
+            return false;
+        }
+
+        // ALWAYS process the first fill or if the feature is disabled
+        if (this.lastFill == -1 || !this.lootable.getNMSWorld().paperConfig.autoReplenishLootables) {
+            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;
+            int max = paperConfig.lootableRegenMax;
+            this.nextRefill = this.lastFill + (min + RANDOM.nextInt(max - min + 1)) * 1000L;
+            this.numRefills++;
+            if (paperConfig.changeLootTableSeedOnFill) {
+                this.lootable.setSeed(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.getCompound(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/com/destroystokyo/paper/loottable/PaperMinecartLootableInventory.java b/src/main/java/com/destroystokyo/paper/loottable/PaperMinecartLootableInventory.java
new file mode 100644
index 0000000000000000000000000000000000000000..f9fbc221bd8f9b04276611ef5b800595f23dedd7
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/loottable/PaperMinecartLootableInventory.java
@@ -0,0 +1,64 @@
+package com.destroystokyo.paper.loottable;
+
+import net.minecraft.server.Entity;
+import net.minecraft.server.EntityMinecartContainer;
+import net.minecraft.server.MinecraftKey;
+import net.minecraft.server.World;
+import org.bukkit.Bukkit;
+import org.bukkit.craftbukkit.util.CraftNamespacedKey;
+
+public class PaperMinecartLootableInventory implements PaperLootableEntityInventory {
+
+    private EntityMinecartContainer entity;
+
+    public PaperMinecartLootableInventory(EntityMinecartContainer entity) {
+        this.entity = entity;
+    }
+
+    @Override
+    public org.bukkit.loot.LootTable getLootTable() {
+        return entity.getLootTableKey() != null ? Bukkit.getLootTable(CraftNamespacedKey.fromMinecraft(entity.getLootTableKey())) : null;
+    }
+
+    @Override
+    public void setLootTable(org.bukkit.loot.LootTable table, long seed) {
+        setLootTable(table);
+        setSeed(seed);
+    }
+
+    @Override
+    public void setSeed(long seed) {
+        entity.lootTableSeed = seed;
+    }
+
+    @Override
+    public long getSeed() {
+        return entity.lootTableSeed;
+    }
+
+    @Override
+    public void setLootTable(org.bukkit.loot.LootTable table) {
+        MinecraftKey newKey = (table == null) ? null : CraftNamespacedKey.toMinecraft(table.getKey());
+        entity.setLootTable(newKey);
+    }
+
+    @Override
+    public PaperLootableInventoryData getLootableData() {
+        return entity.lootableData;
+    }
+
+    @Override
+    public Entity getHandle() {
+        return entity;
+    }
+
+    @Override
+    public LootableInventory getAPILootableInventory() {
+        return (LootableInventory) entity.getBukkitEntity();
+    }
+
+    @Override
+    public World getNMSWorld() {
+        return entity.world;
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/loottable/PaperTileEntityLootableInventory.java b/src/main/java/com/destroystokyo/paper/loottable/PaperTileEntityLootableInventory.java
new file mode 100644
index 0000000000000000000000000000000000000000..d50410532c27bd2aa932c2a3f5765ca1eede5304
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/loottable/PaperTileEntityLootableInventory.java
@@ -0,0 +1,67 @@
+package com.destroystokyo.paper.loottable;
+
+import net.minecraft.server.MCUtil;
+import net.minecraft.server.MinecraftKey;
+import net.minecraft.server.TileEntityLootable;
+import net.minecraft.server.World;
+import org.bukkit.Bukkit;
+import org.bukkit.craftbukkit.util.CraftNamespacedKey;
+
+public class PaperTileEntityLootableInventory implements PaperLootableBlockInventory {
+    private TileEntityLootable tileEntityLootable;
+
+    public PaperTileEntityLootableInventory(TileEntityLootable tileEntityLootable) {
+        this.tileEntityLootable = tileEntityLootable;
+    }
+
+    @Override
+    public org.bukkit.loot.LootTable getLootTable() {
+        return tileEntityLootable.getLootTableKey() != null ? Bukkit.getLootTable(CraftNamespacedKey.fromMinecraft(tileEntityLootable.getLootTableKey())) : null;
+    }
+
+    @Override
+    public void setLootTable(org.bukkit.loot.LootTable table, long seed) {
+        setLootTable(table);
+        setSeed(seed);
+    }
+
+    @Override
+    public void setLootTable(org.bukkit.loot.LootTable table) {
+        MinecraftKey newKey = (table == null) ? null : CraftNamespacedKey.toMinecraft(table.getKey());
+        tileEntityLootable.setLootTable(newKey);
+    }
+
+    @Override
+    public void setSeed(long seed) {
+        tileEntityLootable.setSeed(seed);
+    }
+
+    @Override
+    public long getSeed() {
+        return tileEntityLootable.getSeed();
+    }
+
+    @Override
+    public PaperLootableInventoryData getLootableData() {
+        return tileEntityLootable.lootableData;
+    }
+
+    @Override
+    public TileEntityLootable getTileEntity() {
+        return tileEntityLootable;
+    }
+
+    @Override
+    public LootableInventory getAPILootableInventory() {
+        World world = tileEntityLootable.getWorld();
+        if (world == null) {
+            return null;
+        }
+        return (LootableInventory) getBukkitWorld().getBlockAt(MCUtil.toLocation(world, tileEntityLootable.getPosition())).getState();
+    }
+
+    @Override
+    public World getNMSWorld() {
+        return tileEntityLootable.getWorld();
+    }
+}
diff --git a/src/main/java/net/minecraft/server/Entity.java b/src/main/java/net/minecraft/server/Entity.java
index d6fc93b5923096d3f092f561cbefdf26f2bbfb15..f90627a7f4c399f0515690b2709eaf8c7e884313 100644
--- a/src/main/java/net/minecraft/server/Entity.java
+++ b/src/main/java/net/minecraft/server/Entity.java
@@ -73,6 +73,7 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke
     };
     // Paper end
 
+    public com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData; // Paper
     private CraftEntity bukkitEntity;
 
     public CraftEntity getBukkitEntity() {
diff --git a/src/main/java/net/minecraft/server/EntityMinecartContainer.java b/src/main/java/net/minecraft/server/EntityMinecartContainer.java
index 34ceace23f82e2c0bbaaad01eaef0fd0045cf8e2..60efd439af8fa8ed46d81d703271aa945c19d77e 100644
--- a/src/main/java/net/minecraft/server/EntityMinecartContainer.java
+++ b/src/main/java/net/minecraft/server/EntityMinecartContainer.java
@@ -15,10 +15,11 @@ public abstract class EntityMinecartContainer extends EntityMinecartAbstract imp
     private NonNullList<ItemStack> items;
     private boolean c;
     @Nullable
-    public MinecraftKey lootTable;
+    public MinecraftKey lootTable; public MinecraftKey getLootTableKey() { return this.lootTable; } public void setLootTable(final MinecraftKey key) { this.lootTable = key; } // Paper - OBFHELPER
     public long lootTableSeed;
 
     // CraftBukkit start
+    { this.lootableData = new com.destroystokyo.paper.loottable.PaperLootableInventoryData(new com.destroystokyo.paper.loottable.PaperMinecartLootableInventory(this)); } // Paper
     public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
     private int maxStack = MAX_STACK;
 
@@ -169,12 +170,13 @@ public abstract class EntityMinecartContainer extends EntityMinecartAbstract imp
     @Override
     protected void b(NBTTagCompound nbttagcompound) {
         super.b(nbttagcompound);
+        this.lootableData.saveNbt(nbttagcompound); // Paper
         if (this.lootTable != null) {
             nbttagcompound.setString("LootTable", this.lootTable.toString());
             if (this.lootTableSeed != 0L) {
                 nbttagcompound.setLong("LootTableSeed", this.lootTableSeed);
             }
-        } else {
+        } if (true) { // Paper - Always save the items, Table may stick around
             ContainerUtil.a(nbttagcompound, this.items);
         }
 
@@ -183,11 +185,12 @@ public abstract class EntityMinecartContainer extends EntityMinecartAbstract imp
     @Override
     protected void a(NBTTagCompound nbttagcompound) {
         super.a(nbttagcompound);
+        this.lootableData.loadNbt(nbttagcompound); // Paper
         this.items = NonNullList.a(this.getSize(), ItemStack.a);
         if (nbttagcompound.hasKeyOfType("LootTable", 8)) {
             this.lootTable = new MinecraftKey(nbttagcompound.getString("LootTable"));
             this.lootTableSeed = nbttagcompound.getLong("LootTableSeed");
-        } else {
+        } if (true) { // Paper - always load the items, table may still remain
             ContainerUtil.b(nbttagcompound, this.items);
         }
 
@@ -213,10 +216,10 @@ public abstract class EntityMinecartContainer extends EntityMinecartAbstract imp
     }
 
     public void d(@Nullable EntityHuman entityhuman) {
-        if (this.lootTable != null && this.world.getMinecraftServer() != null) {
+        if (this.lootableData.shouldReplenish(entityhuman) && this.world.getMinecraftServer() != null) { // Paper
             LootTable loottable = this.world.getMinecraftServer().getLootTableRegistry().getLootTable(this.lootTable);
 
-            this.lootTable = null;
+            this.lootableData.processRefill(entityhuman); // Paper
             LootTableInfo.Builder loottableinfo_builder = (new LootTableInfo.Builder((WorldServer) this.world)).set(LootContextParameters.POSITION, new BlockPosition(this)).a(this.lootTableSeed);
 
             if (entityhuman != null) {
diff --git a/src/main/java/net/minecraft/server/TileEntityLootable.java b/src/main/java/net/minecraft/server/TileEntityLootable.java
index f5316a4045efdd6bffba226b4a6244961a8b245d..d4cbce3243fe1f4973c9c0ae0dbdab10e3390897 100644
--- a/src/main/java/net/minecraft/server/TileEntityLootable.java
+++ b/src/main/java/net/minecraft/server/TileEntityLootable.java
@@ -6,8 +6,9 @@ import javax.annotation.Nullable;
 public abstract class TileEntityLootable extends TileEntityContainer {
 
     @Nullable
-    public MinecraftKey lootTable;
-    public long lootTableSeed;
+    public MinecraftKey lootTable; public MinecraftKey getLootTableKey() { return this.lootTable; } public void setLootTable(final MinecraftKey key) { this.lootTable = key; } // Paper - OBFHELPER
+    public long lootTableSeed; public long getSeed() { return this.lootTableSeed; } public void setSeed(final long seed) { this.lootTableSeed = seed; } // Paper - OBFHELPER
+    public final com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData = new com.destroystokyo.paper.loottable.PaperLootableInventoryData(new com.destroystokyo.paper.loottable.PaperTileEntityLootableInventory(this)); // Paper
 
     protected TileEntityLootable(TileEntityTypes<?> tileentitytypes) {
         super(tileentitytypes);
@@ -23,16 +24,18 @@ public abstract class TileEntityLootable extends TileEntityContainer {
     }
 
     protected boolean d(NBTTagCompound nbttagcompound) {
+        this.lootableData.loadNbt(nbttagcompound); // Paper
         if (nbttagcompound.hasKeyOfType("LootTable", 8)) {
             this.lootTable = new MinecraftKey(nbttagcompound.getString("LootTable"));
             this.lootTableSeed = nbttagcompound.getLong("LootTableSeed");
-            return true;
+            return false; // Paper - always load the items, table may still remain
         } else {
             return false;
         }
     }
 
     protected boolean e(NBTTagCompound nbttagcompound) {
+        this.lootableData.saveNbt(nbttagcompound); // Paper
         if (this.lootTable == null) {
             return false;
         } else {
@@ -41,15 +44,15 @@ public abstract class TileEntityLootable extends TileEntityContainer {
                 nbttagcompound.setLong("LootTableSeed", this.lootTableSeed);
             }
 
-            return true;
+            return false; // Paper - always save the items, table may still remain
         }
     }
 
     public void d(@Nullable EntityHuman entityhuman) {
-        if (this.lootTable != null && this.world.getMinecraftServer() != null) {
+        if (this.lootableData.shouldReplenish(entityhuman) && this.world.getMinecraftServer() != null) { // Paper
             LootTable loottable = this.world.getMinecraftServer().getLootTableRegistry().getLootTable(this.lootTable);
 
-            this.lootTable = null;
+            this.lootableData.processRefill(entityhuman); // Paper
             LootTableInfo.Builder loottableinfo_builder = (new LootTableInfo.Builder((WorldServer) this.world)).set(LootContextParameters.POSITION, new BlockPosition(this.position)).a(this.lootTableSeed);
 
             if (entityhuman != null) {
diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java
index 2831419446f4bd531f614f59308c8f5b61933507..17d80b5c6e512e0c582b05c92bb795b004ba27c2 100644
--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java
+++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java
@@ -64,7 +64,7 @@ public class CraftBlockEntityState<T extends TileEntity> extends CraftBlockState
     }
 
     // gets the wrapped TileEntity
-    protected T getTileEntity() {
+    public T getTileEntity() { // Paper - protected -> public
         return tileEntity;
     }
 
diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftChest.java b/src/main/java/org/bukkit/craftbukkit/block/CraftChest.java
index 6beb992622ba90dd37159124107b48426e5d7776..019fa71181ade0037598ace6288db587d16b5179 100644
--- a/src/main/java/org/bukkit/craftbukkit/block/CraftChest.java
+++ b/src/main/java/org/bukkit/craftbukkit/block/CraftChest.java
@@ -11,8 +11,9 @@ import org.bukkit.craftbukkit.CraftWorld;
 import org.bukkit.craftbukkit.inventory.CraftInventory;
 import org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest;
 import org.bukkit.inventory.Inventory;
+import com.destroystokyo.paper.loottable.PaperLootableBlockInventory; // Paper
 
-public class CraftChest extends CraftLootable<TileEntityChest> implements Chest {
+public class CraftChest extends CraftLootable<TileEntityChest> implements Chest, PaperLootableBlockInventory { // Paper
 
     public CraftChest(final Block block) {
         super(block, TileEntityChest.class);
diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftLootable.java b/src/main/java/org/bukkit/craftbukkit/block/CraftLootable.java
index e1ad26a242b9bff8f5a567c24cf58b2150c24144..678aa09d477f653461276e5eab277e1abc253dd8 100644
--- a/src/main/java/org/bukkit/craftbukkit/block/CraftLootable.java
+++ b/src/main/java/org/bukkit/craftbukkit/block/CraftLootable.java
@@ -1,5 +1,6 @@
 package org.bukkit.craftbukkit.block;
 
+import com.destroystokyo.paper.loottable.PaperLootableBlockInventory;
 import net.minecraft.server.MinecraftKey;
 import net.minecraft.server.TileEntityLootable;
 import org.bukkit.Bukkit;
@@ -10,7 +11,7 @@ import org.bukkit.craftbukkit.util.CraftNamespacedKey;
 import org.bukkit.loot.LootTable;
 import org.bukkit.loot.Lootable;
 
-public abstract class CraftLootable<T extends TileEntityLootable> extends CraftContainer<T> implements Nameable, Lootable {
+public abstract class CraftLootable<T extends TileEntityLootable> extends CraftContainer<T> implements Nameable, Lootable, PaperLootableBlockInventory { // Paper
 
     public CraftLootable(Block block, Class<T> tileEntityClass) {
         super(block, tileEntityClass);
@@ -54,7 +55,7 @@ public abstract class CraftLootable<T extends TileEntityLootable> extends CraftC
         setLootTable(getLootTable(), seed);
     }
 
-    private void setLootTable(LootTable table, long seed) {
+    public void setLootTable(LootTable table, long seed) { // Paper - public
         MinecraftKey key = (table == null) ? null : CraftNamespacedKey.toMinecraft(table.getKey());
         getSnapshot().setLootTable(key, seed);
     }
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java
index e05624e6432f55c3efff3f9d765975557d1be16f..ab4807b2cd3cdcd61d8ac4ae2825df69dd2b7c64 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.PaperLootableEntityInventory; // Paper
 import net.minecraft.server.EntityMinecartChest;
 import org.bukkit.craftbukkit.CraftServer;
 import org.bukkit.craftbukkit.inventory.CraftInventory;
@@ -8,7 +9,7 @@ import org.bukkit.entity.minecart.StorageMinecart;
 import org.bukkit.inventory.Inventory;
 
 @SuppressWarnings("deprecation")
-public class CraftMinecartChest extends CraftMinecartContainer implements StorageMinecart {
+public class CraftMinecartChest extends CraftMinecartContainer implements StorageMinecart, PaperLootableEntityInventory { // Paper
     private final CraftInventory inventory;
 
     public CraftMinecartChest(CraftServer server, EntityMinecartChest entity) {
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartContainer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartContainer.java
index 2d776b520bd9b0b8f7475f10e59b02b2ad7b9d8b..fcc9787848c0a0a4025bdf698debf9592c818bff 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartContainer.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartContainer.java
@@ -47,7 +47,7 @@ public abstract class CraftMinecartContainer extends CraftMinecart implements Lo
         return getHandle().lootTableSeed;
     }
 
-    private void setLootTable(LootTable table, long seed) {
+    public void setLootTable(LootTable table, long seed) { // Paper
         MinecraftKey newKey = (table == null) ? null : CraftNamespacedKey.toMinecraft(table.getKey());
         getHandle().setLootTable(newKey, seed);
     }
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java
index 334bd5bb3ffc8b2bcdfca3995cdbe543cee8f311..f5b31237fc6e62345edcc3d6b02ff9e94237ae31 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.PaperLootableEntityInventory; // Paper
 import net.minecraft.server.EntityMinecartHopper;
 import org.bukkit.craftbukkit.CraftServer;
 import org.bukkit.craftbukkit.inventory.CraftInventory;
@@ -7,7 +8,7 @@ import org.bukkit.entity.EntityType;
 import org.bukkit.entity.minecart.HopperMinecart;
 import org.bukkit.inventory.Inventory;
 
-public final class CraftMinecartHopper extends CraftMinecartContainer implements HopperMinecart {
+public final class CraftMinecartHopper extends CraftMinecartContainer implements HopperMinecart, PaperLootableEntityInventory { // Paper
     private final CraftInventory inventory;
 
     public CraftMinecartHopper(CraftServer server, EntityMinecartHopper entity) {