aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/0094-LootTable-API-and-replenishable-lootables.patch
diff options
context:
space:
mode:
Diffstat (limited to 'patches/server/0094-LootTable-API-and-replenishable-lootables.patch')
-rw-r--r--patches/server/0094-LootTable-API-and-replenishable-lootables.patch964
1 files changed, 964 insertions, 0 deletions
diff --git a/patches/server/0094-LootTable-API-and-replenishable-lootables.patch b/patches/server/0094-LootTable-API-and-replenishable-lootables.patch
new file mode 100644
index 0000000000..97e331b6e4
--- /dev/null
+++ b/patches/server/0094-LootTable-API-and-replenishable-lootables.patch
@@ -0,0 +1,964 @@
+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 and replenishable lootables
+
+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/PaperLootable.java b/src/main/java/com/destroystokyo/paper/loottable/PaperLootable.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a53d51be1da25b87f2bc0a29a196d8f9996dbd2b
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootable.java
+@@ -0,0 +1,21 @@
++package com.destroystokyo.paper.loottable;
++
++import org.bukkit.loot.LootTable;
++import org.bukkit.loot.Lootable;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public interface PaperLootable extends Lootable {
++
++ @Override
++ default void setLootTable(final @Nullable LootTable table) {
++ this.setLootTable(table, this.getSeed());
++ }
++
++ @Override
++ default void setSeed(final long seed) {
++ this.setLootTable(this.getLootTable(), seed);
++ }
++}
+diff --git a/src/main/java/com/destroystokyo/paper/loottable/PaperLootableBlock.java b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableBlock.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9e9ea13234703d3e4a39eed2b007e8be69dfbd12
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableBlock.java
+@@ -0,0 +1,27 @@
++package com.destroystokyo.paper.loottable;
++
++import net.minecraft.world.RandomizableContainer;
++import org.bukkit.craftbukkit.CraftLootTable;
++import org.bukkit.loot.LootTable;
++import org.checkerframework.checker.nullness.qual.Nullable;
++
++public interface PaperLootableBlock extends PaperLootable {
++
++ RandomizableContainer getRandomizableContainer();
++
++ /* Lootable */
++ @Override
++ default @Nullable LootTable getLootTable() {
++ return CraftLootTable.minecraftToBukkit(this.getRandomizableContainer().getLootTable());
++ }
++
++ @Override
++ default void setLootTable(final @Nullable LootTable table, final long seed) {
++ this.getRandomizableContainer().setLootTable(CraftLootTable.bukkitToMinecraft(table), seed);
++ }
++
++ @Override
++ default long getSeed() {
++ return this.getRandomizableContainer().getLootTableSeed();
++ }
++}
+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..0699c60920333ea1fec04e3c94d952244d2abeae
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableBlockInventory.java
+@@ -0,0 +1,26 @@
++package com.destroystokyo.paper.loottable;
++
++import java.util.Objects;
++import net.minecraft.core.BlockPos;
++import org.bukkit.block.Block;
++import org.bukkit.craftbukkit.block.CraftBlock;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public interface PaperLootableBlockInventory extends LootableBlockInventory, PaperLootableInventory, PaperLootableBlock {
++
++ /* PaperLootableInventory */
++ @Override
++ default PaperLootableInventoryData lootableDataForAPI() {
++ return Objects.requireNonNull(this.getRandomizableContainer().lootableData(), "Can only manage loot tables on tile entities with lootableData");
++ }
++
++ /* LootableBlockInventory */
++ @Override
++ default Block getBlock() {
++ final BlockPos position = this.getRandomizableContainer().getBlockPos();
++ return CraftBlock.at(this.getNMSWorld(), position);
++ }
++
++}
+diff --git a/src/main/java/com/destroystokyo/paper/loottable/PaperLootableEntity.java b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableEntity.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d933054535c83f877888cd36cd8bd8bf9d93a9df
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableEntity.java
+@@ -0,0 +1,29 @@
++package com.destroystokyo.paper.loottable;
++
++import net.minecraft.world.entity.vehicle.ContainerEntity;
++import org.bukkit.craftbukkit.CraftLootTable;
++import org.bukkit.loot.LootTable;
++import org.bukkit.loot.Lootable;
++import org.checkerframework.checker.nullness.qual.Nullable;
++
++public interface PaperLootableEntity extends Lootable {
++
++ ContainerEntity getHandle();
++
++ /* Lootable */
++ @Override
++ default @Nullable LootTable getLootTable() {
++ return CraftLootTable.minecraftToBukkit(this.getHandle().getContainerLootTable());
++ }
++
++ @Override
++ default void setLootTable(final @Nullable LootTable table, final long seed) {
++ this.getHandle().setContainerLootTable(CraftLootTable.bukkitToMinecraft(table));
++ this.getHandle().setContainerLootTableSeed(seed);
++ }
++
++ @Override
++ default long getSeed() {
++ return this.getHandle().getContainerLootTableSeed();
++ }
++}
+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..5c57acc95f638a8bcb351ae44e9434a056835470
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableEntityInventory.java
+@@ -0,0 +1,26 @@
++package com.destroystokyo.paper.loottable;
++
++import net.minecraft.world.level.Level;
++import org.bukkit.entity.Entity;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public interface PaperLootableEntityInventory extends LootableEntityInventory, PaperLootableInventory, PaperLootableEntity {
++
++ /* PaperLootableInventory */
++ @Override
++ default Level getNMSWorld() {
++ return this.getHandle().level();
++ }
++
++ @Override
++ default PaperLootableInventoryData lootableDataForAPI() {
++ return this.getHandle().lootableData();
++ }
++
++ /* LootableEntityInventory */
++ default Entity getEntity() {
++ return ((net.minecraft.world.entity.Entity) this.getHandle()).getBukkitEntity();
++ }
++}
+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..9e7c22ef49f1699df298f7121d50d27b4cb0923f
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventory.java
+@@ -0,0 +1,79 @@
++package com.destroystokyo.paper.loottable;
++
++import java.util.UUID;
++import net.minecraft.world.level.Level;
++import org.bukkit.World;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public interface PaperLootableInventory extends PaperLootable, LootableInventory {
++
++ /* impl */
++ PaperLootableInventoryData lootableDataForAPI();
++
++ Level getNMSWorld();
++
++ default World getBukkitWorld() {
++ return this.getNMSWorld().getWorld();
++ }
++
++ /* LootableInventory */
++ @Override
++ default boolean isRefillEnabled() {
++ return this.getNMSWorld().paperConfig().lootables.autoReplenish;
++ }
++
++ @Override
++ default boolean hasBeenFilled() {
++ return this.getLastFilled() != -1;
++ }
++
++ @Override
++ default boolean hasPlayerLooted(final UUID player) {
++ return this.lootableDataForAPI().hasPlayerLooted(player);
++ }
++
++ @Override
++ default boolean canPlayerLoot(final UUID player) {
++ return this.lootableDataForAPI().canPlayerLoot(player, this.getNMSWorld().paperConfig());
++ }
++
++ @Override
++ default Long getLastLooted(final UUID player) {
++ return this.lootableDataForAPI().getLastLooted(player);
++ }
++
++ @Override
++ default boolean setHasPlayerLooted(final UUID player, final boolean looted) {
++ final boolean hasLooted = this.hasPlayerLooted(player);
++ if (hasLooted != looted) {
++ this.lootableDataForAPI().setPlayerLootedState(player, looted);
++ }
++ return hasLooted;
++ }
++
++ @Override
++ default boolean hasPendingRefill() {
++ final long nextRefill = this.lootableDataForAPI().getNextRefill();
++ return nextRefill != -1 && nextRefill > this.lootableDataForAPI().getLastFill();
++ }
++
++ @Override
++ default long getLastFilled() {
++ return this.lootableDataForAPI().getLastFill();
++ }
++
++ @Override
++ default long getNextRefill() {
++ return this.lootableDataForAPI().getNextRefill();
++ }
++
++ @Override
++ default long setNextRefill(long refillAt) {
++ if (refillAt < -1) {
++ refillAt = -1;
++ }
++ return this.lootableDataForAPI().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..861bff267cb397e13e8e1c79bd0776b130c6e5da
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventoryData.java
+@@ -0,0 +1,249 @@
++package com.destroystokyo.paper.loottable;
++
++import io.papermc.paper.configuration.WorldConfiguration;
++import io.papermc.paper.configuration.type.DurationOrDisabled;
++import java.util.HashMap;
++import java.util.Map;
++import java.util.Objects;
++import java.util.Random;
++import java.util.UUID;
++import java.util.concurrent.TimeUnit;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.nbt.ListTag;
++import net.minecraft.nbt.Tag;
++import net.minecraft.world.RandomizableContainer;
++import net.minecraft.world.entity.vehicle.ContainerEntity;
++import org.bukkit.entity.Player;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public class PaperLootableInventoryData {
++
++ private static final Random RANDOM = new Random();
++
++ private long lastFill = -1;
++ private long nextRefill = -1;
++ private int numRefills = 0;
++ private @Nullable Map<UUID, Long> lootedPlayers;
++
++ public long getLastFill() {
++ return this.lastFill;
++ }
++
++ long getNextRefill() {
++ return this.nextRefill;
++ }
++
++ long setNextRefill(final long nextRefill) {
++ final long prev = this.nextRefill;
++ this.nextRefill = nextRefill;
++ return prev;
++ }
++
++ public <T> boolean shouldReplenish(final T lootTableHolder, final LootTableInterface<T> holderInterface, final net.minecraft.world.entity.player.@Nullable Player player) {
++
++ // No Loot Table associated
++ if (!holderInterface.hasLootTable(lootTableHolder)) {
++ return false;
++ }
++
++ // ALWAYS process the first fill or if the feature is disabled
++ if (this.lastFill == -1 || !holderInterface.paperConfig(lootTableHolder).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 = holderInterface.paperConfig(lootTableHolder);
++
++ // 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();
++ final LootableInventoryReplenishEvent event = new LootableInventoryReplenishEvent(bukkitPlayer, holderInterface.getInventoryForEvent(lootTableHolder));
++ event.setCancelled(!this.canPlayerLoot(player.getUUID(), paperConfig));
++ return event.callEvent();
++ }
++
++ public interface LootTableInterface<T> {
++
++ WorldConfiguration paperConfig(T holder);
++
++ void setSeed(T holder, long seed);
++
++ boolean hasLootTable(T holder);
++
++ LootableInventory getInventoryForEvent(T holder);
++ }
++
++ public static final LootTableInterface<RandomizableContainer> CONTAINER = new LootTableInterface<>() {
++ @Override
++ public WorldConfiguration paperConfig(final RandomizableContainer holder) {
++ return Objects.requireNonNull(holder.getLevel(), "Can only manager loot replenishment on block entities in a world").paperConfig();
++ }
++
++ @Override
++ public void setSeed(final RandomizableContainer holder, final long seed) {
++ holder.setLootTableSeed(seed);
++ }
++
++ @Override
++ public boolean hasLootTable(final RandomizableContainer holder) {
++ return holder.getLootTable() != null;
++ }
++
++ @Override
++ public LootableInventory getInventoryForEvent(final RandomizableContainer holder) {
++ return holder.getLootableInventory();
++ }
++ };
++
++ public static final LootTableInterface<ContainerEntity> ENTITY = new LootTableInterface<>() {
++ @Override
++ public WorldConfiguration paperConfig(final ContainerEntity holder) {
++ return holder.level().paperConfig();
++ }
++
++ @Override
++ public void setSeed(final ContainerEntity holder, final long seed) {
++ holder.setContainerLootTableSeed(seed);
++ }
++
++ @Override
++ public boolean hasLootTable(final ContainerEntity holder) {
++ return holder.getContainerLootTable() != null;
++ }
++
++ @Override
++ public LootableInventory getInventoryForEvent(final ContainerEntity holder) {
++ return holder.getLootableInventory();
++ }
++ };
++
++ public <T> boolean shouldClearLootTable(final T lootTableHolder, final LootTableInterface<T> holderInterface, final net.minecraft.world.entity.player.@Nullable Player player) {
++ this.lastFill = System.currentTimeMillis();
++ final WorldConfiguration paperConfig = holderInterface.paperConfig(lootTableHolder);
++ if (paperConfig.lootables.autoReplenish) {
++ final long min = paperConfig.lootables.refreshMin.seconds();
++ final long max = paperConfig.lootables.refreshMax.seconds();
++ this.nextRefill = this.lastFill + (min + RANDOM.nextLong(max - min + 1)) * 1000L;
++ this.numRefills++;
++ if (paperConfig.lootables.resetSeedOnFill) {
++ holderInterface.setSeed(lootTableHolder, 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);
++ }
++ return false;
++ }
++ return true;
++ }
++
++ private static final String ROOT = "Paper.LootableData";
++ private static final String LAST_FILL = "lastFill";
++ private static final String NEXT_REFILL = "nextRefill";
++ private static final String NUM_REFILLS = "numRefills";
++ private static final String LOOTED_PLAYERS = "lootedPlayers";
++
++ public void loadNbt(final CompoundTag base) {
++ if (!base.contains(ROOT, Tag.TAG_COMPOUND)) {
++ return;
++ }
++ final CompoundTag comp = base.getCompound(ROOT);
++ if (comp.contains(LAST_FILL)) {
++ this.lastFill = comp.getLong(LAST_FILL);
++ }
++ if (comp.contains(NEXT_REFILL)) {
++ this.nextRefill = comp.getLong(NEXT_REFILL);
++ }
++
++ if (comp.contains(NUM_REFILLS)) {
++ this.numRefills = comp.getInt(NUM_REFILLS);
++ }
++ if (comp.contains(LOOTED_PLAYERS, Tag.TAG_LIST)) {
++ final ListTag list = comp.getList(LOOTED_PLAYERS, Tag.TAG_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);
++ this.lootedPlayers.put(cmp.getUUID("UUID"), cmp.getLong("Time"));
++ }
++ }
++ }
++
++ public void saveNbt(final CompoundTag base) {
++ final CompoundTag comp = new CompoundTag();
++ if (this.nextRefill != -1) {
++ comp.putLong(NEXT_REFILL, this.nextRefill);
++ }
++ if (this.lastFill != -1) {
++ comp.putLong(LAST_FILL, this.lastFill);
++ }
++ if (this.numRefills != 0) {
++ comp.putInt(NUM_REFILLS, this.numRefills);
++ }
++ if (this.lootedPlayers != null && !this.lootedPlayers.isEmpty()) {
++ final ListTag list = new ListTag();
++ for (final Map.Entry<UUID, Long> entry : this.lootedPlayers.entrySet()) {
++ final CompoundTag cmp = new CompoundTag();
++ cmp.putUUID("UUID", entry.getKey());
++ cmp.putLong("Time", entry.getValue());
++ list.add(cmp);
++ }
++ comp.put(LOOTED_PLAYERS, list);
++ }
++
++ if (!comp.isEmpty()) {
++ base.put(ROOT, comp);
++ }
++ }
++
++ void setPlayerLootedState(final UUID player, final boolean looted) {
++ if (looted && this.lootedPlayers == null) {
++ this.lootedPlayers = new HashMap<>();
++ }
++ if (looted) {
++ this.lootedPlayers.put(player, System.currentTimeMillis());
++ } else if (this.lootedPlayers != null) {
++ this.lootedPlayers.remove(player);
++ }
++ }
++
++ boolean canPlayerLoot(final UUID player, final WorldConfiguration worldConfiguration) {
++ final @Nullable Long lastLooted = this.getLastLooted(player);
++ if (!worldConfiguration.lootables.restrictPlayerReloot || lastLooted == null) return true;
++
++ final DurationOrDisabled restrictPlayerRelootTime = worldConfiguration.lootables.restrictPlayerRelootTime;
++ if (restrictPlayerRelootTime.value().isEmpty()) return false;
++
++ return TimeUnit.SECONDS.toMillis(restrictPlayerRelootTime.value().get().seconds()) + lastLooted < System.currentTimeMillis();
++ }
++
++ boolean hasPlayerLooted(final UUID player) {
++ return this.lootedPlayers != null && this.lootedPlayers.containsKey(player);
++ }
++
++ @Nullable Long getLastLooted(final UUID player) {
++ return this.lootedPlayers != null ? this.lootedPlayers.get(player) : null;
++ }
++}
+diff --git a/src/main/java/net/minecraft/world/RandomizableContainer.java b/src/main/java/net/minecraft/world/RandomizableContainer.java
+index 9715f1b63aeea39bde9258275f51e3e8508ca6e4..084935138b1484f3d96e99f4e5655a6c04931907 100644
+--- a/src/main/java/net/minecraft/world/RandomizableContainer.java
++++ b/src/main/java/net/minecraft/world/RandomizableContainer.java
+@@ -28,7 +28,7 @@ public interface RandomizableContainer extends Container {
+
+ void setLootTable(@Nullable ResourceKey<LootTable> lootTable);
+
+- default void setLootTable(ResourceKey<LootTable> lootTableId, long lootTableSeed) {
++ default void setLootTable(@Nullable ResourceKey<LootTable> lootTableId, long lootTableSeed) { // Paper - add nullable
+ this.setLootTable(lootTableId);
+ this.setLootTableSeed(lootTableSeed);
+ }
+@@ -51,13 +51,14 @@ public interface RandomizableContainer extends Container {
+ default boolean tryLoadLootTable(CompoundTag nbt) {
+ if (nbt.contains("LootTable", 8)) {
+ this.setLootTable(ResourceKey.create(Registries.LOOT_TABLE, ResourceLocation.parse(nbt.getString("LootTable"))));
++ if (this.lootableData() != null && this.getLootTable() != null) this.lootableData().loadNbt(nbt); // Paper - LootTable API
+ if (nbt.contains("LootTableSeed", 4)) {
+ this.setLootTableSeed(nbt.getLong("LootTableSeed"));
+ } else {
+ this.setLootTableSeed(0L);
+ }
+
+- return true;
++ return this.lootableData() == null; // Paper - only track the loot table if there is chance for replenish
+ } else {
+ return false;
+ }
+@@ -69,26 +70,44 @@ public interface RandomizableContainer extends Container {
+ return false;
+ } else {
+ nbt.putString("LootTable", resourceKey.location().toString());
++ if (this.lootableData() != null) this.lootableData().saveNbt(nbt); // Paper - LootTable API
+ long l = this.getLootTableSeed();
+ if (l != 0L) {
+ nbt.putLong("LootTableSeed", l);
+ }
+
+- return true;
++ return this.lootableData() == null; // Paper - only track the loot table if there is chance for replenish
+ }
+ }
+
+ default void unpackLootTable(@Nullable Player player) {
++ // Paper start - LootTable API
++ this.unpackLootTable(player, false);
++ }
++ default void unpackLootTable(@Nullable final Player player, final boolean forceClearLootTable) {
++ // Paper end - LootTable API
+ Level level = this.getLevel();
+ BlockPos blockPos = this.getBlockPos();
+ ResourceKey<LootTable> resourceKey = this.getLootTable();
+- if (resourceKey != null && level != null && level.getServer() != null) {
++ // Paper start - LootTable API
++ lootReplenish: if (resourceKey != null && level != null && level.getServer() != null) {
++ if (this.lootableData() != null && !this.lootableData().shouldReplenish(this, com.destroystokyo.paper.loottable.PaperLootableInventoryData.CONTAINER, player)) {
++ if (forceClearLootTable) {
++ this.setLootTable(null);
++ }
++ break lootReplenish;
++ }
++ // Paper end - LootTable API
+ LootTable lootTable = level.getServer().reloadableRegistries().getLootTable(resourceKey);
+ if (player instanceof ServerPlayer) {
+ CriteriaTriggers.GENERATE_LOOT.trigger((ServerPlayer)player, resourceKey);
+ }
+
+- this.setLootTable(null);
++ // Paper start - LootTable API
++ if (forceClearLootTable || this.lootableData() == null || this.lootableData().shouldClearLootTable(this, com.destroystokyo.paper.loottable.PaperLootableInventoryData.CONTAINER, player)) {
++ this.setLootTable(null);
++ }
++ // Paper end - LootTable API
+ LootParams.Builder builder = new LootParams.Builder((ServerLevel)level).withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(blockPos));
+ if (player != null) {
+ builder.withLuck(player.getLuck()).withParameter(LootContextParams.THIS_ENTITY, player);
+@@ -97,4 +116,16 @@ public interface RandomizableContainer extends Container {
+ lootTable.fill(this, builder.create(LootContextParamSets.CHEST), this.getLootTableSeed());
+ }
+ }
++
++ // Paper start - LootTable API
++ @Nullable @org.jetbrains.annotations.Contract(pure = true)
++ default com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData() {
++ return null; // some containers don't really have a "replenish" ability like decorated pots
++ }
++
++ default com.destroystokyo.paper.loottable.PaperLootableInventory getLootableInventory() {
++ final org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(java.util.Objects.requireNonNull(this.getLevel(), "Cannot manage loot tables on block entities not in world"), this.getBlockPos());
++ return (com.destroystokyo.paper.loottable.PaperLootableInventory) block.getState(false);
++ }
++ // Paper end - LootTable API
+ }
+diff --git a/src/main/java/net/minecraft/world/entity/vehicle/AbstractChestBoat.java b/src/main/java/net/minecraft/world/entity/vehicle/AbstractChestBoat.java
+index 9c871c74ddc9983f6b4df27c7614f7224b682269..8033abfd77bcc20326b992a9d81e2faa9582fb83 100644
+--- a/src/main/java/net/minecraft/world/entity/vehicle/AbstractChestBoat.java
++++ b/src/main/java/net/minecraft/world/entity/vehicle/AbstractChestBoat.java
+@@ -181,7 +181,7 @@ public abstract class AbstractChestBoat extends AbstractBoat implements HasCusto
+ @Nullable
+ @Override
+ public AbstractContainerMenu createMenu(int syncId, Inventory playerInventory, Player player) {
+- if (this.lootTable != null && player.isSpectator()) {
++ if (this.lootTable != null && player.isSpectator()) { // Paper - LootTable API (TODO spectators can open chests that aren't ready to be re-generated but this doesn't support that)
+ return null;
+ } else {
+ this.unpackLootTable(playerInventory.player);
+@@ -229,6 +229,14 @@ public abstract class AbstractChestBoat extends AbstractBoat implements HasCusto
+ this.level().gameEvent((Holder) GameEvent.CONTAINER_CLOSE, this.position(), GameEvent.Context.of((Entity) player));
+ }
+
++ // Paper start - LootTable API
++ final com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData = new com.destroystokyo.paper.loottable.PaperLootableInventoryData();
++
++ @Override
++ public com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData() {
++ return this.lootableData;
++ }
++ // Paper end - LootTable API
+ // 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/AbstractMinecartContainer.java b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java
+index a4be7b19b626957efdf2f2507121f0085ba1da50..d528e8e4aea266c495377365f01e314001eb1970 100644
+--- a/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java
++++ b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java
+@@ -36,6 +36,14 @@ public abstract class AbstractMinecartContainer extends AbstractMinecart impleme
+ public ResourceKey<LootTable> lootTable;
+ public long lootTableSeed;
+
++ // Paper start - LootTable API
++ final com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData = new com.destroystokyo.paper.loottable.PaperLootableInventoryData();
++
++ @Override
++ public com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData() {
++ return this.lootableData;
++ }
++ // Paper end - LootTable API
+ // 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 beba927cffdeedcd68d8048708f5bf1a409ff965..874a44ab77248665c2db243764e8542bfc0d6514 100644
+--- a/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java
++++ b/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java
+@@ -62,22 +62,26 @@ public interface ContainerEntity extends Container, MenuProvider {
+ default void addChestVehicleSaveData(CompoundTag nbt, HolderLookup.Provider registries) {
+ if (this.getContainerLootTable() != null) {
+ nbt.putString("LootTable", this.getContainerLootTable().location().toString());
++ this.lootableData().saveNbt(nbt); // Paper
+ if (this.getContainerLootTableSeed() != 0L) {
+ nbt.putLong("LootTableSeed", this.getContainerLootTableSeed());
+ }
+- } else {
+- ContainerHelper.saveAllItems(nbt, this.getItemStacks(), registries);
+ }
++ ContainerHelper.saveAllItems(nbt, this.getItemStacks(), registries); // Paper - always save the items, table may still remain
+ }
+
+ default void readChestVehicleSaveData(CompoundTag nbt, HolderLookup.Provider registries) {
+ this.clearItemStacks();
+ if (nbt.contains("LootTable", 8)) {
+ this.setContainerLootTable(ResourceKey.create(Registries.LOOT_TABLE, ResourceLocation.parse(nbt.getString("LootTable"))));
++ // Paper start - LootTable API
++ if (this.getContainerLootTable() != null) {
++ this.lootableData().loadNbt(nbt);
++ }
++ // Paper end - LootTable API
+ this.setContainerLootTableSeed(nbt.getLong("LootTableSeed"));
+- } else {
+- ContainerHelper.loadAllItems(nbt, this.getItemStacks(), registries);
+ }
++ ContainerHelper.loadAllItems(nbt, this.getItemStacks(), registries); // Paper - always save the items, table may still remain
+ }
+
+ default void chestVehicleDestroyed(DamageSource source, ServerLevel world, Entity vehicle) {
+@@ -97,13 +101,18 @@ public interface ContainerEntity extends Container, MenuProvider {
+
+ default void unpackChestVehicleLootTable(@Nullable Player player) {
+ MinecraftServer minecraftServer = this.level().getServer();
+- if (this.getContainerLootTable() != null && minecraftServer != null) {
++ if (minecraftServer != null && this.lootableData().shouldReplenish(this, com.destroystokyo.paper.loottable.PaperLootableInventoryData.ENTITY, player)) { // Paper - LootTable API
+ LootTable lootTable = minecraftServer.reloadableRegistries().getLootTable(this.getContainerLootTable());
+ if (player != null) {
+ CriteriaTriggers.GENERATE_LOOT.trigger((ServerPlayer)player, this.getContainerLootTable());
+ }
+
+- this.setContainerLootTable(null);
++ // Paper start - LootTable API
++ if (this.lootableData().shouldClearLootTable(this, com.destroystokyo.paper.loottable.PaperLootableInventoryData.ENTITY, player)) {
++ this.setContainerLootTable(null);
++ }
++ // Paper end - LootTable API
++
+ LootParams.Builder builder = new LootParams.Builder((ServerLevel)this.level()).withParameter(LootContextParams.ORIGIN, this.position());
+ if (player != null) {
+ builder.withLuck(player.getLuck()).withParameter(LootContextParams.THIS_ENTITY, player);
+@@ -173,4 +182,14 @@ public interface ContainerEntity extends Container, MenuProvider {
+ default boolean isChestVehicleStillValid(Player player) {
+ return !this.isRemoved() && player.canInteractWithEntity(this.getBoundingBox(), 4.0);
+ }
++
++ // Paper start - LootTable API
++ default com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData() {
++ throw new UnsupportedOperationException("Implement this method");
++ }
++
++ default com.destroystokyo.paper.loottable.PaperLootableInventory getLootableInventory() {
++ return ((com.destroystokyo.paper.loottable.PaperLootableInventory) ((net.minecraft.world.entity.Entity) this).getBukkitEntity());
++ }
++ // Paper end - LootTable API
+ }
+diff --git a/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java b/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java
+index 63e41d3ed8844d6d41ff57b85779e190e57dc889..0712818e2d9205078bfc8846452ba31388840034 100644
+--- a/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java
++++ b/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java
+@@ -137,7 +137,7 @@ public class ShulkerBoxBlock extends BaseEntityBlock {
+ itemEntity.setDefaultPickUpDelay();
+ world.addFreshEntity(itemEntity);
+ } else {
+- shulkerBoxBlockEntity.unpackLootTable(player);
++ shulkerBoxBlockEntity.unpackLootTable(player, true); // Paper - force clear loot table so replenish data isn't persisted in the stack
+ }
+ }
+
+@@ -147,7 +147,15 @@ public class ShulkerBoxBlock extends BaseEntityBlock {
+ @Override
+ protected List<ItemStack> getDrops(BlockState state, LootParams.Builder builder) {
+ BlockEntity blockEntity = builder.getOptionalParameter(LootContextParams.BLOCK_ENTITY);
++ Runnable reAdd = null; // Paper
+ if (blockEntity instanceof ShulkerBoxBlockEntity shulkerBoxBlockEntity) {
++ // Paper start - clear loot table if it was already used
++ if (shulkerBoxBlockEntity.lootableData().getLastFill() != -1 || !builder.getLevel().paperConfig().lootables.retainUnlootedShulkerBoxLootTableOnNonPlayerBreak) {
++ net.minecraft.resources.ResourceKey<net.minecraft.world.level.storage.loot.LootTable> lootTableResourceKey = shulkerBoxBlockEntity.getLootTable();
++ reAdd = () -> shulkerBoxBlockEntity.setLootTable(lootTableResourceKey);
++ shulkerBoxBlockEntity.setLootTable(null);
++ }
++ // Paper end
+ builder = builder.withDynamicDrop(CONTENTS, lootConsumer -> {
+ for (int i = 0; i < shulkerBoxBlockEntity.getContainerSize(); i++) {
+ lootConsumer.accept(shulkerBoxBlockEntity.getItem(i));
+@@ -155,7 +163,13 @@ public class ShulkerBoxBlock extends BaseEntityBlock {
+ });
+ }
+
++ // Paper start - re-set loot table if it was cleared
++ try {
+ return super.getDrops(state, builder);
++ } finally {
++ if (reAdd != null) reAdd.run();
++ }
++ // Paper end - re-set loot table if it was cleared
+ }
+
+ @Override
+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 74c833e589160f0fe31f3b5e515f3515201159bd..fc657b6052d4310ad9c28988042c2cf37cf5d213 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
+@@ -115,4 +115,13 @@ public abstract class RandomizableContainerBlockEntity extends BaseContainerBloc
+ nbt.remove("LootTable");
+ nbt.remove("LootTableSeed");
+ }
++
++ // Paper start - LootTable API
++ final com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData = new com.destroystokyo.paper.loottable.PaperLootableInventoryData(); // Paper
++
++ @Override
++ public com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData() {
++ return this.lootableData;
++ }
++ // Paper end - LootTable API
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBrushableBlock.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBrushableBlock.java
+index 949e074a32b6593bd8b7405499e686a074e283e5..1f084b73f2ec67dd2022feafc5ab5dac02c338f6 100644
+--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBrushableBlock.java
++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBrushableBlock.java
+@@ -58,7 +58,8 @@ public class CraftBrushableBlock extends CraftBlockEntityState<BrushableBlockEnt
+ this.setLootTable(this.getLootTable(), seed);
+ }
+
+- private void setLootTable(LootTable table, long seed) {
++ @Override // Paper - this is now an override
++ public void setLootTable(LootTable table, long seed) { // Paper - make public since it overrides a public method
+ this.getSnapshot().setLootTable(CraftLootTable.bukkitToMinecraft(table), seed);
+ }
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftLootable.java b/src/main/java/org/bukkit/craftbukkit/block/CraftLootable.java
+index 74315a46f6101775321b1cf4944c124c69aed182..f23fbb8ed39a754b36d2eb162358877ef6dacb17 100644
+--- a/src/main/java/org/bukkit/craftbukkit/block/CraftLootable.java
++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftLootable.java
+@@ -8,7 +8,7 @@ import org.bukkit.craftbukkit.CraftLootTable;
+ 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);
+@@ -27,29 +27,17 @@ public abstract class CraftLootable<T extends RandomizableContainerBlockEntity>
+ }
+ }
+
++ // Paper start - move to PaperLootableBlockInventory
+ @Override
+- public LootTable getLootTable() {
+- return CraftLootTable.minecraftToBukkit(this.getSnapshot().lootTable);
++ public net.minecraft.world.level.Level getNMSWorld() {
++ return ((org.bukkit.craftbukkit.CraftWorld) this.getWorld()).getHandle();
+ }
+
+ @Override
+- public void setLootTable(LootTable table) {
+- this.setLootTable(table, this.getSeed());
+- }
+-
+- @Override
+- public long getSeed() {
+- return this.getSnapshot().lootTableSeed;
+- }
+-
+- @Override
+- public void setSeed(long seed) {
+- this.setLootTable(this.getLootTable(), seed);
+- }
+-
+- public void setLootTable(LootTable table, long seed) {
+- this.getSnapshot().setLootTable(CraftLootTable.bukkitToMinecraft(table), seed);
++ public net.minecraft.world.RandomizableContainer getRandomizableContainer() {
++ return this.getSnapshot();
+ }
++ // Paper end - move to PaperLootableBlockInventory
+
+ @Override
+ public abstract CraftLootable<T> copy();
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java
+index 62accb551344c41671fc22b15d7b25b6fc97d915..a1e04bb965f18ffd07e2f5bf827c5e4ddd6aeeda 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java
+@@ -7,8 +7,7 @@ import org.bukkit.craftbukkit.inventory.CraftInventory;
+ import org.bukkit.inventory.Inventory;
+ import org.bukkit.loot.LootTable;
+
+-public abstract class CraftChestBoat extends CraftBoat implements org.bukkit.entity.ChestBoat {
+-
++public abstract class CraftChestBoat extends CraftBoat implements org.bukkit.entity.ChestBoat, com.destroystokyo.paper.loottable.PaperLootableEntityInventory { // Paper
+ private final Inventory inventory;
+
+ public CraftChestBoat(CraftServer server, AbstractChestBoat entity) {
+@@ -31,28 +30,6 @@ public abstract class CraftChestBoat extends CraftBoat implements org.bukkit.ent
+ return this.inventory;
+ }
+
+- @Override
+- public void setLootTable(LootTable table) {
+- this.setLootTable(table, this.getSeed());
+- }
++ // Paper - moved loot table logic to PaperLootableEntityInventory
+
+- @Override
+- public LootTable getLootTable() {
+- return CraftLootTable.minecraftToBukkit(this.getHandle().getContainerLootTable());
+- }
+-
+- @Override
+- public void setSeed(long seed) {
+- this.setLootTable(this.getLootTable(), seed);
+- }
+-
+- @Override
+- public long getSeed() {
+- return this.getHandle().getContainerLootTableSeed();
+- }
+-
+- private void setLootTable(LootTable table, long seed) {
+- this.getHandle().setContainerLootTable(CraftLootTable.bukkitToMinecraft(table));
+- this.getHandle().setContainerLootTableSeed(seed);
+- }
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java
+index fd42f0b20132d08039ca7735d31a61806a6b07dc..b1a708de6790bbe336202b13ab862ced78de084f 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java
+@@ -7,7 +7,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/CraftMinecartContainer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartContainer.java
+index 4388cd0303b45faf21631e7644baebb63baaba10..451f3a6f0b47493da3af3f5d6baced6a8c97f350 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartContainer.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartContainer.java
+@@ -7,7 +7,7 @@ import org.bukkit.craftbukkit.CraftServer;
+ import org.bukkit.loot.LootTable;
+ import org.bukkit.loot.Lootable;
+
+-public abstract class CraftMinecartContainer extends CraftMinecart implements Lootable {
++public abstract class CraftMinecartContainer extends CraftMinecart implements com.destroystokyo.paper.loottable.PaperLootableEntityInventory { // Paper
+
+ public CraftMinecartContainer(CraftServer server, AbstractMinecart entity) {
+ super(server, entity);
+@@ -18,27 +18,5 @@ public abstract class CraftMinecartContainer extends CraftMinecart implements Lo
+ return (AbstractMinecartContainer) this.entity;
+ }
+
+- @Override
+- public void setLootTable(LootTable table) {
+- this.setLootTable(table, this.getSeed());
+- }
+-
+- @Override
+- public LootTable getLootTable() {
+- return CraftLootTable.minecraftToBukkit(this.getHandle().lootTable);
+- }
+-
+- @Override
+- public void setSeed(long seed) {
+- this.setLootTable(this.getLootTable(), seed);
+- }
+-
+- @Override
+- public long getSeed() {
+- return this.getHandle().lootTableSeed;
+- }
+-
+- public void setLootTable(LootTable table, long seed) {
+- this.getHandle().setLootTable(CraftLootTable.bukkitToMinecraft(table), seed);
+- }
++ // Paper - moved loot table logic to PaperLootableEntityInventory
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java
+index 39427b4f284e9402663be2b160ccb5f03f8b91da..17f5684cba9d3ed22d9925d1951520cc4751dfe2 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java
+@@ -6,7 +6,7 @@ import org.bukkit.craftbukkit.inventory.CraftInventory;
+ 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) {