diff --git a/paper-api/src/main/java/io/papermc/paper/InternalAPIBridge.java b/paper-api/src/main/java/io/papermc/paper/InternalAPIBridge.java index 64306b5745a9..d83045ad9953 100644 --- a/paper-api/src/main/java/io/papermc/paper/InternalAPIBridge.java +++ b/paper-api/src/main/java/io/papermc/paper/InternalAPIBridge.java @@ -5,6 +5,7 @@ import io.papermc.paper.command.brigadier.CommandSourceStack; import io.papermc.paper.datacomponent.item.ResolvableProfile; import io.papermc.paper.entity.poi.PoiType; +import io.papermc.paper.persistence.PersistentDataKey; import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager; import io.papermc.paper.world.damagesource.CombatEntry; import io.papermc.paper.world.damagesource.FallLocationType; @@ -13,6 +14,7 @@ import java.util.function.BooleanSupplier; import java.util.function.Function; import java.util.function.Predicate; +import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.flattener.ComponentFlattener; import net.kyori.adventure.util.Services; @@ -31,6 +33,7 @@ import org.bukkit.entity.Pose; import org.bukkit.entity.SpawnCategory; import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.ApiStatus; @@ -121,4 +124,6 @@ default VersionFetcher getVersionFetcher() { Component resolveWithContext(Component component, @Nullable CommandSender context, @Nullable Entity scoreboardSubject, boolean bypassPermissions) throws IOException; ComponentFlattener componentFlattener(); + + PersistentDataKey createPersistentDataKey(Key key, PersistentDataType type); } diff --git a/paper-api/src/main/java/io/papermc/paper/persistence/PersistentDataContainerView.java b/paper-api/src/main/java/io/papermc/paper/persistence/PersistentDataContainerView.java index fd27d65893a2..ad6693a35a3f 100644 --- a/paper-api/src/main/java/io/papermc/paper/persistence/PersistentDataContainerView.java +++ b/paper-api/src/main/java/io/papermc/paper/persistence/PersistentDataContainerView.java @@ -1,6 +1,7 @@ package io.papermc.paper.persistence; import java.util.Set; +import com.google.common.base.Preconditions; import org.bukkit.NamespacedKey; import org.bukkit.persistence.PersistentDataAdapterContext; import org.bukkit.persistence.PersistentDataContainer; @@ -66,6 +67,32 @@ public interface PersistentDataContainerView { */ boolean has(NamespacedKey key); + /** + * Returns if the persistent metadata provider has metadata registered + * matching the provided parameters. + *

+ * This method will only return true if the found value has the same primitive + * data type as the provided data key. + *

+ * Storing a value using a custom {@link PersistentDataType} implementation + * will not store the complex data type. Therefore, storing a UUID (by + * storing a byte[]) will match has("key", {@link PersistentDataType#BYTE_ARRAY}). + * Likewise, a stored byte[] will always match your UUID {@link PersistentDataType} + * even if it is not 16 bytes long. + *

+ * This method is only usable for custom object keys. Overwriting existing + * tags, like the display name, will not work as the values are stored + * using your namespace. + * + * @param dataKey the data key the value is stored under + * @return if a value with the provided key and type exists + * @throws IllegalArgumentException if the key to look up is null + */ + default boolean has(final PersistentDataKey dataKey) { + Preconditions.checkArgument(dataKey != null, "The data key cannot be null"); + return this.has(dataKey.key(), dataKey.dataType()); + } + /** * Returns the metadata value that is stored on the * {@link PersistentDataHolder} instance. @@ -110,6 +137,46 @@ public interface PersistentDataContainerView { */ C getOrDefault(NamespacedKey key, PersistentDataType type, C defaultValue); + /** + * Returns the metadata value that is stored on the + * {@link PersistentDataHolder} instance. + * + * @param dataKey the data key to look up in the custom tag map + * @param the generic type of the eventually created complex object + * @return the value or {@code null} if no value was mapped under the given + * value + * @throws IllegalArgumentException if the data key to look up is null + * @throws IllegalArgumentException if a value exists under the given key, + * but cannot be accessed using the given type + * @throws IllegalArgumentException if no suitable adapter was found for + * the {@link PersistentDataType#getPrimitiveType()} + */ + default @Nullable C get(final PersistentDataKey dataKey) { + Preconditions.checkArgument(dataKey != null, "The data key cannot be null"); + return this.get(dataKey.key(), dataKey.dataType()); + } + + /** + * Returns the metadata value that is stored on the + * {@link PersistentDataHolder} instance. If the value does not exist in the + * container, the default value provided is returned. + * + * @param dataKey the data key to look up in the custom tag map + * @param defaultValue the default value to return if no value was found for + * the provided key + * @param the generic type of the eventually created complex object + * @return the value or the default value if no value was mapped under the given key + * @throws IllegalArgumentException if the data key to look up is null + * @throws IllegalArgumentException if a value exists under the given key, + * but cannot be accessed using the given type + * @throws IllegalArgumentException if no suitable adapter was found for + * the {@link PersistentDataType#getPrimitiveType()} + */ + default C getOrDefault(final PersistentDataKey dataKey, final C defaultValue) { + Preconditions.checkArgument(dataKey != null, "The data key cannot be null"); + return this.getOrDefault(dataKey.key(), dataKey.dataType(), defaultValue); + } + /** * Get the set of keys present on this {@link PersistentDataContainer} * instance. diff --git a/paper-api/src/main/java/io/papermc/paper/persistence/PersistentDataKey.java b/paper-api/src/main/java/io/papermc/paper/persistence/PersistentDataKey.java new file mode 100644 index 000000000000..fb7a4cd4f7b5 --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/persistence/PersistentDataKey.java @@ -0,0 +1,69 @@ +package io.papermc.paper.persistence; + +import io.papermc.paper.InternalAPIBridge; +import net.kyori.adventure.key.Key; +import org.bukkit.NamespacedKey; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.jspecify.annotations.NullMarked; + +/** + * This class represents the union of a {@link Key} and a + * {@link PersistentDataType}. + *

+ * The advantage of this class is more direct, typed access to data stored in a {@link PersistentDataContainer}. + * Instead of having to keep track of the {@link NamespacedKey} and {@link PersistentDataType} separately + * when working with PDC, this class allows you to store both together. + *

+ *

Example usage

+ *
{@code
+ * public class CustomLogic {
+ *   public static final PersistentDataKey TRACKED_VALUE = PersistentDataKey.of(
+ *     Key.key("custom:tracked_value"),
+ *     PersistentDataType.INTEGER
+ *   );
+ *   public static final PersistentDataKey OWNING_PLAYER = PersistentDataKey.of(
+ *     Key.key("custom:owning_player"),
+ *     new UUIDTagType() // Custom type
+ *   );
+ *
+ *   // Checks if the stored PDC value for OWNING_PLAYER matches the executor UUID and adds
+ *   // 1 to the TRACKED_VALUE, if it does.
+ *   public static void incrementIfOwner(PersistentDataContainer pdc, UUID executor) {
+ *     if (!Objects.equals(pdc.get(OWNING_PLAYER), executor) {
+ *       return;
+ *     }
+ *
+ *     pdc.set(TRACKED_VALUE, pdc.getOrDefault(TRACKED_VALUE, 0) + 1);
+ *   }
+ * }
+ * }
+ * + * @param the retrieved object type when applying this tag type + */ +@NullMarked +public interface PersistentDataKey { + + /** + * Creates a new {@link PersistentDataKey} with the given {@link Key} and {@link PersistentDataType}. + * + * @param key the access key to store + * @param type the persistent data type to reference + * @param the retrieved object type when applying this tag type + * @return a new {@link PersistentDataKey} of the given key and type + * @throws IllegalArgumentException if either key or type are null + */ + static PersistentDataKey of(final Key key, final PersistentDataType type) { + return InternalAPIBridge.get().createPersistentDataKey(key, type); + } + + /** + * {@return the access key of this {@link PersistentDataKey}} + */ + NamespacedKey key(); + + /** + * {@return the data type of this {@link PersistentDataKey}} + */ + PersistentDataType dataType(); +} diff --git a/paper-api/src/main/java/org/bukkit/persistence/PersistentDataContainer.java b/paper-api/src/main/java/org/bukkit/persistence/PersistentDataContainer.java index 9320c0585876..73b675f0474f 100644 --- a/paper-api/src/main/java/org/bukkit/persistence/PersistentDataContainer.java +++ b/paper-api/src/main/java/org/bukkit/persistence/PersistentDataContainer.java @@ -1,5 +1,7 @@ package org.bukkit.persistence; +import com.google.common.base.Preconditions; +import io.papermc.paper.persistence.PersistentDataKey; import org.bukkit.NamespacedKey; import org.jetbrains.annotations.NotNull; @@ -26,11 +28,34 @@ public interface PersistentDataContainer extends io.papermc.paper.persistence.Pe * @throws IllegalArgumentException if the key is null * @throws IllegalArgumentException if the type is null * @throws IllegalArgumentException if the value is null. Removing a tag should - * be done using {@link #remove(NamespacedKey)} + * be done using {@link #remove(NamespacedKey)} or {@link #remove(PersistentDataKey)} * @throws IllegalArgumentException if no suitable adapter was found for * the {@link PersistentDataType#getPrimitiveType()} */ void set(@NotNull NamespacedKey key, @NotNull PersistentDataType type, @NotNull C value); + + /** + * Stores a metadata value on the {@link PersistentDataHolder} instance. + *

+ * This API cannot be used to manipulate minecraft data, as the values will + * be stored using your namespace. This method will override any existing + * value the {@link PersistentDataHolder} may have stored under the provided + * key. + * + * @param dataKey the data key this value will be stored under + * @param value the value to store in the tag + * @param the generic type of the object to store + * + * @throws IllegalArgumentException if the data key is null + * @throws IllegalArgumentException if the value is null. Removing a tag should + * be done using {@link #remove(NamespacedKey)} or {@link #remove(PersistentDataKey)} + * @throws IllegalArgumentException if no suitable adapter was found for + * the {@link PersistentDataType#getPrimitiveType()} + */ + default void set(final @NotNull PersistentDataKey dataKey, final @NotNull C value) { + Preconditions.checkArgument(dataKey != null, "The data key cannot be null"); + this.set(dataKey.key(), dataKey.dataType(), value); + } // Paper - move to PersistentDataContainerView /** @@ -41,6 +66,17 @@ public interface PersistentDataContainer extends io.papermc.paper.persistence.Pe * @throws IllegalArgumentException if the provided key is null */ void remove(@NotNull NamespacedKey key); + + /** + * Removes a custom key from the {@link PersistentDataHolder} instance. + * + * @param dataKey the data key to remove + * @throws IllegalArgumentException if the provided data key is null + */ + default void remove(final @NotNull PersistentDataKey dataKey) { + Preconditions.checkArgument(dataKey != null, "The data key cannot be null"); + this.remove(dataKey.key()); + } // Paper - move to PersistentDataContainerView // Paper start - byte array serialization diff --git a/paper-server/src/main/java/io/papermc/paper/PaperServerInternalAPIBridge.java b/paper-server/src/main/java/io/papermc/paper/PaperServerInternalAPIBridge.java index 0152a712697a..2442ffe47ac9 100644 --- a/paper-server/src/main/java/io/papermc/paper/PaperServerInternalAPIBridge.java +++ b/paper-server/src/main/java/io/papermc/paper/PaperServerInternalAPIBridge.java @@ -13,6 +13,8 @@ import io.papermc.paper.datacomponent.item.ResolvableProfile; import io.papermc.paper.entity.poi.PaperPoiType; import io.papermc.paper.entity.poi.PoiType; +import io.papermc.paper.persistence.PaperPersistentDataKey; +import io.papermc.paper.persistence.PersistentDataKey; import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager; import io.papermc.paper.plugin.lifecycle.event.PaperLifecycleEventManager; import io.papermc.paper.util.MCUtil; @@ -26,6 +28,7 @@ import java.util.function.BooleanSupplier; import java.util.function.Function; import java.util.function.Predicate; +import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.flattener.ComponentFlattener; import net.minecraft.Optionull; @@ -69,6 +72,7 @@ import org.bukkit.entity.Pose; import org.bukkit.entity.SpawnCategory; import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; import org.jspecify.annotations.NullMarked; @@ -257,4 +261,12 @@ public Component resolveWithContext(final Component component, final @Nullable C public ComponentFlattener componentFlattener() { return PaperAdventure.FLATTENER; } + + @Override + public PersistentDataKey createPersistentDataKey(final Key key, final PersistentDataType type) { + final NamespacedKey namespacedKey = key instanceof NamespacedKey thisKey + ? thisKey + : new NamespacedKey(key.namespace(), key.value()); + return new PaperPersistentDataKey<>(namespacedKey, type); + } } diff --git a/paper-server/src/main/java/io/papermc/paper/persistence/PaperPersistentDataKey.java b/paper-server/src/main/java/io/papermc/paper/persistence/PaperPersistentDataKey.java new file mode 100644 index 000000000000..a49e8016a489 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/persistence/PaperPersistentDataKey.java @@ -0,0 +1,18 @@ +package io.papermc.paper.persistence; + +import com.google.common.base.Preconditions; +import org.bukkit.NamespacedKey; +import org.bukkit.persistence.PersistentDataType; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public record PaperPersistentDataKey( + NamespacedKey key, + PersistentDataType dataType +) implements PersistentDataKey { + + public PaperPersistentDataKey { + Preconditions.checkArgument(key != null, "The key cannot be null"); + Preconditions.checkArgument(dataType != null, "The type cannot be null"); + } +}