diff --git a/build-data/paper.at b/build-data/paper.at index cf91314d93c0..8e27f71577e5 100644 --- a/build-data/paper.at +++ b/build-data/paper.at @@ -628,6 +628,11 @@ public net.minecraft.world.level.block.entity.trialspawner.TrialSpawner stateAcc public net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerData currentMobs public net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerData detectedPlayers public net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerData nextSpawnData +public net.minecraft.world.level.block.entity.vault.VaultBlockEntity serverData +public net.minecraft.world.level.block.entity.vault.VaultServerData getRewardedPlayers()Ljava/util/Set; +public net.minecraft.world.level.block.entity.vault.VaultServerData pauseStateUpdatingUntil(J)V +public net.minecraft.world.level.block.entity.vault.VaultServerData stateUpdatingResumesAt()J +public net.minecraft.world.level.block.entity.vault.VaultSharedData getConnectedPlayers()Ljava/util/Set; public net.minecraft.world.level.block.state.BlockBehaviour getMenuProvider(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/MenuProvider; public net.minecraft.world.level.block.state.BlockBehaviour hasCollision public net.minecraft.world.level.block.state.BlockBehaviour$BlockStateBase destroySpeed diff --git a/paper-api/src/main/java/org/bukkit/block/Vault.java b/paper-api/src/main/java/org/bukkit/block/Vault.java index 64c7b432c372..6963f296933e 100644 --- a/paper-api/src/main/java/org/bukkit/block/Vault.java +++ b/paper-api/src/main/java/org/bukkit/block/Vault.java @@ -1,7 +1,184 @@ package org.bukkit.block; +import org.bukkit.World; +import org.bukkit.inventory.ItemStack; +import org.bukkit.loot.LootTable; +import org.jetbrains.annotations.Unmodifiable; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import java.util.Collection; +import java.util.Set; +import java.util.UUID; + /** - * Represents a captured state of a trial spawner. + * Represents a captured state of a vault. */ +@NullMarked public interface Vault extends TileState { + /** + * Gets the range in blocks at which this vault will become active when a player is near. + * + * @return This vault's activation range. + */ + double getActivationRange(); + + /** + * Sets the range in blocks at which the vault will become active when a player is near. + * + * @param activationRange The new activation range. + * @throws IllegalArgumentException if the new range is not a number, or if the new range is more than {@link #getDeactivationRange()}. + */ + void setActivationRange(double activationRange); + + /** + * Gets the range in blocks at which this vault will become inactive when a player is not near. + * + * @return This vault's deactivation range. + */ + double getDeactivationRange(); + + /** + * Sets the range in blocks at which this vault will become inactive when a player is not near. + * + * @param deactivationRange The new deactivation range + * @throws IllegalArgumentException if the new range is not a number, or if the new range is less than {@link #getActivationRange()}. + */ + void setDeactivationRange(double deactivationRange); + + /** + * Gets the {@link ItemStack} that players must use to unlock this vault. + * + * @return The item that players must use to unlock this vault. + */ + ItemStack getKeyItem(); + + /** + * Sets the {@link ItemStack} that players must use to unlock this vault. + * + * @param key The key item. + */ + void setKeyItem(ItemStack key); + + /** + * Gets the {@link LootTable} that this vault will select rewards from. + * + * @return The loot table. + */ + LootTable getLootTable(); + + /** + * Sets the {@link LootTable} that this vault will select rewards from. + * + * @param lootTable The new loot table. + */ + void setLootTable(LootTable lootTable); + + /** + * Gets the loot table that this vault will display items from. + *

+ * Falls back to the regular {@link #getLootTable() loot table} if unset. + * + * @return The {@link LootTable} that will be used to display items. + */ + @Nullable + LootTable getDisplayedLootTable(); + + /** + * Sets the loot table that this vault will display items from. + * + * @param lootTable The new loot table to display, or {@code null} to clear this display override. + */ + void setDisplayedLootTable(@Nullable LootTable lootTable); + + /** + * Gets the next time (in {@link World#getGameTime() game time}) that this vault block will be updated/ticked at. + * + * @return The next time that this vault block will be updated/ticked at. + * @see World#getGameTime() + */ + long getNextStateUpdateTime(); + + /** + * Sets the next time that this vault block will be updated/ticked at, if this value is less than or equals to the current + * {@link World#getGameTime()}, then it will be updated in the first possible tick. + * + * @param nextStateUpdateTime The next time that this vault block will be updated/ticked at. + * @see World#getGameTime() + */ + void setNextStateUpdateTime(long nextStateUpdateTime); + + /** + * Gets the players who have used a key on this vault and unlocked it. + * + * @return An unmodifiable collection of player uuids. + * + * @apiNote Only the most recent 128 player UUIDs will be stored by vault blocks. + */ + @Unmodifiable + Collection getRewardedPlayers(); + + /** + * Adds a player as rewarded for this vault. + * + * @param playerUUID The player's uuid. + * @return {@code true} if this player was previously not rewarded, and has been added as a result of this operation. + * + * @apiNote Only the most recent 128 player UUIDs will be stored by vault blocks. Attempting to add more will result in + * the first player UUID being removed. + */ + boolean addRewardedPlayer(UUID playerUUID); + + /** + * Removes a player as rewarded for this vault, allowing them to use a {@link #getKeyItem() key item} again to receive rewards. + * + * @param playerUUID The player's uuid. + * @return {@code true} if this player was previously rewarded, and has been removed as a result of this operation. + * + * @apiNote Only the most recent 128 player UUIDs will be stored by vault blocks. + */ + boolean removeRewardedPlayer(UUID playerUUID); + + /** + * Returns whether a given player has already been rewarded by this vault. + * + * @param playerUUID The player's uuid. + * @return Whether this player was previously rewarded by this vault. + */ + boolean hasRewardedPlayer(UUID playerUUID); + + /** + * Gets an unmodifiable set of "connected players"; players who are inside this vault's activation range and who have not received rewards yet. + * + * @apiNote Vaults will only periodically scan for nearby players, so it may take until the next {@link #getNextStateUpdateTime() update time} for this + * collection to be updated upon a player entering its range. + * + * @return An unmodifiable set of connected player uuids. + */ + @Unmodifiable + Set getConnectedPlayers(); + + /** + * Returns whether a given player is currently connected to this vault. + * + * @param playerUUID the player's uuid + * @return {@code true} if this player is currently connected to this vault. + * + * @see #getConnectedPlayers() + */ + boolean hasConnectedPlayer(UUID playerUUID); + + /** + * Gets the item currently being displayed inside this vault. Displayed items will automatically cycle between random items from the {@link #getDisplayedLootTable()} + * or {@link #getLootTable()} loot tables while this vault is active. + * + * @return The item currently being displayed inside this vault. + */ + ItemStack getDisplayedItem(); + + /** + * Sets the item to display inside this vault until the next cycle. + * + * @param displayedItem The item to display + */ + void setDisplayedItem(ItemStack displayedItem); } diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/vault/VaultServerData.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/vault/VaultServerData.java.patch new file mode 100644 index 000000000000..4447be70e6f1 --- /dev/null +++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/vault/VaultServerData.java.patch @@ -0,0 +1,40 @@ +--- a/net/minecraft/world/level/block/entity/vault/VaultServerData.java ++++ b/net/minecraft/world/level/block/entity/vault/VaultServerData.java +@@ -66,7 +_,12 @@ + + @VisibleForTesting + public void addToRewardedPlayers(Player player) { +- this.rewardedPlayers.add(player.getUUID()); ++ // Paper start - Vault API ++ addToRewardedPlayers(player.getUUID()); ++ } ++ public boolean addToRewardedPlayers(final java.util.UUID player) { ++ final boolean removed = this.rewardedPlayers.add(player); ++ // Paper end - Vault API + if (this.rewardedPlayers.size() > 128) { + Iterator iterator = this.rewardedPlayers.iterator(); + if (iterator.hasNext()) { +@@ -76,6 +_,7 @@ + } + + this.markChanged(); ++ return removed; // Paper - Vault API + } + + public long stateUpdatingResumesAt() { +@@ -131,4 +_,15 @@ + public float ejectionProgress() { + return this.totalEjectionsNeeded == 1 ? 1.0F : 1.0F - Mth.inverseLerp((float)this.getItemsToEject().size(), 1.0F, (float)this.totalEjectionsNeeded); + } ++ ++ // Paper start - Vault API ++ public boolean removeFromRewardedPlayers(final UUID uuid) { ++ if (this.rewardedPlayers.remove(uuid)) { ++ this.markChanged(); ++ return true; ++ } ++ ++ return false; ++ } ++ // Paper end - Vault API + } diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftVault.java b/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftVault.java index bfee498287c8..a8e455ac982b 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftVault.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftVault.java @@ -1,17 +1,33 @@ package org.bukkit.craftbukkit.block; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableSet; +import net.minecraft.resources.ResourceKey; import net.minecraft.world.level.block.entity.vault.VaultBlockEntity; +import net.minecraft.world.level.block.entity.vault.VaultConfig; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.block.Vault; +import org.bukkit.craftbukkit.CraftLootTable; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.inventory.ItemStack; +import org.bukkit.loot.LootTable; +import org.jetbrains.annotations.Unmodifiable; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +@NullMarked public class CraftVault extends CraftBlockEntityState implements Vault { public CraftVault(World world, VaultBlockEntity tileEntity) { super(world, tileEntity); } - protected CraftVault(CraftVault state, Location location) { + protected CraftVault(CraftVault state, @Nullable Location location) { super(state, location); } @@ -24,4 +40,124 @@ public CraftVault copy() { public CraftVault copy(Location location) { return new CraftVault(this, location); } + + @Override + public double getActivationRange() { + return this.getSnapshot().getConfig().activationRange(); + } + + @Override + public void setActivationRange(final double activationRange) { + Preconditions.checkArgument(Double.isFinite(activationRange), "activation range must not be NaN or infinite"); + Preconditions.checkArgument(activationRange <= this.getDeactivationRange(), "New activation range (%s) must be less or equal to deactivation range (%s)", activationRange, this.getDeactivationRange()); + + final VaultConfig config = this.getSnapshot().getConfig(); + this.getSnapshot().setConfig(new VaultConfig(config.lootTable(), activationRange, config.deactivationRange(), config.keyItem(), config.overrideLootTableToDisplay())); + } + + @Override + public double getDeactivationRange() { + return this.getSnapshot().getConfig().deactivationRange(); + } + + @Override + public void setDeactivationRange(final double deactivationRange) { + Preconditions.checkArgument(Double.isFinite(deactivationRange), "deactivation range must not be NaN or infinite"); + Preconditions.checkArgument(deactivationRange >= this.getActivationRange(), "New deactivation range (%s) must be more or equal to activation range (%s)", deactivationRange, this.getActivationRange()); + + final VaultConfig config = this.getSnapshot().getConfig(); + this.getSnapshot().setConfig(new VaultConfig(config.lootTable(), config.activationRange(), deactivationRange, config.keyItem(), config.overrideLootTableToDisplay())); + } + + @Override + public ItemStack getKeyItem() { + return this.getSnapshot().getConfig().keyItem().asBukkitCopy(); + } + + @Override + public void setKeyItem(final ItemStack key) { + Preconditions.checkArgument(key != null, "key must not be null"); + + final VaultConfig config = this.getSnapshot().getConfig(); + this.getSnapshot().setConfig(new VaultConfig(config.lootTable(), config.activationRange(), config.deactivationRange(), CraftItemStack.asNMSCopy(key), config.overrideLootTableToDisplay())); + } + + @Override + public LootTable getLootTable() { + return CraftLootTable.minecraftToBukkit(this.getSnapshot().getConfig().lootTable()); + } + + @Override + public void setLootTable(final LootTable lootTable) { + final ResourceKey lootTableKey = CraftLootTable.bukkitToMinecraft(lootTable); + Preconditions.checkArgument(lootTableKey != null, "lootTable must not be null"); + + final VaultConfig config = this.getSnapshot().getConfig(); + this.getSnapshot().setConfig(new VaultConfig(lootTableKey, config.activationRange(), config.deactivationRange(), config.keyItem(), config.overrideLootTableToDisplay())); + } + + @Override + public @Nullable LootTable getDisplayedLootTable() { + return this.getSnapshot().getConfig().overrideLootTableToDisplay().map(CraftLootTable::minecraftToBukkit).orElse(null); + } + + @Override + public void setDisplayedLootTable(final @Nullable LootTable lootTable) { + final VaultConfig config = this.getSnapshot().getConfig(); + + this.getSnapshot().setConfig(new VaultConfig(config.lootTable(), config.activationRange(), config.deactivationRange(), config.keyItem(), Optional.ofNullable(CraftLootTable.bukkitToMinecraft(lootTable)))); + } + + @Override + public long getNextStateUpdateTime() { + return this.getSnapshot().serverData.stateUpdatingResumesAt(); + } + + @Override + public void setNextStateUpdateTime(final long nextStateUpdateTime) { + this.getSnapshot().serverData.pauseStateUpdatingUntil(nextStateUpdateTime); + } + + @Override + public @Unmodifiable Collection getRewardedPlayers() { + return ImmutableSet.copyOf(this.getSnapshot().serverData.getRewardedPlayers()); + } + + @Override + public boolean addRewardedPlayer(final UUID playerUUID) { + Preconditions.checkArgument(playerUUID != null, "playerUUID must not be null"); + return this.getSnapshot().serverData.addToRewardedPlayers(playerUUID); + } + + @Override + public boolean removeRewardedPlayer(final UUID playerUUID) { + Preconditions.checkArgument(playerUUID != null, "playerUUID must not be null"); + return this.getSnapshot().serverData.removeFromRewardedPlayers(playerUUID); + } + + @Override + public boolean hasRewardedPlayer(final UUID playerUUID) { + return this.getSnapshot().serverData.getRewardedPlayers().contains(playerUUID); + } + + @Override + public @Unmodifiable Set getConnectedPlayers() { + return ImmutableSet.copyOf(this.getSnapshot().getSharedData().getConnectedPlayers()); + } + + @Override + public boolean hasConnectedPlayer(final UUID playerUUID) { + return this.getSnapshot().getSharedData().getConnectedPlayers().contains(playerUUID); + } + + @Override + public ItemStack getDisplayedItem() { + return CraftItemStack.asBukkitCopy(this.getSnapshot().getSharedData().getDisplayItem()); + } + + @Override + public void setDisplayedItem(final ItemStack displayedItem) { + Preconditions.checkArgument(displayedItem != null, "displayedItem must not be null"); + this.getSnapshot().getSharedData().setDisplayItem(CraftItemStack.asNMSCopy(displayedItem)); + } }