From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Aikar 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..de528b8bafd75b6f14b1384157f3a8a27e06b4a2 --- /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().getLootTable()); + } + + @Override + default void setLootTable(final @Nullable LootTable table, final long seed) { + this.getHandle().setLootTable(CraftLootTable.bukkitToMinecraft(table)); + this.getHandle().setLootTableSeed(seed); + } + + @Override + default long getSeed() { + return this.getHandle().getLootTableSeed(); + } +} 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..cd61276a45894a02cbefc41a63c27e2cf6361d1e --- /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 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.setLootTableSeed(seed); + } + + @Override + public boolean hasLootTable(final ContainerEntity holder) { + return holder.getLootTable() != 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/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); - 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 @@ 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 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/AbstractMinecartContainer.java b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java index 67840327e934b631a85cf2d64911f5cfab4402b1..9549eee0d92f322bd5232abd7e695213660c2e22 100644 --- a/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java +++ b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java @@ -35,6 +35,14 @@ public abstract class AbstractMinecartContainer extends AbstractMinecart impleme 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; diff --git a/src/main/java/net/minecraft/world/entity/vehicle/ChestBoat.java b/src/main/java/net/minecraft/world/entity/vehicle/ChestBoat.java index 42f8e2d961f83c3e9ce384158b9dfc4014eb0a5f..4cdf3b54187ebcb1f5ddfa6114386127a2846f01 100644 --- a/src/main/java/net/minecraft/world/entity/vehicle/ChestBoat.java +++ b/src/main/java/net/minecraft/world/entity/vehicle/ChestBoat.java @@ -217,7 +217,7 @@ public class ChestBoat extends Boat implements HasCustomInventoryScreen, Contain @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); @@ -265,6 +265,14 @@ public class ChestBoat extends Boat implements HasCustomInventoryScreen, Contain 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/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java b/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java index 3ee99193de5deb6a38d6ded561fe8f2fbf711327..ccc7367ab2740bea0f2b907223a0920b11665092 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 registriesLookup) { if (this.getLootTable() != null) { nbt.putString("LootTable", this.getLootTable().location().toString()); + this.lootableData().saveNbt(nbt); // Paper if (this.getLootTableSeed() != 0L) { nbt.putLong("LootTableSeed", this.getLootTableSeed()); } - } else { - ContainerHelper.saveAllItems(nbt, this.getItemStacks(), registriesLookup); } + ContainerHelper.saveAllItems(nbt, this.getItemStacks(), registriesLookup); // Paper - always save the items, table may still remain } default void readChestVehicleSaveData(CompoundTag nbt, HolderLookup.Provider registriesLookup) { this.clearItemStacks(); if (nbt.contains("LootTable", 8)) { this.setLootTable(ResourceKey.create(Registries.LOOT_TABLE, ResourceLocation.parse(nbt.getString("LootTable")))); + // Paper start - LootTable API + if (this.getLootTable() != null) { + this.lootableData().loadNbt(nbt); + } + // Paper end - LootTable API this.setLootTableSeed(nbt.getLong("LootTableSeed")); - } else { - ContainerHelper.loadAllItems(nbt, this.getItemStacks(), registriesLookup); } + ContainerHelper.loadAllItems(nbt, this.getItemStacks(), registriesLookup); // Paper - always save the items, table may still remain } default void chestVehicleDestroyed(DamageSource source, Level world, Entity vehicle) { @@ -99,13 +103,17 @@ public interface ContainerEntity extends Container, MenuProvider { default void unpackChestVehicleLootTable(@Nullable Player player) { MinecraftServer minecraftServer = this.level().getServer(); - if (this.getLootTable() != 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.getLootTable()); if (player != null) { CriteriaTriggers.GENERATE_LOOT.trigger((ServerPlayer)player, this.getLootTable()); } - this.setLootTable(null); + // Paper start - LootTable API + if (this.lootableData().shouldClearLootTable(this, com.destroystokyo.paper.loottable.PaperLootableInventoryData.ENTITY, player)) { + this.setLootTable(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); @@ -175,4 +183,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 d85da0661096a3587917c6636728bfd2e3eb90a2..6323c96d9b0cd14f89609b38da37d7fcc12d211b 100644 --- a/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java +++ b/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java @@ -148,7 +148,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 } } @@ -158,7 +158,15 @@ public class ShulkerBoxBlock extends BaseEntityBlock { @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)); @@ -166,7 +174,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 c2493c15d8fe4587d6ee2db100cc13303b66b39b..13c9a68b604d4c7c6e09e72b3cea7ab2214b06ab 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 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/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java index cfde210ea9d4b62fe514d3ab0dbab2f43eda0c7a..e4f899a6a1d055b3ea17d1114ed0228fbba53352 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 class CraftChestBoat extends CraftBoat implements org.bukkit.entity.ChestBoat { - +public class CraftChestBoat extends CraftBoat implements org.bukkit.entity.ChestBoat, com.destroystokyo.paper.loottable.PaperLootableEntityInventory { // Paper private final Inventory inventory; public CraftChestBoat(CraftServer server, ChestBoat entity) { @@ -31,28 +30,5 @@ public class CraftChestBoat extends CraftBoat implements org.bukkit.entity.Chest return this.inventory; } - @Override - public void setLootTable(LootTable table) { - this.setLootTable(table, this.getSeed()); - } - - @Override - public LootTable getLootTable() { - return CraftLootTable.minecraftToBukkit(this.getHandle().getLootTable()); - } - - @Override - public void setSeed(long seed) { - this.setLootTable(this.getLootTable(), seed); - } - - @Override - public long getSeed() { - return this.getHandle().getLootTableSeed(); - } - - private void setLootTable(LootTable table, long seed) { - this.getHandle().setLootTable(CraftLootTable.bukkitToMinecraft(table)); - this.getHandle().setLootTableSeed(seed); - } + // Paper - moved loot table logic to PaperLootableEntityInventory } 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) {