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"

== AT ==
public org.bukkit.craftbukkit.block.CraftBlockEntityState getTileEntity()Lnet/minecraft/world/level/block/entity/BlockEntity;
public org.bukkit.craftbukkit.block.CraftLootable setLootTable(Lorg/bukkit/loot/LootTable;J)V
public org.bukkit.craftbukkit.entity.CraftMinecartContainer setLootTable(Lorg/bukkit/loot/LootTable;J)V

diff --git a/src/main/java/com/destroystokyo/paper/loottable/PaperContainerEntityLootableInventory.java b/src/main/java/com/destroystokyo/paper/loottable/PaperContainerEntityLootableInventory.java
new file mode 100644
index 0000000000000000000000000000000000000000..88e32ed64f90bfd277dac84ba4bd84f0d943f5f8
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/loottable/PaperContainerEntityLootableInventory.java
@@ -0,0 +1,63 @@
+package com.destroystokyo.paper.loottable;
+
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.vehicle.AbstractMinecartContainer;
+import net.minecraft.world.entity.vehicle.ContainerEntity;
+import net.minecraft.world.level.Level;
+import org.bukkit.Bukkit;
+import org.bukkit.craftbukkit.util.CraftNamespacedKey;
+
+public class PaperContainerEntityLootableInventory implements PaperLootableEntityInventory {
+
+    private final ContainerEntity entity;
+
+    public PaperContainerEntityLootableInventory(ContainerEntity entity) {
+        this.entity = entity;
+    }
+
+    @Override
+    public org.bukkit.loot.LootTable getLootTable() {
+        return entity.getLootTable() != null ? Bukkit.getLootTable(CraftNamespacedKey.fromMinecraft(entity.getLootTable())) : null;
+    }
+
+    @Override
+    public void setLootTable(org.bukkit.loot.LootTable table, long seed) {
+        setLootTable(table);
+        setSeed(seed);
+    }
+
+    @Override
+    public void setSeed(long seed) {
+        entity.setLootTableSeed(seed);
+    }
+
+    @Override
+    public long getSeed() {
+        return entity.getLootTableSeed();
+    }
+
+    @Override
+    public void setLootTable(org.bukkit.loot.LootTable table) {
+        entity.setLootTable((table == null) ? null : CraftNamespacedKey.toMinecraft(table.getKey()));
+    }
+
+    @Override
+    public PaperLootableInventoryData getLootableData() {
+        return entity.getLootableData();
+    }
+
+    @Override
+    public Entity getHandle() {
+        return entity.getEntity();
+    }
+
+    @Override
+    public LootableInventory getAPILootableInventory() {
+        return (LootableInventory) entity.getEntity().getBukkitEntity();
+    }
+
+    @Override
+    public Level getNMSWorld() {
+        return entity.getLevel();
+    }
+}
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..70ca5625ff5d13a8e9cd64953066a7e1547ff223
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableBlockInventory.java
@@ -0,0 +1,33 @@
+package com.destroystokyo.paper.loottable;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
+import org.bukkit.Chunk;
+import org.bukkit.block.Block;
+
+public interface PaperLootableBlockInventory extends LootableBlockInventory, PaperLootableInventory {
+
+    RandomizableContainerBlockEntity getTileEntity();
+
+    @Override
+    default LootableInventory getAPILootableInventory() {
+        return this;
+    }
+
+    @Override
+    default Level getNMSWorld() {
+        return getTileEntity().getLevel();
+    }
+
+    default Block getBlock() {
+        final BlockPos position = getTileEntity().getBlockPos();
+        final Chunk bukkitChunk = getTileEntity().getLevel().getChunkAt(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..2fba5bc0f982e143ad5f5bda55d768edc5f847df
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableEntityInventory.java
@@ -0,0 +1,28 @@
+package com.destroystokyo.paper.loottable;
+
+import net.minecraft.world.level.Level;
+import org.bukkit.entity.Entity;
+
+public interface PaperLootableEntityInventory extends LootableEntityInventory, PaperLootableInventory {
+
+    net.minecraft.world.entity.Entity getHandle();
+
+    @Override
+    default LootableInventory getAPILootableInventory() {
+        return this;
+    }
+
+    default Entity getEntity() {
+        return getHandle().getBukkitEntity();
+    }
+
+    @Override
+    default Level getNMSWorld() {
+        return getHandle().getCommandSenderWorld();
+    }
+
+    @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..ce135d990c785b02df468391ea622aa236290f07
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventory.java
@@ -0,0 +1,70 @@
+package com.destroystokyo.paper.loottable;
+
+import org.bukkit.loot.Lootable;
+import java.util.UUID;
+import net.minecraft.world.level.Level;
+
+public interface PaperLootableInventory extends LootableInventory, Lootable {
+
+    PaperLootableInventoryData getLootableData();
+    LootableInventory getAPILootableInventory();
+
+    Level getNMSWorld();
+
+    default org.bukkit.World getBukkitWorld() {
+        return getNMSWorld().getWorld();
+    }
+
+    @Override
+    default boolean isRefillEnabled() {
+        return getNMSWorld().paperConfig().lootables.autoReplenish;
+    }
+
+    @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..e5ea9f27a1936ed9e329e74317c91c5df89b9fbd
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventoryData.java
@@ -0,0 +1,179 @@
+package com.destroystokyo.paper.loottable;
+
+import io.papermc.paper.configuration.WorldConfiguration;
+import org.bukkit.entity.Player;
+import org.bukkit.loot.LootTable;
+import javax.annotation.Nullable;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.ListTag;
+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 net.minecraft.world.entity.player.Player 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().lootables.autoReplenish) {
+            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 WorldConfiguration paperConfig = this.lootable.getNMSWorld().paperConfig();
+
+        // Check if max refills has been hit
+        if (paperConfig.lootables.maxRefills != -1 && this.numRefills >= paperConfig.lootables.maxRefills) {
+            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.lootables.restrictPlayerReloot && hasPlayerLooted(player.getUUID())) {
+            event.setCancelled(true);
+        }
+        return event.callEvent();
+    }
+    public void processRefill(@Nullable net.minecraft.world.entity.player.Player player) {
+        this.lastFill = System.currentTimeMillis();
+        final WorldConfiguration paperConfig = this.lootable.getNMSWorld().paperConfig();
+        if (paperConfig.lootables.autoReplenish) {
+            long min = paperConfig.lootables.refreshMin.seconds();
+            long max = paperConfig.lootables.refreshMax.seconds();
+            this.nextRefill = this.lastFill + (min + RANDOM.nextLong(max - min + 1)) * 1000L;
+            this.numRefills++;
+            if (paperConfig.lootables.resetSeedOnFill) {
+                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.getUUID(), true);
+            }
+        } else {
+            this.lootable.clearLootTable();
+        }
+    }
+
+
+    public void loadNbt(CompoundTag base) {
+        if (!base.contains("Paper.LootableData", 10)) { // 10 = compound
+            return;
+        }
+        CompoundTag comp = base.getCompound("Paper.LootableData");
+        if (comp.contains("lastFill")) {
+            this.lastFill = comp.getLong("lastFill");
+        }
+        if (comp.contains("nextRefill")) {
+            this.nextRefill = comp.getLong("nextRefill");
+        }
+
+        if (comp.contains("numRefills")) {
+            this.numRefills = comp.getInt("numRefills");
+        }
+        if (comp.contains("lootedPlayers", 9)) { // 9 = list
+            ListTag 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 CompoundTag cmp = list.getCompound(i);
+                lootedPlayers.put(cmp.getUUID("UUID"), cmp.getLong("Time"));
+            }
+        }
+    }
+    public void saveNbt(CompoundTag base) {
+        CompoundTag comp = new CompoundTag();
+        if (this.nextRefill != -1) {
+            comp.putLong("nextRefill", this.nextRefill);
+        }
+        if (this.lastFill != -1) {
+            comp.putLong("lastFill", this.lastFill);
+        }
+        if (this.numRefills != 0) {
+            comp.putInt("numRefills", this.numRefills);
+        }
+        if (this.lootedPlayers != null && !this.lootedPlayers.isEmpty()) {
+            ListTag list = new ListTag();
+            for (Map.Entry<UUID, Long> entry : this.lootedPlayers.entrySet()) {
+                CompoundTag cmp = new CompoundTag();
+                cmp.putUUID("UUID", entry.getKey());
+                cmp.putLong("Time", entry.getValue());
+                list.add(cmp);
+            }
+            comp.put("lootedPlayers", list);
+        }
+
+        if (!comp.isEmpty()) {
+            base.put("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/PaperTileEntityLootableInventory.java b/src/main/java/com/destroystokyo/paper/loottable/PaperTileEntityLootableInventory.java
new file mode 100644
index 0000000000000000000000000000000000000000..9cfa5d36a6991067a3866e0d437749fafcc0158e
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/loottable/PaperTileEntityLootableInventory.java
@@ -0,0 +1,65 @@
+package com.destroystokyo.paper.loottable;
+
+import io.papermc.paper.util.MCUtil;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
+import org.bukkit.Bukkit;
+import org.bukkit.craftbukkit.util.CraftNamespacedKey;
+
+public class PaperTileEntityLootableInventory implements PaperLootableBlockInventory {
+    private RandomizableContainerBlockEntity tileEntityLootable;
+
+    public PaperTileEntityLootableInventory(RandomizableContainerBlockEntity tileEntityLootable) {
+        this.tileEntityLootable = tileEntityLootable;
+    }
+
+    @Override
+    public org.bukkit.loot.LootTable getLootTable() {
+        return tileEntityLootable.lootTable != null ? Bukkit.getLootTable(CraftNamespacedKey.fromMinecraft(tileEntityLootable.lootTable)) : 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) {
+        tileEntityLootable.lootTable = (table == null) ? null : CraftNamespacedKey.toMinecraft(table.getKey());
+    }
+
+    @Override
+    public void setSeed(long seed) {
+        tileEntityLootable.lootTableSeed = seed;
+    }
+
+    @Override
+    public long getSeed() {
+        return tileEntityLootable.lootTableSeed;
+    }
+
+    @Override
+    public PaperLootableInventoryData getLootableData() {
+        return tileEntityLootable.lootableData;
+    }
+
+    @Override
+    public RandomizableContainerBlockEntity getTileEntity() {
+        return tileEntityLootable;
+    }
+
+    @Override
+    public LootableInventory getAPILootableInventory() {
+        Level world = tileEntityLootable.getLevel();
+        if (world == null) {
+            return null;
+        }
+        return (LootableInventory) getBukkitWorld().getBlockAt(MCUtil.toLocation(world, tileEntityLootable.getBlockPos())).getState();
+    }
+
+    @Override
+    public Level getNMSWorld() {
+        return tileEntityLootable.getLevel();
+    }
+}
diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
index c070fd1b224aeeaa9ebb054105a0f73285956da1..0c0060cdc35e0d8d3098a0ade8080f91dbc81a2b 100644
--- a/src/main/java/net/minecraft/world/entity/Entity.java
+++ b/src/main/java/net/minecraft/world/entity/Entity.java
@@ -232,6 +232,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource {
     }
     // Paper end
 
+    public com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData; // Paper
     private CraftEntity bukkitEntity;
 
     public CraftEntity getBukkitEntity() {
diff --git a/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java
index 9347faecdaa3ef8c375fe8b0a89fc3385b6823bd..cc74eeb45913fab03e85969957215d2811252a83 100644
--- a/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java
+++ b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java
@@ -32,6 +32,20 @@ public abstract class AbstractMinecartContainer extends AbstractMinecart impleme
     public ResourceLocation lootTable;
     public long lootTableSeed;
 
+    // Paper start
+    {
+        this.lootableData = new com.destroystokyo.paper.loottable.PaperLootableInventoryData(new com.destroystokyo.paper.loottable.PaperContainerEntityLootableInventory(this));
+    }
+    @Override
+    public Entity getEntity() {
+        return this;
+    }
+
+    @Override
+    public com.destroystokyo.paper.loottable.PaperLootableInventoryData getLootableData() {
+        return this.lootableData;
+    }
+    // Paper end
     // CraftBukkit start
     public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
     private int maxStack = MAX_STACK;
@@ -134,12 +148,14 @@ public abstract class AbstractMinecartContainer extends AbstractMinecart impleme
     @Override
     protected void addAdditionalSaveData(CompoundTag nbt) {
         super.addAdditionalSaveData(nbt);
+        this.lootableData.saveNbt(nbt); // Paper
         this.addChestVehicleSaveData(nbt);
     }
 
     @Override
     protected void readAdditionalSaveData(CompoundTag nbt) {
         super.readAdditionalSaveData(nbt);
+        this.lootableData.loadNbt(nbt); // Paper
         this.readChestVehicleSaveData(nbt);
     }
 
diff --git a/src/main/java/net/minecraft/world/entity/vehicle/ChestBoat.java b/src/main/java/net/minecraft/world/entity/vehicle/ChestBoat.java
index e1c4289ff790c691fdae8f7653414a60eb196dc2..a99e2586962952332dd9904ef5bb50a0d3083266 100644
--- a/src/main/java/net/minecraft/world/entity/vehicle/ChestBoat.java
+++ b/src/main/java/net/minecraft/world/entity/vehicle/ChestBoat.java
@@ -65,12 +65,14 @@ public class ChestBoat extends Boat implements HasCustomInventoryScreen, Contain
     @Override
     protected void addAdditionalSaveData(CompoundTag nbt) {
         super.addAdditionalSaveData(nbt);
+        this.lootableData.saveNbt(nbt); // Paper
         this.addChestVehicleSaveData(nbt);
     }
 
     @Override
     protected void readAdditionalSaveData(CompoundTag nbt) {
         super.readAdditionalSaveData(nbt);
+        this.lootableData.loadNbt(nbt); // Paper
         this.readChestVehicleSaveData(nbt);
     }
 
@@ -226,6 +228,20 @@ public class ChestBoat extends Boat implements HasCustomInventoryScreen, Contain
         this.itemStacks = NonNullList.withSize(this.getContainerSize(), ItemStack.EMPTY);
     }
 
+    // Paper start
+    {
+        this.lootableData = new com.destroystokyo.paper.loottable.PaperLootableInventoryData(new com.destroystokyo.paper.loottable.PaperContainerEntityLootableInventory(this));
+    }
+    @Override
+    public Entity getEntity() {
+        return this;
+    }
+
+    @Override
+    public com.destroystokyo.paper.loottable.PaperLootableInventoryData getLootableData() {
+        return this.lootableData;
+    }
+    // Paper end
     // CraftBukkit start
     public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
     private int maxStack = MAX_STACK;
diff --git a/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java b/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java
index e2a9782e0ae60eb5557dce0831084c5722687df6..6ae9cccb28b2354ee47e6682082f92f3ecd56a67 100644
--- a/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java
+++ b/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java
@@ -61,7 +61,7 @@ public interface ContainerEntity extends Container, MenuProvider {
             if (this.getLootTableSeed() != 0L) {
                 nbt.putLong("LootTableSeed", this.getLootTableSeed());
             }
-        } else {
+        } else if (true) { // Paper - always load the items, table may still remain
             ContainerHelper.saveAllItems(nbt, this.getItemStacks());
         }
 
@@ -72,7 +72,7 @@ public interface ContainerEntity extends Container, MenuProvider {
         if (nbt.contains("LootTable", 8)) {
             this.setLootTable(new ResourceLocation(nbt.getString("LootTable")));
             this.setLootTableSeed(nbt.getLong("LootTableSeed"));
-        } else {
+        } else if (true) { // Paper - always load the items, table may still remain
             ContainerHelper.loadAllItems(nbt, this.getItemStacks());
         }
 
@@ -104,13 +104,15 @@ public interface ContainerEntity extends Container, MenuProvider {
 
     default void unpackChestVehicleLootTable(@Nullable Player player) {
         MinecraftServer minecraftServer = this.getLevel().getServer();
-        if (this.getLootTable() != null && minecraftServer != null) {
+        if (this.getLootableData().shouldReplenish(player) && minecraftServer != null) { // Paper
             LootTable lootTable = minecraftServer.getLootTables().get(this.getLootTable());
             if (player != null) {
                 CriteriaTriggers.GENERATE_LOOT.trigger((ServerPlayer)player, this.getLootTable());
             }
 
-            this.setLootTable((ResourceLocation)null);
+            // this.setLootTable((ResourceLocation)null); // Paper
+            this.getLootableData().processRefill(player); // Paper
+
             LootContext.Builder builder = (new LootContext.Builder((ServerLevel)this.getLevel())).withParameter(LootContextParams.ORIGIN, this.position()).withOptionalRandomSeed(this.getLootTableSeed());
             if (player != null) {
                 builder.withLuck(player.getLuck()).withParameter(LootContextParams.THIS_ENTITY, player);
@@ -184,4 +186,13 @@ public interface ContainerEntity extends Container, MenuProvider {
     default boolean isChestVehicleStillValid(Player player) {
         return !this.isRemoved() && this.position().closerThan(player.position(), 8.0D);
     }
+    // Paper start
+    default Entity getEntity() {
+        throw new UnsupportedOperationException();
+    }
+
+    default com.destroystokyo.paper.loottable.PaperLootableInventoryData getLootableData() {
+        throw new UnsupportedOperationException();
+    }
+    // Paper end
 }
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 216fc20326d71121098430dc1b9f7477265a91b7..e3bee2df77d87630e96621470e940d9d9e152e7f 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
@@ -28,6 +28,7 @@ public abstract class RandomizableContainerBlockEntity extends BaseContainerBloc
     @Nullable
     public ResourceLocation lootTable;
     public long lootTableSeed;
+    public final com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData = new com.destroystokyo.paper.loottable.PaperLootableInventoryData(new com.destroystokyo.paper.loottable.PaperTileEntityLootableInventory(this)); // Paper
 
     protected RandomizableContainerBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
         super(type, pos, state);
@@ -42,16 +43,19 @@ public abstract class RandomizableContainerBlockEntity extends BaseContainerBloc
     }
 
     protected boolean tryLoadLootTable(CompoundTag nbt) {
+        this.lootableData.loadNbt(nbt); // Paper
         if (nbt.contains("LootTable", 8)) {
             this.lootTable = new ResourceLocation(nbt.getString("LootTable"));
+            try { org.bukkit.craftbukkit.util.CraftNamespacedKey.fromMinecraft(this.lootTable); } catch (IllegalArgumentException ex) { this.lootTable = null; } // Paper - validate
             this.lootTableSeed = nbt.getLong("LootTableSeed");
-            return true;
+            return false; // Paper - always load the items, table may still remain
         } else {
             return false;
         }
     }
 
     protected boolean trySaveLootTable(CompoundTag nbt) {
+        this.lootableData.saveNbt(nbt); // Paper
         if (this.lootTable == null) {
             return false;
         } else {
@@ -60,18 +64,19 @@ public abstract class RandomizableContainerBlockEntity extends BaseContainerBloc
                 nbt.putLong("LootTableSeed", this.lootTableSeed);
             }
 
-            return true;
+            return false; // Paper - always save the items, table may still remain
         }
     }
 
     public void unpackLootTable(@Nullable Player player) {
-        if (this.lootTable != null && this.level.getServer() != null) {
+        if (this.lootableData.shouldReplenish(player) && this.level.getServer() != null) { // Paper
             LootTable lootTable = this.level.getServer().getLootTables().get(this.lootTable);
             if (player instanceof ServerPlayer) {
                 CriteriaTriggers.GENERATE_LOOT.trigger((ServerPlayer)player, this.lootTable);
             }
 
-            this.lootTable = null;
+            //this.lootTable = null; // Paper
+            this.lootableData.processRefill(player); // Paper
             LootContext.Builder builder = (new LootContext.Builder((ServerLevel)this.level)).withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(this.worldPosition)).withOptionalRandomSeed(this.lootTableSeed);
             if (player != null) {
                 builder.withLuck(player.getLuck()).withParameter(LootContextParams.THIS_ENTITY, player);
diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftChest.java b/src/main/java/org/bukkit/craftbukkit/block/CraftChest.java
index b31fa63019822bc172d85427b3f3f2c154d7e179..82b3f3b3aced73ce136b6b94fe212972ac6090ef 100644
--- a/src/main/java/org/bukkit/craftbukkit/block/CraftChest.java
+++ b/src/main/java/org/bukkit/craftbukkit/block/CraftChest.java
@@ -12,8 +12,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<ChestBlockEntity> implements Chest {
+public class CraftChest extends CraftLootable<ChestBlockEntity> implements Chest, PaperLootableBlockInventory { // Paper
 
     public CraftChest(World world, ChestBlockEntity tileEntity) {
         super(world, tileEntity);
diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftLootable.java b/src/main/java/org/bukkit/craftbukkit/block/CraftLootable.java
index 982adacb361b0590799dc68f9b7c13c7195627fd..e49eece9bff3a53469673d03a7bbf8f9cf8776b8 100644
--- a/src/main/java/org/bukkit/craftbukkit/block/CraftLootable.java
+++ b/src/main/java/org/bukkit/craftbukkit/block/CraftLootable.java
@@ -9,7 +9,7 @@ import org.bukkit.craftbukkit.util.CraftNamespacedKey;
 import org.bukkit.loot.LootTable;
 import org.bukkit.loot.Lootable;
 
-public abstract class CraftLootable<T extends RandomizableContainerBlockEntity> extends CraftContainer<T> implements Nameable, Lootable {
+public abstract class CraftLootable<T extends RandomizableContainerBlockEntity> extends CraftContainer<T> implements Nameable, Lootable, com.destroystokyo.paper.loottable.PaperLootableBlockInventory { // Paper
 
     public CraftLootable(World world, T tileEntity) {
         super(world, tileEntity);
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java
index ae07db69b11b993b50782c94aa5c45b97d949612..06a96f027f90fd5bf05de72c8722ff5a81608b66 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java
@@ -11,8 +11,7 @@ import org.bukkit.entity.EntityType;
 import org.bukkit.inventory.Inventory;
 import org.bukkit.loot.LootTable;
 
-public class CraftChestBoat extends CraftBoat implements org.bukkit.entity.ChestBoat {
-
+public class CraftChestBoat extends CraftBoat implements org.bukkit.entity.ChestBoat, com.destroystokyo.paper.loottable.PaperLootableEntityInventory { // Paper
     private final Inventory inventory;
 
     public CraftChestBoat(CraftServer server, ChestBoat entity) {
@@ -66,7 +65,7 @@ public class CraftChestBoat extends CraftBoat implements org.bukkit.entity.Chest
         return this.getHandle().getLootTableSeed();
     }
 
-    private void setLootTable(LootTable table, long seed) {
+    public void setLootTable(LootTable table, long seed) { // Paper - change visibility since it overrides a public method
         ResourceLocation newKey = (table == null) ? null : CraftNamespacedKey.toMinecraft(table.getKey());
         this.getHandle().setLootTable(newKey);
         this.getHandle().setLootTableSeed(seed);
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java
index eb21b8457774d5ac765fa9008157cb29d9b72509..abf58bef2042a9efba5a78fd7f97339deceaa780 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java
@@ -8,7 +8,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, com.destroystokyo.paper.loottable.PaperLootableEntityInventory { // Paper
     private final CraftInventory inventory;
 
     public CraftMinecartChest(CraftServer server, MinecartChest entity) {
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java
index 34b8f103625f087bb725bed595dd9c30f4a6f70c..ee9648739fb39c5842063d7442df6eb5c9336d7f 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java
@@ -7,7 +7,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, com.destroystokyo.paper.loottable.PaperLootableEntityInventory { // Paper
     private final CraftInventory inventory;
 
     public CraftMinecartHopper(CraftServer server, MinecartHopper entity) {
