diff --git a/paper-server/patches/sources/net/minecraft/world/RandomizableContainer.java.patch b/paper-server/patches/sources/net/minecraft/world/RandomizableContainer.java.patch new file mode 100644 index 000000000..f5c11bafc --- /dev/null +++ b/paper-server/patches/sources/net/minecraft/world/RandomizableContainer.java.patch @@ -0,0 +1,92 @@ +--- a/net/minecraft/world/RandomizableContainer.java ++++ b/net/minecraft/world/RandomizableContainer.java +@@ -28,7 +28,7 @@ + + void setLootTable(@Nullable ResourceKey lootTable); + +- default void setLootTable(ResourceKey lootTableId, long lootTableSeed) { ++ default void setLootTable(@Nullable ResourceKey lootTableId, long lootTableSeed) { // Paper - add nullable + this.setLootTable(lootTableId); + this.setLootTableSeed(lootTableSeed); + } +@@ -51,13 +51,14 @@ + 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 @@ + 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 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 @@ + 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/paper-server/patches/sources/net/minecraft/world/entity/vehicle/AbstractChestBoat.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/AbstractChestBoat.java.patch index 8547e8355..58fcd4f2f 100644 --- a/paper-server/patches/sources/net/minecraft/world/entity/vehicle/AbstractChestBoat.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/AbstractChestBoat.java.patch @@ -37,11 +37,28 @@ } @Override -@@ -212,4 +228,51 @@ +@@ -165,7 +181,7 @@ + @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); +@@ -212,4 +228,59 @@ public void stopOpen(Player player) { 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 transaction = new java.util.ArrayList(); + private int maxStack = MAX_STACK; diff --git a/paper-server/patches/sources/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java.patch index 3a4e5c217..b33136b34 100644 --- a/paper-server/patches/sources/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java.patch @@ -15,10 +15,18 @@ public abstract class AbstractMinecartContainer extends AbstractMinecart implements ContainerEntity { -@@ -28,9 +36,50 @@ +@@ -28,9 +36,58 @@ public ResourceKey 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 transaction = new java.util.ArrayList(); + private int maxStack = MAX_STACK; @@ -67,7 +75,7 @@ } @Override -@@ -74,11 +123,18 @@ +@@ -74,11 +131,18 @@ @Override public void remove(Entity.RemovalReason reason) { diff --git a/paper-server/patches/sources/net/minecraft/world/entity/vehicle/ContainerEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/ContainerEntity.java.patch new file mode 100644 index 000000000..0e502d4f1 --- /dev/null +++ b/paper-server/patches/sources/net/minecraft/world/entity/vehicle/ContainerEntity.java.patch @@ -0,0 +1,69 @@ +--- a/net/minecraft/world/entity/vehicle/ContainerEntity.java ++++ b/net/minecraft/world/entity/vehicle/ContainerEntity.java +@@ -62,22 +62,26 @@ + 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 @@ + + 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 @@ + 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/paper-server/patches/sources/net/minecraft/world/level/block/ShulkerBoxBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/ShulkerBoxBlock.java.patch new file mode 100644 index 000000000..a1554d181 --- /dev/null +++ b/paper-server/patches/sources/net/minecraft/world/level/block/ShulkerBoxBlock.java.patch @@ -0,0 +1,41 @@ +--- a/net/minecraft/world/level/block/ShulkerBoxBlock.java ++++ b/net/minecraft/world/level/block/ShulkerBoxBlock.java +@@ -137,7 +137,7 @@ + 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 @@ + @Override + protected List 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 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 @@ + }); + } + ++ // 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/paper-server/patches/sources/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java.patch new file mode 100644 index 000000000..16ff81b3c --- /dev/null +++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java.patch @@ -0,0 +1,16 @@ +--- a/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java +@@ -115,4 +115,13 @@ + 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/paper-server/src/main/java/com/destroystokyo/paper/loottable/PaperLootable.java b/paper-server/src/main/java/com/destroystokyo/paper/loottable/PaperLootable.java new file mode 100644 index 000000000..a53d51be1 --- /dev/null +++ b/paper-server/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/paper-server/src/main/java/com/destroystokyo/paper/loottable/PaperLootableBlock.java b/paper-server/src/main/java/com/destroystokyo/paper/loottable/PaperLootableBlock.java new file mode 100644 index 000000000..9e9ea1323 --- /dev/null +++ b/paper-server/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/paper-server/src/main/java/com/destroystokyo/paper/loottable/PaperLootableBlockInventory.java b/paper-server/src/main/java/com/destroystokyo/paper/loottable/PaperLootableBlockInventory.java new file mode 100644 index 000000000..0699c6092 --- /dev/null +++ b/paper-server/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/paper-server/src/main/java/com/destroystokyo/paper/loottable/PaperLootableEntity.java b/paper-server/src/main/java/com/destroystokyo/paper/loottable/PaperLootableEntity.java new file mode 100644 index 000000000..d93305453 --- /dev/null +++ b/paper-server/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/paper-server/src/main/java/com/destroystokyo/paper/loottable/PaperLootableEntityInventory.java b/paper-server/src/main/java/com/destroystokyo/paper/loottable/PaperLootableEntityInventory.java new file mode 100644 index 000000000..5c57acc95 --- /dev/null +++ b/paper-server/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/paper-server/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventory.java b/paper-server/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventory.java new file mode 100644 index 000000000..9e7c22ef4 --- /dev/null +++ b/paper-server/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/paper-server/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventoryData.java b/paper-server/src/main/java/com/destroystokyo/paper/loottable/PaperLootableInventoryData.java new file mode 100644 index 000000000..861bff267 --- /dev/null +++ b/paper-server/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 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 boolean shouldReplenish(final T lootTableHolder, final LootTableInterface 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 { + + WorldConfiguration paperConfig(T holder); + + void setSeed(T holder, long seed); + + boolean hasLootTable(T holder); + + LootableInventory getInventoryForEvent(T holder); + } + + public static final LootTableInterface 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 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 boolean shouldClearLootTable(final T lootTableHolder, final LootTableInterface 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 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/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftBrushableBlock.java b/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftBrushableBlock.java index 949e074a3..1f084b73f 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftBrushableBlock.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftBrushableBlock.java @@ -58,7 +58,8 @@ public class CraftBrushableBlock extends CraftBlockEntityState extends CraftContainer implements Nameable, Lootable { +public abstract class CraftLootable extends CraftContainer 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 } } + // 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 copy(); diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java index 62accb551..a1e04bb96 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java +++ b/paper-server/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/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java index fd42f0b20..b1a708de6 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartChest.java +++ b/paper-server/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/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartContainer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartContainer.java index 4388cd030..451f3a6f0 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartContainer.java +++ b/paper-server/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/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java index 39427b4f2..17f5684cb 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java +++ b/paper-server/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) {