aboutsummaryrefslogtreecommitdiffhomepage
path: root/Spigot-Server-Patches-Unmapped/0096-LootTable-API-Replenishable-Lootables-Feature.patch
diff options
context:
space:
mode:
Diffstat (limited to 'Spigot-Server-Patches-Unmapped/0096-LootTable-API-Replenishable-Lootables-Feature.patch')
-rw-r--r--Spigot-Server-Patches-Unmapped/0096-LootTable-API-Replenishable-Lootables-Feature.patch736
1 files changed, 736 insertions, 0 deletions
diff --git a/Spigot-Server-Patches-Unmapped/0096-LootTable-API-Replenishable-Lootables-Feature.patch b/Spigot-Server-Patches-Unmapped/0096-LootTable-API-Replenishable-Lootables-Feature.patch
new file mode 100644
index 0000000000..7d27f20665
--- /dev/null
+++ b/Spigot-Server-Patches-Unmapped/0096-LootTable-API-Replenishable-Lootables-Feature.patch
@@ -0,0 +1,736 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Aikar <[email protected]>
+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 5baccb8d50c135ab20c38ffd0690f585514ce5af..eb04fdb172a50ec1f5b7fe78fa0e7655246abd60 100644
+--- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
++++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
+@@ -269,4 +269,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..5dfc3c8008d64ad4ed71b4904c897f5005491349
+--- /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.BlockPosition;
++import net.minecraft.world.level.World;
++import net.minecraft.world.level.block.entity.TileEntityLootable;
++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..019a06fa2b43cacd3bbd4d58aba71b3728f37581
+--- /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.World;
++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 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..59e8aea749bbba079e3304d9a5854280db2692e9
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventory.java
+@@ -0,0 +1,71 @@
++package com.destroystokyo.paper.loottable;
++
++import net.minecraft.world.level.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..904332454ede006f4ee33337d46b11674d78bef7
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventoryData.java
+@@ -0,0 +1,181 @@
++package com.destroystokyo.paper.loottable;
++
++import com.destroystokyo.paper.PaperWorldConfig;
++import net.minecraft.nbt.NBTTagCompound;
++import net.minecraft.nbt.NBTTagList;
++import net.minecraft.world.entity.player.EntityHuman;
++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..c682bd7700d8103533026d46cfc63a7abde5a5f4
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/loottable/PaperMinecartLootableInventory.java
+@@ -0,0 +1,62 @@
++package com.destroystokyo.paper.loottable;
++
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.entity.vehicle.EntityMinecartContainer;
++import net.minecraft.world.level.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.lootTable != null ? Bukkit.getLootTable(CraftNamespacedKey.fromMinecraft(entity.lootTable)) : 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) {
++ entity.lootTable = (table == null) ? null : CraftNamespacedKey.toMinecraft(table.getKey());
++ }
++
++ @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..9dae34370d014a291f025f83b55e18bff4619a23
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/loottable/PaperTileEntityLootableInventory.java
+@@ -0,0 +1,65 @@
++package com.destroystokyo.paper.loottable;
++
++import net.minecraft.server.MCUtil;
++import net.minecraft.world.level.World;
++import net.minecraft.world.level.block.entity.TileEntityLootable;
++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.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 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/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
+index 6cbb797cb0de4b26d8ddd7f0bf567f49bd36f9c0..2b1b46bda48c0b137fe914c47a387e6e72a1be40 100644
+--- a/src/main/java/net/minecraft/world/entity/Entity.java
++++ b/src/main/java/net/minecraft/world/entity/Entity.java
+@@ -158,6 +158,7 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, ne
+ };
+ // 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/EntityMinecartContainer.java b/src/main/java/net/minecraft/world/entity/vehicle/EntityMinecartContainer.java
+index c4b970c37b1792ac0022936f2df4740183621a0d..0166d11cb540a536390f486e1069d6119d8d23d6 100644
+--- a/src/main/java/net/minecraft/world/entity/vehicle/EntityMinecartContainer.java
++++ b/src/main/java/net/minecraft/world/entity/vehicle/EntityMinecartContainer.java
+@@ -46,6 +46,7 @@ public abstract class EntityMinecartContainer extends EntityMinecartAbstract imp
+ 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;
+
+@@ -203,12 +204,13 @@ public abstract class EntityMinecartContainer extends EntityMinecartAbstract imp
+ @Override
+ protected void saveData(NBTTagCompound nbttagcompound) {
+ super.saveData(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);
+ }
+
+@@ -217,11 +219,12 @@ public abstract class EntityMinecartContainer extends EntityMinecartAbstract imp
+ @Override
+ protected void loadData(NBTTagCompound nbttagcompound) {
+ super.loadData(nbttagcompound);
++ this.lootableData.loadNbt(nbttagcompound); // Paper
+ this.items = NonNullList.a(this.getSize(), ItemStack.b);
+ 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);
+ }
+
+@@ -252,14 +255,15 @@ 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);
+
+ if (entityhuman instanceof EntityPlayer) {
+ CriterionTriggers.N.a((EntityPlayer) entityhuman, this.lootTable);
+ }
+
+- this.lootTable = null;
++ //this.lootTable = null; // Paper
++ this.lootableData.processRefill(entityhuman); // Paper
+ LootTableInfo.Builder loottableinfo_builder = (new LootTableInfo.Builder((WorldServer) this.world)).set(LootContextParameters.ORIGIN, this.getPositionVector()).a(this.lootTableSeed);
+
+ if (entityhuman != null) {
+diff --git a/src/main/java/net/minecraft/world/level/block/entity/TileEntityLootable.java b/src/main/java/net/minecraft/world/level/block/entity/TileEntityLootable.java
+index 62e6833a90d7adae3c7df33e3bc73b4288e0370b..1508e267a38555820e2d31f3075adca185fbd4b6 100644
+--- a/src/main/java/net/minecraft/world/level/block/entity/TileEntityLootable.java
++++ b/src/main/java/net/minecraft/world/level/block/entity/TileEntityLootable.java
+@@ -27,6 +27,7 @@ public abstract class TileEntityLootable extends TileEntityContainer {
+ @Nullable
+ public MinecraftKey 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 TileEntityLootable(TileEntityTypes<?> tileentitytypes) {
+ super(tileentitytypes);
+@@ -42,16 +43,19 @@ public abstract class TileEntityLootable extends TileEntityContainer {
+ }
+
+ protected boolean b(NBTTagCompound nbttagcompound) {
++ this.lootableData.loadNbt(nbttagcompound); // Paper
+ if (nbttagcompound.hasKeyOfType("LootTable", 8)) {
+ this.lootTable = new MinecraftKey(nbttagcompound.getString("LootTable"));
++ try { org.bukkit.craftbukkit.util.CraftNamespacedKey.fromMinecraft(this.lootTable); } catch (IllegalArgumentException ex) { this.lootTable = null; } // Paper - validate
+ this.lootTableSeed = nbttagcompound.getLong("LootTableSeed");
+- return true;
++ return false; // Paper - always load the items, table may still remain
+ } else {
+ return false;
+ }
+ }
+
+ protected boolean c(NBTTagCompound nbttagcompound) {
++ this.lootableData.saveNbt(nbttagcompound); // Paper
+ if (this.lootTable == null) {
+ return false;
+ } else {
+@@ -60,19 +64,20 @@ 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);
+
+ if (entityhuman instanceof EntityPlayer) {
+ CriterionTriggers.N.a((EntityPlayer) entityhuman, this.lootTable);
+ }
+
+- this.lootTable = null;
++ //this.lootTable = null; // Paper
++ this.lootableData.processRefill(entityhuman); // Paper
+ LootTableInfo.Builder loottableinfo_builder = (new LootTableInfo.Builder((WorldServer) this.world)).set(LootContextParameters.ORIGIN, Vec3D.a((BaseBlockPosition) 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 524f27830752f424493c3ae8d793b871f6495594..dcf3f9265b0b00a7bbb9ff428e10da3c198ba08a 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 486fa8937d644f59a770db163482259525a7e465..54eb170fd533b0e91572601268fcbc167ed9bb5c 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<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 f0a7e61a26c4668a9aa823d641f29bdecd42dd1f..3512054ede5fd1dd7605444e827e30a0be47f935 100644
+--- a/src/main/java/org/bukkit/craftbukkit/block/CraftLootable.java
++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftLootable.java
+@@ -10,7 +10,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, com.destroystokyo.paper.loottable.PaperLootableBlockInventory { // Paper
+
+ public CraftLootable(Block block, Class<T> tileEntityClass) {
+ super(block, tileEntityClass);
+@@ -54,7 +54,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 cbd121c21adfaf098dadca33de16a2e68d83c19a..d9a2552782c9242fb84cc0c8309a614a44777509 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, 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 5ffb8108f456c2f7f3ed1a25249baccb4cbf4add..bf8b5b25d1af0c5129261e10abf2866521b2c375 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 17a42aec76f32a28b0c9885c60d1ed50c6727161..bfdcf01d2c6570493e86330d56500427dbb23146 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, EntityMinecartHopper entity) {