aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches
diff options
context:
space:
mode:
Diffstat (limited to 'patches')
-rw-r--r--patches/api/0473-General-ItemMeta-fixes.patch52
-rw-r--r--patches/api/0478-Proxy-ItemStack-to-CraftItemStack.patch472
-rw-r--r--patches/api/0479-Make-a-PDC-view-accessible-directly-from-ItemStack.patch434
-rw-r--r--patches/server/0537-ItemStack-repair-check-API.patch11
-rw-r--r--patches/server/0970-General-ItemMeta-fixes.patch94
-rw-r--r--patches/server/0977-Fix-equipment-slot-and-group-API.patch2
-rw-r--r--patches/server/1024-Proxy-ItemStack-to-CraftItemStack.patch300
-rw-r--r--patches/server/1025-Make-a-PDC-view-accessible-directly-from-ItemStack.patch266
8 files changed, 1611 insertions, 20 deletions
diff --git a/patches/api/0473-General-ItemMeta-fixes.patch b/patches/api/0473-General-ItemMeta-fixes.patch
index 22f1db839c..a8eb5a99e1 100644
--- a/patches/api/0473-General-ItemMeta-fixes.patch
+++ b/patches/api/0473-General-ItemMeta-fixes.patch
@@ -19,3 +19,55 @@ index 637fa73d4366c2d88e2716e5c8d3465706d788a7..0f03c546ecdd6383fb36a362d18d6fb5
this.flicker = flicker;
this.trail = trail;
this.colors = colors;
+diff --git a/src/main/java/org/bukkit/inventory/meta/Damageable.java b/src/main/java/org/bukkit/inventory/meta/Damageable.java
+index ff6818b6d9e0207eafdd749928f33aeac3f27191..992f39da07bafe9769effaa7dc6adc018c89329d 100644
+--- a/src/main/java/org/bukkit/inventory/meta/Damageable.java
++++ b/src/main/java/org/bukkit/inventory/meta/Damageable.java
+@@ -9,14 +9,17 @@ import org.jetbrains.annotations.Nullable;
+ public interface Damageable extends ItemMeta {
+
+ /**
+- * Checks to see if this item has damage
++ * Checks to see if this item has damage greater than 0.
+ *
+- * @return true if this has damage
++ * @return true if this has damage > 0
+ */
+ boolean hasDamage();
+
+ /**
+ * Gets the damage
++ * <p>
++ * Call {@link #hasDamageValue()} to be sure
++ * a damage value is set.
+ *
+ * @return the damage
+ */
+@@ -26,9 +29,27 @@ public interface Damageable extends ItemMeta {
+ * Sets the damage
+ *
+ * @param damage item damage
++ * @see #resetDamage() to reset and clear the damage data component
+ */
+ void setDamage(int damage);
+
++ // Paper start
++ /**
++ * Checks if any damage value, including 0,
++ * is set on this meta.
++ *
++ * @return true if any value is set
++ */
++ boolean hasDamageValue();
++
++ /**
++ * Clears the damage component from the meta. Differs
++ * from {@code setDamage(0)} in that it removes the component
++ * instead of adding the component with a value of 0.
++ */
++ void resetDamage();
++ // Paper end
++
+ /**
+ * Checks to see if this item has a maximum amount of damage.
+ *
diff --git a/patches/api/0478-Proxy-ItemStack-to-CraftItemStack.patch b/patches/api/0478-Proxy-ItemStack-to-CraftItemStack.patch
new file mode 100644
index 0000000000..cb34e5b4af
--- /dev/null
+++ b/patches/api/0478-Proxy-ItemStack-to-CraftItemStack.patch
@@ -0,0 +1,472 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <[email protected]>
+Date: Tue, 14 May 2024 11:57:51 -0700
+Subject: [PATCH] Proxy ItemStack to CraftItemStack
+
+
+diff --git a/src/main/java/org/bukkit/UnsafeValues.java b/src/main/java/org/bukkit/UnsafeValues.java
+index 7332034bb1753f48f7904dafab1ef4b3ee117ea3..11bccf6c27b8b2a2ed558a1375a3b3adc7952076 100644
+--- a/src/main/java/org/bukkit/UnsafeValues.java
++++ b/src/main/java/org/bukkit/UnsafeValues.java
+@@ -277,4 +277,6 @@ public interface UnsafeValues {
+ @NotNull java.util.List<net.kyori.adventure.text.Component> computeTooltipLines(@NotNull ItemStack itemStack, @NotNull io.papermc.paper.inventory.tooltip.TooltipContext tooltipContext, @Nullable org.bukkit.entity.Player player); // Paper - expose itemstack tooltip lines
+
+ <A extends Keyed, M> io.papermc.paper.registry.tag.@Nullable Tag<A> getTag(io.papermc.paper.registry.tag.@NotNull TagKey<A> tagKey); // Paper - hack to get tags for non-server backed registries
++
++ ItemStack createEmptyStack(); // Paper - proxy ItemStack
+ }
+diff --git a/src/main/java/org/bukkit/inventory/ItemStack.java b/src/main/java/org/bukkit/inventory/ItemStack.java
+index 7f5633e7689b82b937d5b985c3e6ae15dc94a20f..da91938ff941bb5903dcbd430c68c5980c01b57b 100644
+--- a/src/main/java/org/bukkit/inventory/ItemStack.java
++++ b/src/main/java/org/bukkit/inventory/ItemStack.java
+@@ -28,10 +28,38 @@ import org.jetbrains.annotations.Nullable;
+ * returns false.</b>
+ */
+ public class ItemStack implements Cloneable, ConfigurationSerializable, Translatable, net.kyori.adventure.text.event.HoverEventSource<net.kyori.adventure.text.event.HoverEvent.ShowItem>, net.kyori.adventure.translation.Translatable { // Paper
+- private Material type = Material.AIR;
+- private int amount = 0;
++ private ItemStack craftDelegate; // Paper - always delegate to server-backed stack
+ private MaterialData data = null;
+- private ItemMeta meta;
++
++ // Paper start - add static factory methods
++ /**
++ * Creates an itemstack with the specified item type and a count of 1.
++ *
++ * @param type the item type to use
++ * @return a new itemstack
++ * @throws IllegalArgumentException if the Material provided is not an item ({@link Material#isItem()})
++ */
++ @org.jetbrains.annotations.Contract(value = "_ -> new", pure = true)
++ public static @NotNull ItemStack of(final @NotNull Material type) {
++ return of(type, 1);
++ }
++
++ /**
++ * Creates an itemstack with the specified item type and count.
++ *
++ * @param type the item type to use
++ * @param amount the count of items in the stack
++ * @return a new itemstack
++ * @throws IllegalArgumentException if the Material provided is not an item ({@link Material#isItem()})
++ * @throws IllegalArgumentException if the amount is less than 1
++ */
++ @org.jetbrains.annotations.Contract(value = "_, _ -> new", pure = true)
++ public static @NotNull ItemStack of(final @NotNull Material type, final int amount) {
++ Preconditions.checkArgument(type.asItemType() != null, type + " isn't an item");
++ Preconditions.checkArgument(amount > 0, "amount must be greater than 0");
++ return java.util.Objects.requireNonNull(type.asItemType(), type + " is not an item").createItemStack(amount); // Paper - delegate
++ }
++ // Paper end
+
+ @Utility
+ protected ItemStack() {}
+@@ -44,7 +72,10 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ * {@link Material#isItem()} returns false.</b>
+ *
+ * @param type item material
++ * @apiNote use {@link #of(Material)}
++ * @see #of(Material)
+ */
++ @org.jetbrains.annotations.ApiStatus.Obsolete(since = "1.21") // Paper
+ public ItemStack(@NotNull final Material type) {
+ this(type, 1);
+ }
+@@ -58,7 +89,10 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ *
+ * @param type item material
+ * @param amount stack size
++ * @apiNote Use {@link #of(Material, int)}
++ * @see #of(Material, int)
+ */
++ @org.jetbrains.annotations.ApiStatus.Obsolete(since = "1.21") // Paper
+ public ItemStack(@NotNull final Material type, final int amount) {
+ this(type, amount, (short) 0);
+ }
+@@ -93,8 +127,7 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ type = Bukkit.getUnsafe().fromLegacy(new MaterialData(type, data == null ? (byte) damage : data), true);
+ }
+ }
+- this.type = type;
+- this.amount = amount;
++ this.craftDelegate = ItemStack.of(type, amount); // Paper - create delegate
+ if (damage != 0) {
+ setDurability(damage);
+ }
+@@ -109,17 +142,16 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ * @param stack the stack to copy
+ * @throws IllegalArgumentException if the specified stack is null or
+ * returns an item meta not created by the item factory
++ * @apiNote Use {@link #clone()}
++ * @see #clone()
+ */
++ @org.jetbrains.annotations.ApiStatus.Obsolete(since = "1.21") // Paper
+ public ItemStack(@NotNull final ItemStack stack) throws IllegalArgumentException {
+ Preconditions.checkArgument(stack != null, "Cannot copy null stack");
+- this.type = stack.getType();
+- this.amount = stack.getAmount();
+- if (this.type.isLegacy()) {
++ this.craftDelegate = stack.clone(); // Paper - delegate
++ if (stack.getType().isLegacy()) {
+ this.data = stack.getData();
+ }
+- if (stack.hasItemMeta()) {
+- setItemMeta0(stack.getItemMeta(), type);
+- }
+ }
+
+ /**
+@@ -127,10 +159,9 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ *
+ * @return Type of the items in this stack
+ */
+- @Utility
+ @NotNull
+ public Material getType() {
+- return type;
++ return this.craftDelegate.getType(); // Paper - delegate
+ }
+
+ /**
+@@ -153,19 +184,10 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ * Using this method in ItemStacks passed in events will result in undefined behavior.
+ * @see ItemStack#withType(Material)
+ */
+- @Utility
+ @Deprecated // Paper
+ public void setType(@NotNull Material type) {
+ Preconditions.checkArgument(type != null, "Material cannot be null");
+- this.type = type;
+- if (this.meta != null) {
+- this.meta = Bukkit.getItemFactory().asMetaFor(meta, type);
+- }
+- if (type.isLegacy()) {
+- createData((byte) 0);
+- } else {
+- this.data = null;
+- }
++ this.craftDelegate.setType(type); // Paper - delegate
+ }
+ // Paper start
+ /**
+@@ -177,12 +199,7 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ @NotNull
+ @org.jetbrains.annotations.Contract(value = "_ -> new", pure = true)
+ public ItemStack withType(@NotNull Material type) {
+- ItemStack itemStack = new ItemStack(type, this.amount);
+- if (this.hasItemMeta()) {
+- itemStack.setItemMeta(this.getItemMeta());
+- }
+-
+- return itemStack;
++ return this.craftDelegate.withType(type); // Paper - delegate
+ }
+ // Paper end
+
+@@ -192,7 +209,7 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ * @return Amount of items in this stack
+ */
+ public int getAmount() {
+- return amount;
++ return this.craftDelegate.getAmount(); // Paper - delegate
+ }
+
+ /**
+@@ -201,7 +218,7 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ * @param amount New amount of items in this stack
+ */
+ public void setAmount(int amount) {
+- this.amount = amount;
++ this.craftDelegate.setAmount(amount); // Paper - delegate
+ }
+
+ /**
+@@ -254,11 +271,7 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ */
+ @Deprecated
+ public void setDurability(final short durability) {
+- ItemMeta meta = getItemMeta();
+- if (meta != null) {
+- ((Damageable) meta).setDamage(durability);
+- setItemMeta(meta);
+- }
++ this.craftDelegate.setDurability(durability); // Paper - delegate
+ }
+
+ /**
+@@ -269,8 +282,7 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ */
+ @Deprecated
+ public short getDurability() {
+- ItemMeta meta = getItemMeta();
+- return (meta == null) ? 0 : (short) ((Damageable) meta).getDamage();
++ return this.craftDelegate.getDurability(); // Paper - delegate
+ }
+
+ /**
+@@ -282,17 +294,12 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ *
+ * @return The maximum you can stack this item to.
+ */
+- @Utility
+ public int getMaxStackSize() {
+- if (meta != null && meta.hasMaxStackSize()) {
+- return meta.getMaxStackSize();
+- }
+-
+- return getType().getMaxStackSize();
++ return this.craftDelegate.getMaxStackSize(); // Paper - delegate
+ }
+
+ private void createData(final byte data) {
+- this.data = type.getNewData(data);
++ this.data = this.craftDelegate.getType().getNewData(data); // Paper
+ }
+
+ @Override
+@@ -306,17 +313,8 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ }
+
+ @Override
+- @Utility
+ public boolean equals(Object obj) {
+- if (this == obj) {
+- return true;
+- }
+- if (!(obj instanceof ItemStack)) {
+- return false;
+- }
+-
+- ItemStack stack = (ItemStack) obj;
+- return getAmount() == stack.getAmount() && isSimilar(stack);
++ return this.craftDelegate.equals(obj); // Paper - delegate
+ }
+
+ /**
+@@ -326,49 +324,19 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ * @param stack the item stack to compare to
+ * @return true if the two stacks are equal, ignoring the amount
+ */
+- @Utility
+ public boolean isSimilar(@Nullable ItemStack stack) {
+- if (stack == null) {
+- return false;
+- }
+- if (stack == this) {
+- return true;
+- }
+- Material comparisonType = (this.type.isLegacy()) ? Bukkit.getUnsafe().fromLegacy(this.getData(), true) : this.type; // This may be called from legacy item stacks, try to get the right material
+- return comparisonType == stack.getType() && /* getDurability() == stack.getDurability() && */hasItemMeta() == stack.hasItemMeta() && (hasItemMeta() ? Bukkit.getItemFactory().equals(getItemMeta(), stack.getItemMeta()) : true); // Paper - remove redundant item durability check
++ return this.craftDelegate.isSimilar(stack); // Paper - delegate
+ }
+
+ @NotNull
+ @Override
+ public ItemStack clone() {
+- try {
+- ItemStack itemStack = (ItemStack) super.clone();
+-
+- if (this.meta != null) {
+- itemStack.meta = this.meta.clone();
+- }
+-
+- if (this.data != null) {
+- itemStack.data = this.data.clone();
+- }
+-
+- return itemStack;
+- } catch (CloneNotSupportedException e) {
+- throw new Error(e);
+- }
++ return this.craftDelegate.clone(); // Paper - delegate
+ }
+
+ @Override
+- @Utility
+ public int hashCode() {
+- int hash = 1;
+-
+- hash = hash * 31 + getType().hashCode();
+- hash = hash * 31 + getAmount();
+- hash = hash * 31 + (getDurability() & 0xffff);
+- hash = hash * 31 + (hasItemMeta() ? (meta == null ? getItemMeta().hashCode() : meta.hashCode()) : 0);
+-
+- return hash;
++ return this.craftDelegate.hashCode(); // Paper - delegate
+ }
+
+ /**
+@@ -378,7 +346,7 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ * @return True if this has the given enchantment
+ */
+ public boolean containsEnchantment(@NotNull Enchantment ench) {
+- return meta == null ? false : meta.hasEnchant(ench);
++ return this.craftDelegate.containsEnchantment(ench); // Paper - delegate
+ }
+
+ /**
+@@ -388,7 +356,7 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ * @return Level of the enchantment, or 0
+ */
+ public int getEnchantmentLevel(@NotNull Enchantment ench) {
+- return meta == null ? 0 : meta.getEnchantLevel(ench);
++ return this.craftDelegate.getEnchantmentLevel(ench); // Paper - delegate
+ }
+
+ /**
+@@ -398,7 +366,7 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ */
+ @NotNull
+ public Map<Enchantment, Integer> getEnchantments() {
+- return meta == null ? ImmutableMap.<Enchantment, Integer>of() : meta.getEnchants();
++ return this.craftDelegate.getEnchantments(); // Paper - delegate
+ }
+
+ /**
+@@ -474,10 +442,7 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ * @param level Level of the enchantment
+ */
+ public void addUnsafeEnchantment(@NotNull Enchantment ench, int level) {
+- ItemMeta itemMeta = (meta == null ? meta = Bukkit.getItemFactory().getItemMeta(type) : meta);
+- if (itemMeta != null) {
+- itemMeta.addEnchant(ench, level, true);
+- }
++ this.craftDelegate.addUnsafeEnchantment(ench, level); // Paper - delegate
+ }
+
+ /**
+@@ -488,23 +453,14 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ * @return Previous level, or 0
+ */
+ public int removeEnchantment(@NotNull Enchantment ench) {
+- int level = getEnchantmentLevel(ench);
+- if (level == 0 || meta == null) {
+- return level;
+- }
+- meta.removeEnchant(ench);
+- return level;
++ return this.craftDelegate.removeEnchantment(ench); // Paper - delegate
+ }
+
+ /**
+ * Removes all enchantments on this ItemStack.
+ */
+ public void removeEnchantments() {
+- if (meta == null) {
+- return;
+- }
+-
+- meta.removeEnchantments();
++ this.craftDelegate.removeEnchantments(); // Paper - delegate
+ }
+
+ @Override
+@@ -660,7 +616,7 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ */
+ @UndefinedNullability // Paper
+ public ItemMeta getItemMeta() {
+- return this.meta == null ? Bukkit.getItemFactory().getItemMeta(this.type) : this.meta.clone();
++ return this.craftDelegate.getItemMeta(); // Paper - delegate
+ }
+
+ /**
+@@ -669,7 +625,7 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ * @return Returns true if some meta data has been set for this item
+ */
+ public boolean hasItemMeta() {
+- return !Bukkit.getItemFactory().equals(meta, null);
++ return this.craftDelegate.hasItemMeta(); // Paper - delegate
+ }
+
+ /**
+@@ -682,28 +638,10 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ * the {@link ItemFactory}
+ */
+ public boolean setItemMeta(@Nullable ItemMeta itemMeta) {
+- return setItemMeta0(itemMeta, type);
++ return this.craftDelegate.setItemMeta(itemMeta); // Paper - delegate
+ }
+
+- /*
+- * Cannot be overridden, so it's safe for constructor call
+- */
+- private boolean setItemMeta0(@Nullable ItemMeta itemMeta, @NotNull Material material) {
+- if (itemMeta == null) {
+- this.meta = null;
+- return true;
+- }
+- if (!Bukkit.getItemFactory().isApplicable(itemMeta, material)) {
+- return false;
+- }
+- this.meta = Bukkit.getItemFactory().asMetaFor(itemMeta, material);
+-
+- if (this.meta == itemMeta) {
+- this.meta = itemMeta.clone();
+- }
+-
+- return true;
+- }
++ // Paper - delegate
+
+ @Override
+ @NotNull
+@@ -807,11 +745,7 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ }
+
+ public int getMaxItemUseDuration(@NotNull final org.bukkit.entity.LivingEntity entity) {
+- if (type == null || type == Material.AIR || !type.isItem()) {
+- return 0;
+- }
+- // Requires access to NMS
+- return ensureServerConversions().getMaxItemUseDuration(entity);
++ return this.craftDelegate.getMaxItemUseDuration(entity); // Paper - delegate
+ }
+
+ /**
+@@ -1061,7 +995,8 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ */
+ @NotNull
+ public static ItemStack empty() {
+- return new ItemStack();
++ //noinspection deprecation
++ return Bukkit.getUnsafe().createEmptyStack(); // Paper - proxy ItemStack
+ }
+
+ /**
+@@ -1069,7 +1004,7 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ * it is either air or the stack has a size of 0.
+ */
+ public boolean isEmpty() {
+- return type.isAir() || amount <= 0;
++ return this.craftDelegate.isEmpty(); // Paper - delegate
+ }
+ // Paper end
+ // Paper start - expose itemstack tooltip lines
+diff --git a/src/test/java/org/bukkit/configuration/ConfigurationSectionTest.java b/src/test/java/org/bukkit/configuration/ConfigurationSectionTest.java
+index ee5bc86f47cf5599e4c5c34e3a9084f86d74bdb7..c6b49c7e0a3357566fc859d4f0c76e77fefbd371 100644
+--- a/src/test/java/org/bukkit/configuration/ConfigurationSectionTest.java
++++ b/src/test/java/org/bukkit/configuration/ConfigurationSectionTest.java
+@@ -547,6 +547,7 @@ public abstract class ConfigurationSectionTest {
+ }
+
+ @Test
++ @org.junit.jupiter.api.Disabled("ItemStack can't exist without the Server, test moved to server")
+ public void testGetItemStack_String() {
+ ConfigurationSection section = getConfigurationSection();
+ String key = "exists";
+@@ -559,6 +560,7 @@ public abstract class ConfigurationSectionTest {
+ }
+
+ @Test
++ @org.junit.jupiter.api.Disabled("ItemStack can't exist without the Server, test moved to server")
+ public void testGetItemStack_String_ItemStack() {
+ ConfigurationSection section = getConfigurationSection();
+ String key = "exists";
+@@ -572,6 +574,7 @@ public abstract class ConfigurationSectionTest {
+ }
+
+ @Test
++ @org.junit.jupiter.api.Disabled("ItemStack can't exist without the Server, test moved to server")
+ public void testIsItemStack() {
+ ConfigurationSection section = getConfigurationSection();
+ String key = "exists";
diff --git a/patches/api/0479-Make-a-PDC-view-accessible-directly-from-ItemStack.patch b/patches/api/0479-Make-a-PDC-view-accessible-directly-from-ItemStack.patch
new file mode 100644
index 0000000000..a37a724014
--- /dev/null
+++ b/patches/api/0479-Make-a-PDC-view-accessible-directly-from-ItemStack.patch
@@ -0,0 +1,434 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <[email protected]>
+Date: Wed, 12 Jun 2024 10:29:30 -0700
+Subject: [PATCH] Make a PDC view accessible directly from ItemStack
+
+
+diff --git a/src/main/java/io/papermc/paper/persistence/PersistentDataContainerView.java b/src/main/java/io/papermc/paper/persistence/PersistentDataContainerView.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..4a4949fde3f03e2ea9d2dde245f7dea119e202bf
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/persistence/PersistentDataContainerView.java
+@@ -0,0 +1,167 @@
++package io.papermc.paper.persistence;
++
++import java.util.Set;
++import org.bukkit.NamespacedKey;
++import org.bukkit.persistence.PersistentDataAdapterContext;
++import org.bukkit.persistence.PersistentDataContainer;
++import org.bukkit.persistence.PersistentDataHolder;
++import org.bukkit.persistence.PersistentDataType;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.jetbrains.annotations.ApiStatus;
++
++/**
++ * This represents a view of a persistent data container. No
++ * methods on this interface mutate the container.
++ * @see PersistentDataContainer
++ */
++public interface PersistentDataContainerView {
++
++ /**
++ * Returns if the persistent metadata provider has metadata registered
++ * matching the provided parameters.
++ * <p>
++ * This method will only return true if the found value has the same primitive
++ * data type as the provided key.
++ * <p>
++ * 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.
++ * <p>
++ * 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 key the key the value is stored under
++ * @param type the type the primative stored value has to match
++ * @param <P> the generic type of the stored primitive
++ * @param <C> the generic type of the eventually created complex object
++ *
++ * @return if a value with the provided key and type exists
++ *
++ * @throws IllegalArgumentException if the key to look up is null
++ * @throws IllegalArgumentException if the type to cast the found object to is
++ * null
++ */
++ <P, C> boolean has(@NonNull NamespacedKey key, @NonNull PersistentDataType<P, C> type);
++
++ /**
++ * Returns if the persistent metadata provider has metadata registered matching
++ * the provided parameters.
++ * <p>
++ * This method will return true as long as a value with the given key exists,
++ * regardless of its type.
++ * <p>
++ * 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 key the key the value is stored under
++ *
++ * @return if a value with the provided key exists
++ *
++ * @throws IllegalArgumentException if the key to look up is null
++ */
++ boolean has(@NonNull NamespacedKey key);
++
++ /**
++ * Returns the metadata value that is stored on the
++ * {@link PersistentDataHolder} instance.
++ *
++ * @param key the key to look up in the custom tag map
++ * @param type the type the value must have and will be casted to
++ * @param <P> the generic type of the stored primitive
++ * @param <C> 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 key to look up is null
++ * @throws IllegalArgumentException if the type to cast the found object to 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()}
++ */
++ <P, C> @Nullable C get(@NonNull NamespacedKey key, @NonNull PersistentDataType<P, C> type);
++
++ /**
++ * 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 key the key to look up in the custom tag map
++ * @param type the type the value must have and will be casted to
++ * @param defaultValue the default value to return if no value was found for
++ * the provided key
++ * @param <P> the generic type of the stored primitive
++ * @param <C> 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 key to look up is null
++ * @throws IllegalArgumentException if the type to cast the found object to 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()}
++ */
++ <P, C> @NonNull C getOrDefault(@NonNull NamespacedKey key, @NonNull PersistentDataType<P, C> type, @NonNull C defaultValue);
++
++ /**
++ * Get the set of keys present on this {@link PersistentDataContainer}
++ * instance.
++ *
++ * Any changes made to the returned set will not be reflected on the
++ * instance.
++ *
++ * @return the key set
++ */
++ @NonNull Set<NamespacedKey> getKeys();
++
++ /**
++ * Returns if the container instance is empty, therefore has no entries
++ * inside it.
++ *
++ * @return the boolean
++ */
++ boolean isEmpty();
++
++ /**
++ * Copies all values from this {@link PersistentDataContainer} to the provided
++ * container.
++ * <p>
++ * This method only copies custom object keys. Existing tags, like the display
++ * name, will not be copied as the values are stored using your namespace.
++ *
++ * @param other the container to copy to
++ * @param replace whether to replace any matching values in the target container
++ *
++ * @throws IllegalArgumentException if the other container is null
++ */
++ void copyTo(@NonNull PersistentDataContainer other, boolean replace);
++
++ /**
++ * Returns the adapter context this tag container uses.
++ *
++ * @return the tag context
++ */
++ @NonNull PersistentDataAdapterContext getAdapterContext();
++
++ /**
++ * Serialize this {@link PersistentDataContainer} instance to a
++ * byte array.
++ *
++ * @return a binary representation of this container
++ * @throws java.io.IOException if we fail to write this container to a byte array
++ */
++ byte @NonNull [] serializeToBytes() throws java.io.IOException;
++}
+diff --git a/src/main/java/io/papermc/paper/persistence/PersistentDataViewHolder.java b/src/main/java/io/papermc/paper/persistence/PersistentDataViewHolder.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9789916f949374bfb50da535b076180700f7ae73
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/persistence/PersistentDataViewHolder.java
+@@ -0,0 +1,23 @@
++package io.papermc.paper.persistence;
++
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.jetbrains.annotations.ApiStatus;
++
++/**
++ * The {@link PersistentDataViewHolder} interface defines an object that can view
++ * custom persistent data on it.
++ */
++public interface PersistentDataViewHolder {
++
++ /**
++ * Returns a custom tag container view capable of viewing tags on the object.
++ * <p>
++ * Note that the tags stored on this container are all stored under their
++ * own custom namespace therefore modifying default tags using this
++ * {@link PersistentDataViewHolder} is impossible.
++ *
++ * @return the persistent data container view
++ */
++ @NonNull PersistentDataContainerView getPersistentDataContainer();
++}
+diff --git a/src/main/java/org/bukkit/inventory/ItemStack.java b/src/main/java/org/bukkit/inventory/ItemStack.java
+index da91938ff941bb5903dcbd430c68c5980c01b57b..f603b5b6ba80af919f415322583a8345a5b1358a 100644
+--- a/src/main/java/org/bukkit/inventory/ItemStack.java
++++ b/src/main/java/org/bukkit/inventory/ItemStack.java
+@@ -27,7 +27,7 @@ import org.jetbrains.annotations.Nullable;
+ * use this class to encapsulate Materials for which {@link Material#isItem()}
+ * returns false.</b>
+ */
+-public class ItemStack implements Cloneable, ConfigurationSerializable, Translatable, net.kyori.adventure.text.event.HoverEventSource<net.kyori.adventure.text.event.HoverEvent.ShowItem>, net.kyori.adventure.translation.Translatable { // Paper
++public class ItemStack implements Cloneable, ConfigurationSerializable, Translatable, net.kyori.adventure.text.event.HoverEventSource<net.kyori.adventure.text.event.HoverEvent.ShowItem>, net.kyori.adventure.translation.Translatable, io.papermc.paper.persistence.PersistentDataViewHolder { // Paper
+ private ItemStack craftDelegate; // Paper - always delegate to server-backed stack
+ private MaterialData data = null;
+
+@@ -61,6 +61,13 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+ }
+ // Paper end
+
++ // Paper start - pdc
++ @Override
++ public io.papermc.paper.persistence.@NotNull PersistentDataContainerView getPersistentDataContainer() {
++ return this.craftDelegate.getPersistentDataContainer();
++ }
++ // Paper end - pdc
++
+ @Utility
+ protected ItemStack() {}
+
+diff --git a/src/main/java/org/bukkit/persistence/PersistentDataContainer.java b/src/main/java/org/bukkit/persistence/PersistentDataContainer.java
+index decf3b1949d4653a9fb01684b93ff91048137076..0f40eb52f6e2a5b6932cf9f223c1bbe9ab9eab1b 100644
+--- a/src/main/java/org/bukkit/persistence/PersistentDataContainer.java
++++ b/src/main/java/org/bukkit/persistence/PersistentDataContainer.java
+@@ -9,7 +9,7 @@ import org.jetbrains.annotations.Nullable;
+ * This interface represents a map like object, capable of storing custom tags
+ * in it.
+ */
+-public interface PersistentDataContainer {
++public interface PersistentDataContainer extends io.papermc.paper.persistence.PersistentDataContainerView { // Paper - split up view and mutable
+
+ /**
+ * Stores a metadata value on the {@link PersistentDataHolder} instance.
+@@ -33,118 +33,7 @@ public interface PersistentDataContainer {
+ * the {@link PersistentDataType#getPrimitiveType()}
+ */
+ <P, C> void set(@NotNull NamespacedKey key, @NotNull PersistentDataType<P, C> type, @NotNull C value);
+-
+- /**
+- * Returns if the persistent metadata provider has metadata registered
+- * matching the provided parameters.
+- * <p>
+- * This method will only return true if the found value has the same primitive
+- * data type as the provided key.
+- * <p>
+- * 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.
+- * <p>
+- * 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 key the key the value is stored under
+- * @param type the type the primative stored value has to match
+- * @param <P> the generic type of the stored primitive
+- * @param <C> the generic type of the eventually created complex object
+- *
+- * @return if a value with the provided key and type exists
+- *
+- * @throws IllegalArgumentException if the key to look up is null
+- * @throws IllegalArgumentException if the type to cast the found object to is
+- * null
+- */
+- <P, C> boolean has(@NotNull NamespacedKey key, @NotNull PersistentDataType<P, C> type);
+-
+- /**
+- * Returns if the persistent metadata provider has metadata registered matching
+- * the provided parameters.
+- * <p>
+- * This method will return true as long as a value with the given key exists,
+- * regardless of its type.
+- * <p>
+- * 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 key the key the value is stored under
+- *
+- * @return if a value with the provided key exists
+- *
+- * @throws IllegalArgumentException if the key to look up is null
+- */
+- boolean has(@NotNull NamespacedKey key);
+-
+- /**
+- * Returns the metadata value that is stored on the
+- * {@link PersistentDataHolder} instance.
+- *
+- * @param key the key to look up in the custom tag map
+- * @param type the type the value must have and will be casted to
+- * @param <P> the generic type of the stored primitive
+- * @param <C> 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 key to look up is null
+- * @throws IllegalArgumentException if the type to cast the found object to 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()}
+- */
+- @Nullable
+- <P, C> C get(@NotNull NamespacedKey key, @NotNull PersistentDataType<P, C> type);
+-
+- /**
+- * 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 key the key to look up in the custom tag map
+- * @param type the type the value must have and will be casted to
+- * @param defaultValue the default value to return if no value was found for
+- * the provided key
+- * @param <P> the generic type of the stored primitive
+- * @param <C> 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 key to look up is null
+- * @throws IllegalArgumentException if the type to cast the found object to 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()}
+- */
+- @NotNull
+- <P, C> C getOrDefault(@NotNull NamespacedKey key, @NotNull PersistentDataType<P, C> type, @NotNull C defaultValue);
+-
+- /**
+- * Get the set of keys present on this {@link PersistentDataContainer}
+- * instance.
+- *
+- * Any changes made to the returned set will not be reflected on the
+- * instance.
+- *
+- * @return the key set
+- */
+- @NotNull
+- Set<NamespacedKey> getKeys();
++ // Paper - move to PersistentDataContainerView
+
+ /**
+ * Removes a custom key from the {@link PersistentDataHolder} instance.
+@@ -154,47 +43,10 @@ public interface PersistentDataContainer {
+ * @throws IllegalArgumentException if the provided key is null
+ */
+ void remove(@NotNull NamespacedKey key);
+-
+- /**
+- * Returns if the container instance is empty, therefore has no entries
+- * inside it.
+- *
+- * @return the boolean
+- */
+- boolean isEmpty();
+-
+- /**
+- * Copies all values from this {@link PersistentDataContainer} to the provided
+- * container.
+- * <p>
+- * This method only copies custom object keys. Existing tags, like the display
+- * name, will not be copied as the values are stored using your namespace.
+- *
+- * @param other the container to copy to
+- * @param replace whether to replace any matching values in the target container
+- *
+- * @throws IllegalArgumentException if the other container is null
+- */
+- void copyTo(@NotNull PersistentDataContainer other, boolean replace);
+-
+- /**
+- * Returns the adapter context this tag container uses.
+- *
+- * @return the tag context
+- */
+- @NotNull
+- PersistentDataAdapterContext getAdapterContext();
++ // Paper - move to PersistentDataContainerView
+
+ // Paper start - byte array serialization
+- /**
+- * Serialize this {@link PersistentDataContainer} instance to a
+- * byte array.
+- *
+- * @return a binary representation of this container
+- * @throws java.io.IOException if we fail to write this container to a byte array
+- */
+- byte @NotNull [] serializeToBytes() throws java.io.IOException;
+-
++ // Paper - move to PersistentDataContainerView
+ /**
+ * Read values from a serialised byte array into this
+ * {@link PersistentDataContainer} instance.
+diff --git a/src/main/java/org/bukkit/persistence/PersistentDataHolder.java b/src/main/java/org/bukkit/persistence/PersistentDataHolder.java
+index 80b277cc57f092f04fbf7810ac78d250b207b775..71f33c1265bc753ef40108ffce0d4bd8656d2903 100644
+--- a/src/main/java/org/bukkit/persistence/PersistentDataHolder.java
++++ b/src/main/java/org/bukkit/persistence/PersistentDataHolder.java
+@@ -5,8 +5,10 @@ import org.jetbrains.annotations.NotNull;
+ /**
+ * The {@link PersistentDataHolder} interface defines an object that can store
+ * custom persistent meta data on it.
++ * <p>Prefer using {@link io.papermc.paper.persistence.PersistentDataViewHolder} for read-only operations
++ * as it covers more types.</p>
+ */
+-public interface PersistentDataHolder {
++public interface PersistentDataHolder extends io.papermc.paper.persistence.PersistentDataViewHolder { // Paper
+
+ /**
+ * Returns a custom tag container capable of storing tags on the object.
diff --git a/patches/server/0537-ItemStack-repair-check-API.patch b/patches/server/0537-ItemStack-repair-check-API.patch
index 1a746225f9..b2c00e80ef 100644
--- a/patches/server/0537-ItemStack-repair-check-API.patch
+++ b/patches/server/0537-ItemStack-repair-check-API.patch
@@ -25,10 +25,10 @@ index a71717fe99e78c480747cc61ab30b53b6667fde7..080ab25d3585552c1abd62a9992d48bf
@Override
diff --git a/src/test/java/io/papermc/paper/util/ItemStackRepairCheckTest.java b/src/test/java/io/papermc/paper/util/ItemStackRepairCheckTest.java
new file mode 100644
-index 0000000000000000000000000000000000000000..6b8d360ef86e181a680ad77f28b7dd7368dddfe7
+index 0000000000000000000000000000000000000000..9f8abe2376f16aeffe4e9f90a2da04b7e3a55429
--- /dev/null
+++ b/src/test/java/io/papermc/paper/util/ItemStackRepairCheckTest.java
-@@ -0,0 +1,48 @@
+@@ -0,0 +1,41 @@
+package io.papermc.paper.util;
+
+import org.bukkit.Material;
@@ -69,11 +69,4 @@ index 0000000000000000000000000000000000000000..6b8d360ef86e181a680ad77f28b7dd73
+
+ assertFalse(diamond.canRepair(new ItemStack(Material.OAK_BUTTON)), "diamond can repair oak button");
+ }
-+
-+ @Test
-+ public void testInvalidItem() {
-+ ItemStack badItemStack = new ItemStack(Material.ACACIA_WALL_SIGN);
-+
-+ assertFalse(badItemStack.isRepairableBy(new ItemStack(Material.DIAMOND)), "acacia wall sign is repairable by diamond");
-+ }
+}
diff --git a/patches/server/0970-General-ItemMeta-fixes.patch b/patches/server/0970-General-ItemMeta-fixes.patch
index 776e90997b..5fad25eee7 100644
--- a/patches/server/0970-General-ItemMeta-fixes.patch
+++ b/patches/server/0970-General-ItemMeta-fixes.patch
@@ -830,7 +830,7 @@ index 97b7085250d749c5e46352b372068b51de89bc7f..7277e7ee566aabf6e01937072d949ed6
}
}
diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
-index 8cfce9c557c4411914adffd10872bf3129435423..d9d4b983672b4340df926d98a7fa6708acad8a05 100644
+index 8cfce9c557c4411914adffd10872bf3129435423..62d615a4c7acfbd970278531e1bbceb71591f7b1 100644
--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
@@ -182,9 +182,10 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
@@ -846,6 +846,15 @@ index 8cfce9c557c4411914adffd10872bf3129435423..d9d4b983672b4340df926d98a7fa6708
<T> Applicator put(ItemMetaKeyType<T> key, T value) {
this.builder.set(key.TYPE, value);
+@@ -278,7 +279,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+ private CraftFoodComponent food;
+ private CraftToolComponent tool;
+ private CraftJukeboxComponent jukebox;
+- private int damage;
++ private Integer damage; // Paper - may not be set
+ private Integer maxDamage;
+
+ private static final Set<DataComponentType> HANDLED_TAGS = Sets.newHashSet();
@@ -310,7 +311,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
this.enchantments = new EnchantmentMap(meta.enchantments); // Paper
}
@@ -876,6 +885,15 @@ index 8cfce9c557c4411914adffd10872bf3129435423..d9d4b983672b4340df926d98a7fa6708
}
for (Object obj : mods.keySet()) {
+@@ -905,7 +911,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+ itemTag.put(CraftMetaItem.JUKEBOX_PLAYABLE, this.jukebox.getHandle());
+ }
+
+- if (this.hasDamage()) {
++ if (this.hasDamageValue()) { // Paper - preserve empty/0 damage
+ itemTag.put(CraftMetaItem.DAMAGE, this.damage);
+ }
+
@@ -970,10 +976,8 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
}
@@ -889,6 +907,15 @@ index 8cfce9c557c4411914adffd10872bf3129435423..d9d4b983672b4340df926d98a7fa6708
return;
}
+@@ -1010,7 +1014,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+
+ @Overridden
+ boolean isEmpty() {
+- return !(this.hasDisplayName() || this.hasItemName() || this.hasLocalizedName() || this.hasEnchants() || (this.lore != null) || this.hasCustomModelData() || this.hasBlockData() || this.hasRepairCost() || !this.unhandledTags.build().isEmpty() || !this.removedTags.isEmpty() || !this.persistentDataContainer.isEmpty() || this.hideFlag != 0 || this.isHideTooltip() || this.isUnbreakable() || this.hasEnchantmentGlintOverride() || this.isFireResistant() || this.hasMaxStackSize() || this.hasRarity() || this.hasFood() || this.hasTool() || this.hasDamage() || this.hasMaxDamage() || this.hasAttributeModifiers() || this.customTag != null || this.canPlaceOnPredicates != null || this.canBreakPredicates != null); // Paper
++ return !(this.hasDisplayName() || this.hasItemName() || this.hasLocalizedName() || this.hasEnchants() || (this.lore != null) || this.hasCustomModelData() || this.hasBlockData() || this.hasRepairCost() || !this.unhandledTags.build().isEmpty() || !this.removedTags.isEmpty() || !this.persistentDataContainer.isEmpty() || this.hideFlag != 0 || this.isHideTooltip() || this.isUnbreakable() || this.hasEnchantmentGlintOverride() || this.isFireResistant() || this.hasMaxStackSize() || this.hasRarity() || this.hasFood() || this.hasTool() || this.hasDamageValue() || this.hasMaxDamage() || this.hasAttributeModifiers() || this.customTag != null || this.canPlaceOnPredicates != null || this.canBreakPredicates != null); // Paper
+ }
+
+ // Paper start
@@ -1106,6 +1110,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
@Override
@@ -1035,7 +1062,19 @@ index 8cfce9c557c4411914adffd10872bf3129435423..d9d4b983672b4340df926d98a7fa6708
for (Map.Entry<Attribute, AttributeModifier> entry : first.entries()) {
if (!second.containsEntry(entry.getKey(), entry.getValue())) {
return false;
-@@ -1616,6 +1631,8 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+@@ -1606,19 +1621,33 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+
+ @Override
+ public boolean hasDamage() {
+- return this.damage > 0;
++ return this.damage != null && this.damage > 0; // Paper - null check
+ }
+
+ @Override
+ public int getDamage() {
+- return this.damage;
++ return this.damage == null ? 0 : this.damage; // Paper - null check
+ }
@Override
public void setDamage(int damage) {
@@ -1044,7 +1083,22 @@ index 8cfce9c557c4411914adffd10872bf3129435423..d9d4b983672b4340df926d98a7fa6708
this.damage = damage;
}
-@@ -1632,6 +1649,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
++ // Paper start - preserve empty/0 damage
++ @Override
++ public boolean hasDamageValue() {
++ return this.damage != null;
++ }
++
++ @Override
++ public void resetDamage() {
++ this.damage = null;
++ }
++ // Paper end - preserve empty/0 damage
++
+ @Override
+ public boolean hasMaxDamage() {
+ return this.maxDamage != null;
+@@ -1632,6 +1661,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
@Override
public void setMaxDamage(Integer maxDamage) {
@@ -1052,7 +1106,7 @@ index 8cfce9c557c4411914adffd10872bf3129435423..d9d4b983672b4340df926d98a7fa6708
this.maxDamage = maxDamage;
}
-@@ -1663,7 +1681,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+@@ -1663,7 +1693,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
&& (this.hasCustomModelData() ? that.hasCustomModelData() && this.customModelData.equals(that.customModelData) : !that.hasCustomModelData())
&& (this.hasBlockData() ? that.hasBlockData() && this.blockData.equals(that.blockData) : !that.hasBlockData())
&& (this.hasRepairCost() ? that.hasRepairCost() && this.repairCost == that.repairCost : !that.hasRepairCost())
@@ -1061,18 +1115,29 @@ index 8cfce9c557c4411914adffd10872bf3129435423..d9d4b983672b4340df926d98a7fa6708
&& (this.unhandledTags.equals(that.unhandledTags))
&& (this.removedTags.equals(that.removedTags))
&& (Objects.equals(this.customTag, that.customTag))
-@@ -1725,8 +1743,8 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+@@ -1678,7 +1708,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+ && (this.hasFood() ? that.hasFood() && this.food.equals(that.food) : !that.hasFood())
+ && (this.hasTool() ? that.hasTool() && this.tool.equals(that.tool) : !that.hasTool())
+ && (this.hasJukeboxPlayable() ? that.hasJukeboxPlayable() && this.jukebox.equals(that.jukebox) : !that.hasJukeboxPlayable())
+- && (this.hasDamage() ? that.hasDamage() && this.damage == that.damage : !that.hasDamage())
++ && (Objects.equals(this.damage, that.damage)) // Paper - preserve empty/0 damage
+ && (this.hasMaxDamage() ? that.hasMaxDamage() && this.maxDamage.equals(that.maxDamage) : !that.hasMaxDamage())
+ && (this.canPlaceOnPredicates != null ? that.canPlaceOnPredicates != null && this.canPlaceOnPredicates.equals(that.canPlaceOnPredicates) : that.canPlaceOnPredicates == null) // Paper
+ && (this.canBreakPredicates != null ? that.canBreakPredicates != null && this.canBreakPredicates.equals(that.canBreakPredicates) : that.canBreakPredicates == null) // Paper
+@@ -1724,9 +1754,9 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+ hash = 61 * hash + (this.hasFood() ? this.food.hashCode() : 0);
hash = 61 * hash + (this.hasTool() ? this.tool.hashCode() : 0);
hash = 61 * hash + (this.hasJukeboxPlayable() ? this.jukebox.hashCode() : 0);
- hash = 61 * hash + (this.hasDamage() ? this.damage : 0);
+- hash = 61 * hash + (this.hasDamage() ? this.damage : 0);
- hash = 61 * hash + (this.hasMaxDamage() ? 1231 : 1237);
- hash = 61 * hash + (this.hasAttributeModifiers() ? this.attributeModifiers.hashCode() : 0);
++ hash = 61 * hash + (this.hasDamageValue() ? this.damage : -1); // Paper - preserve empty/0 damage
+ hash = 61 * hash + (this.hasMaxDamage() ? this.maxDamage.hashCode() : 0); // Paper - max damage is not a boolean
+ hash = 61 * hash + (this.attributeModifiers != null ? this.attributeModifiers.hashCode() : 0); // Paper - track only null attributes
hash = 61 * hash + (this.canPlaceOnPredicates != null ? this.canPlaceOnPredicates.hashCode() : 0); // Paper
hash = 61 * hash + (this.canBreakPredicates != null ? this.canBreakPredicates.hashCode() : 0); // Paper
hash = 61 * hash + this.version;
-@@ -1746,7 +1764,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+@@ -1746,7 +1776,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
if (this.enchantments != null) {
clone.enchantments = new EnchantmentMap(this.enchantments); // Paper
}
@@ -1081,7 +1146,16 @@ index 8cfce9c557c4411914adffd10872bf3129435423..d9d4b983672b4340df926d98a7fa6708
clone.attributeModifiers = LinkedHashMultimap.create(this.attributeModifiers);
}
if (this.customTag != null) {
-@@ -1975,7 +1993,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+@@ -1874,7 +1904,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+ builder.put(CraftMetaItem.JUKEBOX_PLAYABLE.BUKKIT, this.jukebox);
+ }
+
+- if (this.hasDamage()) {
++ if (this.hasDamageValue()) { // Paper - preserve empty/0 damage
+ builder.put(CraftMetaItem.DAMAGE.BUKKIT, this.damage);
+ }
+
+@@ -1975,7 +2005,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
}
static void serializeModifiers(Multimap<Attribute, AttributeModifier> modifiers, ImmutableMap.Builder<String, Object> builder, ItemMetaKey key) {
@@ -1090,7 +1164,7 @@ index 8cfce9c557c4411914adffd10872bf3129435423..d9d4b983672b4340df926d98a7fa6708
return;
}
-@@ -2057,7 +2075,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+@@ -2057,7 +2087,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
// Paper start - improve checking handled tags
@org.jetbrains.annotations.VisibleForTesting
public static final Map<Class<? extends CraftMetaItem>, Set<DataComponentType<?>>> HANDLED_DCTS_PER_TYPE = new HashMap<>();
@@ -1099,7 +1173,7 @@ index 8cfce9c557c4411914adffd10872bf3129435423..d9d4b983672b4340df926d98a7fa6708
CraftMetaItem.NAME.TYPE,
CraftMetaItem.ITEM_NAME.TYPE,
CraftMetaItem.LORE.TYPE,
-@@ -2125,7 +2143,12 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
+@@ -2125,7 +2155,12 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
// Paper end - improve checking handled data component types
protected static <T> Optional<? extends T> getOrEmpty(DataComponentPatch tag, ItemMetaKeyType<T> type) {
diff --git a/patches/server/0977-Fix-equipment-slot-and-group-API.patch b/patches/server/0977-Fix-equipment-slot-and-group-API.patch
index 0369dbaa73..1bc1cdaf85 100644
--- a/patches/server/0977-Fix-equipment-slot-and-group-API.patch
+++ b/patches/server/0977-Fix-equipment-slot-and-group-API.patch
@@ -32,7 +32,7 @@ index 9d74577af071954e1e37201a96368c1360076209..eafa54c870c3e2aef30c3f9f96f51660
throw new IllegalArgumentException("Not implemented. This is a bug");
}
diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
-index d9d4b983672b4340df926d98a7fa6708acad8a05..7c6d4c08cf07da18b414773dfed5704926938bea 100644
+index 62d615a4c7acfbd970278531e1bbceb71591f7b1..4d97024bb05ab815409fc25c5924903868cc3945 100644
--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
@@ -1452,7 +1452,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
diff --git a/patches/server/1024-Proxy-ItemStack-to-CraftItemStack.patch b/patches/server/1024-Proxy-ItemStack-to-CraftItemStack.patch
new file mode 100644
index 0000000000..aba1d9b341
--- /dev/null
+++ b/patches/server/1024-Proxy-ItemStack-to-CraftItemStack.patch
@@ -0,0 +1,300 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <[email protected]>
+Date: Tue, 14 May 2024 11:57:43 -0700
+Subject: [PATCH] Proxy ItemStack to CraftItemStack
+
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
+index be36336a3c7d1ae88277f4ee1be70075001de7a7..814e8ece6821e359b504e8c4d140cc38700f2abe 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
+@@ -25,15 +25,57 @@ import org.bukkit.material.MaterialData;
+ @DelegateDeserialization(ItemStack.class)
+ public final class CraftItemStack extends ItemStack {
+
+- // Paper start - MC Utils
+- public static net.minecraft.world.item.ItemStack unwrap(ItemStack bukkit) {
+- if (bukkit instanceof CraftItemStack craftItemStack) {
+- return craftItemStack.handle != null ? craftItemStack.handle : net.minecraft.world.item.ItemStack.EMPTY;
++ // Paper start - delegate api-ItemStack to CraftItemStack
++ private static final java.lang.invoke.VarHandle API_ITEM_STACK_CRAFT_DELEGATE_FIELD;
++ static {
++ try {
++ API_ITEM_STACK_CRAFT_DELEGATE_FIELD = java.lang.invoke.MethodHandles.privateLookupIn(
++ ItemStack.class,
++ java.lang.invoke.MethodHandles.lookup()
++ ).findVarHandle(ItemStack.class, "craftDelegate", ItemStack.class);
++ } catch (final IllegalAccessException | NoSuchFieldException exception) {
++ throw new RuntimeException(exception);
++ }
++ }
++
++ private static CraftItemStack getCraftStack(final ItemStack bukkit) {
++ if (bukkit instanceof final CraftItemStack craftItemStack) {
++ return craftItemStack;
+ } else {
+- return asNMSCopy(bukkit);
++ return (CraftItemStack) API_ITEM_STACK_CRAFT_DELEGATE_FIELD.get(bukkit);
+ }
+ }
+
++ @Override
++ public int hashCode() {
++ if (this.handle == null || this.handle.isEmpty()) {
++ return net.minecraft.world.item.ItemStack.EMPTY.hashCode();
++ } else {
++ int hash = net.minecraft.world.item.ItemStack.hashItemAndComponents(this.handle);
++ hash = hash * 31 + this.handle.getCount();
++ return hash;
++ }
++ }
++
++ @Override
++ public boolean equals(final Object obj) {
++ if (!(obj instanceof final org.bukkit.inventory.ItemStack bukkit)) return false;
++ final CraftItemStack craftStack = getCraftStack(bukkit);
++ if (this.handle == craftStack.handle) return true;
++ else if (this.handle == null || craftStack.handle == null) return false;
++ else if (this.handle.isEmpty() && craftStack.handle.isEmpty()) return true;
++ else return net.minecraft.world.item.ItemStack.matches(this.handle, craftStack.handle);
++ }
++ // Paper end
++
++ // Paper start - MC Utils
++ public static net.minecraft.world.item.ItemStack unwrap(ItemStack bukkit) {
++ // Paper start - re-implement after delegating all api ItemStack calls to CraftItemStack
++ final CraftItemStack craftItemStack = getCraftStack(bukkit);
++ return craftItemStack.handle == null ? net.minecraft.world.item.ItemStack.EMPTY : craftItemStack.handle;
++ // Paper end - re-implement after delegating all api ItemStack calls to CraftItemStack
++ }
++
+ public static net.minecraft.world.item.ItemStack getOrCloneOnMutation(ItemStack old, ItemStack newInstance) {
+ return old == newInstance ? unwrap(old) : asNMSCopy(newInstance);
+ }
+@@ -47,25 +89,13 @@ public final class CraftItemStack extends ItemStack {
+ // Paper end - override isEmpty to use vanilla's impl
+
+ public static net.minecraft.world.item.ItemStack asNMSCopy(ItemStack original) {
+- if (original instanceof CraftItemStack) {
+- CraftItemStack stack = (CraftItemStack) original;
+- return stack.handle == null ? net.minecraft.world.item.ItemStack.EMPTY : stack.handle.copy();
+- }
+- if (original == null || original.isEmpty()) { // Paper - override isEmpty to use vanilla's impl; use isEmpty
++ // Paper start - re-implement after delegating all api ItemStack calls to CraftItemStack
++ if (original == null || original.isEmpty()) {
+ return net.minecraft.world.item.ItemStack.EMPTY;
+ }
+-
+- Item item = CraftItemType.bukkitToMinecraft(original.getType());
+-
+- if (item == null) {
+- return net.minecraft.world.item.ItemStack.EMPTY;
+- }
+-
+- net.minecraft.world.item.ItemStack stack = new net.minecraft.world.item.ItemStack(item, original.getAmount());
+- if (original.hasItemMeta()) {
+- CraftItemStack.setItemMeta(stack, original.getItemMeta());
+- }
+- return stack;
++ final CraftItemStack stack = getCraftStack(original);
++ return stack.handle == null ? net.minecraft.world.item.ItemStack.EMPTY : stack.handle.copy();
++ // Paper end - re-implement after delegating all api ItemStack calls to CraftItemStack
+ }
+
+ // Paper start
+@@ -88,14 +118,10 @@ public final class CraftItemStack extends ItemStack {
+ * Copies the NMS stack to return as a strictly-Bukkit stack
+ */
+ public static ItemStack asBukkitCopy(net.minecraft.world.item.ItemStack original) {
+- if (original.isEmpty()) {
+- return new ItemStack(Material.AIR);
+- }
+- ItemStack stack = new ItemStack(CraftItemType.minecraftToBukkit(original.getItem()), original.getCount());
+- if (CraftItemStack.hasItemMeta(original)) {
+- stack.setItemMeta(CraftItemStack.getItemMeta(original));
+- }
+- return stack;
++ // Paper start - no such thing as a "strictly-Bukkit stack" anymore
++ // we copy the stack since it should be a complete copy not a mirror
++ return asCraftMirror(original.copy());
++ // Paper end
+ }
+
+ public static CraftItemStack asCraftMirror(net.minecraft.world.item.ItemStack original) {
+@@ -313,11 +339,7 @@ public final class CraftItemStack extends ItemStack {
+
+ @Override
+ public CraftItemStack clone() {
+- CraftItemStack itemStack = (CraftItemStack) super.clone();
+- if (this.handle != null) {
+- itemStack.handle = this.handle.copy();
+- }
+- return itemStack;
++ return new org.bukkit.craftbukkit.inventory.CraftItemStack(this.handle != null ? this.handle.copy() : null); // Paper
+ }
+
+ @Override
+@@ -420,22 +442,14 @@ public final class CraftItemStack extends ItemStack {
+ if (stack == this) {
+ return true;
+ }
+- if (!(stack instanceof CraftItemStack)) {
+- return stack.getClass() == ItemStack.class && stack.isSimilar(this);
+- }
+-
+- CraftItemStack that = (CraftItemStack) stack;
++ final CraftItemStack that = getCraftStack(stack); // Paper - re-implement after delegating all api ItemStack calls to CraftItemStack
+ if (this.handle == that.handle) {
+ return true;
+ }
+ if (this.handle == null || that.handle == null) {
+ return false;
+ }
+- Material comparisonType = CraftLegacy.fromLegacy(that.getType()); // This may be called from legacy item stacks, try to get the right material
+- if (!(comparisonType == this.getType() && this.getDurability() == that.getDurability())) {
+- return false;
+- }
+- return this.hasItemMeta() ? that.hasItemMeta() && this.handle.getComponents().equals(that.handle.getComponents()) : !that.hasItemMeta();
++ return net.minecraft.world.item.ItemStack.isSameItemSameComponents(this.handle, that.handle); // Paper - re-implement after delegating all api ItemStack calls to CraftItemStack
+ }
+
+ @Override
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java
+index 9d2feff3df87cfeefe7105d954854af05cef6f69..07539ebfefa3352de5ee7a17f2724cf2c979f399 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java
+@@ -100,13 +100,14 @@ public class CraftItemType<M extends ItemMeta> implements ItemType.Typed<M>, Han
+ @NotNull
+ @Override
+ public ItemStack createItemStack(final int amount, @Nullable final Consumer<? super M> metaConfigurator) {
+- final ItemStack itemStack = new ItemStack(this.asMaterial(), amount);
++ // Paper start - re-implement to return CraftItemStack
++ final net.minecraft.world.item.ItemStack stack = new net.minecraft.world.item.ItemStack(this.item, amount);
++ final CraftItemStack mirror = CraftItemStack.asCraftMirror(stack);
+ if (metaConfigurator != null) {
+- final ItemMeta itemMeta = itemStack.getItemMeta();
+- metaConfigurator.accept((M) itemMeta);
+- itemStack.setItemMeta(itemMeta);
++ mirror.editMeta(this.getItemMetaClass(), metaConfigurator);
+ }
+- return itemStack;
++ return mirror;
++ // Paper start - reimplement to return CraftItemStack
+ }
+
+ @Override
+diff --git a/src/main/java/org/bukkit/craftbukkit/legacy/MaterialRerouting.java b/src/main/java/org/bukkit/craftbukkit/legacy/MaterialRerouting.java
+index 9c004e7cb46841d874ab997bf2e3b63ae763aec7..d7c8f26b21276d9ff1d5c7c9738cc1126ce7d4b9 100644
+--- a/src/main/java/org/bukkit/craftbukkit/legacy/MaterialRerouting.java
++++ b/src/main/java/org/bukkit/craftbukkit/legacy/MaterialRerouting.java
+@@ -678,4 +678,16 @@ public class MaterialRerouting {
+ return itemStack.withType(material);
+ }
+ // Paper end - register paper API specific material consumers in rerouting
++
++ // Paper start - methods added post 1.13, no-op
++ @RerouteStatic("org/bukkit/inventory/ItemStack")
++ public static ItemStack of(final Material material) {
++ return ItemStack.of(material);
++ }
++
++ @RerouteStatic("org/bukkit/inventory/ItemStack")
++ public static ItemStack of(final Material material, final int amount) {
++ return ItemStack.of(material, amount);
++ }
++ // Paper end
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+index 8dba6c4a2e1f305cf576e8bfdca5d0c07ab871ae..d70c5546c8bd6f364fad9b24880b6867efdab644 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+@@ -700,6 +700,13 @@ public final class CraftMagicNumbers implements UnsafeValues {
+ }
+ // Paper end - hack to get tags for non server-backed registries
+
++ // Paper start - proxy ItemStack
++ @Override
++ public org.bukkit.inventory.ItemStack createEmptyStack() {
++ return CraftItemStack.asCraftMirror(null);
++ }
++ // Paper end - proxy ItemStack
++
+ /**
+ * This helper class represents the different NBT Tags.
+ * <p>
+diff --git a/src/test/java/io/papermc/paper/configuration/ConfigurationSectionTest.java b/src/test/java/io/papermc/paper/configuration/ConfigurationSectionTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..e5c2fb160e9d390cdfa0259a3feb9f488b2dc14d
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/configuration/ConfigurationSectionTest.java
+@@ -0,0 +1,53 @@
++package io.papermc.paper.configuration;
++
++import org.bukkit.Material;
++import org.bukkit.configuration.ConfigurationSection;
++import org.bukkit.inventory.ItemStack;
++import org.bukkit.support.AbstractTestingBase;
++import org.junit.jupiter.api.Test;
++
++import static org.junit.jupiter.api.Assertions.assertEquals;
++import static org.junit.jupiter.api.Assertions.assertFalse;
++import static org.junit.jupiter.api.Assertions.assertNull;
++import static org.junit.jupiter.api.Assertions.assertTrue;
++
++public abstract class ConfigurationSectionTest extends AbstractTestingBase {
++ public abstract ConfigurationSection getConfigurationSection();
++
++ @Test
++ public void testGetItemStack_String() {
++ ConfigurationSection section = getConfigurationSection();
++ String key = "exists";
++ ItemStack value = new ItemStack(Material.ACACIA_WOOD, 50);
++
++ section.set(key, value);
++
++ assertEquals(value, section.getItemStack(key));
++ assertNull(section.getString("doesntExist"));
++ }
++
++ @Test
++ public void testGetItemStack_String_ItemStack() {
++ ConfigurationSection section = getConfigurationSection();
++ String key = "exists";
++ ItemStack value = new ItemStack(Material.ACACIA_WOOD, 50);
++ ItemStack def = new ItemStack(Material.STONE, 1);
++
++ section.set(key, value);
++
++ assertEquals(value, section.getItemStack(key, def));
++ assertEquals(def, section.getItemStack("doesntExist", def));
++ }
++
++ @Test
++ public void testIsItemStack() {
++ ConfigurationSection section = getConfigurationSection();
++ String key = "exists";
++ ItemStack value = new ItemStack(Material.ACACIA_WOOD, 50);
++
++ section.set(key, value);
++
++ assertTrue(section.isItemStack(key));
++ assertFalse(section.isItemStack("doesntExist"));
++ }
++}
+diff --git a/src/test/java/io/papermc/paper/configuration/MemorySectionTest.java b/src/test/java/io/papermc/paper/configuration/MemorySectionTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..def33c36f207a4c5306b5a895336aa70335c1678
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/configuration/MemorySectionTest.java
+@@ -0,0 +1,11 @@
++package io.papermc.paper.configuration;
++
++import org.bukkit.configuration.ConfigurationSection;
++import org.bukkit.configuration.MemoryConfiguration;
++
++public class MemorySectionTest extends ConfigurationSectionTest {
++ @Override
++ public ConfigurationSection getConfigurationSection() {
++ return new MemoryConfiguration().createSection("section");
++ }
++}
diff --git a/patches/server/1025-Make-a-PDC-view-accessible-directly-from-ItemStack.patch b/patches/server/1025-Make-a-PDC-view-accessible-directly-from-ItemStack.patch
new file mode 100644
index 0000000000..95338b76bf
--- /dev/null
+++ b/patches/server/1025-Make-a-PDC-view-accessible-directly-from-ItemStack.patch
@@ -0,0 +1,266 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <[email protected]>
+Date: Wed, 12 Jun 2024 10:29:40 -0700
+Subject: [PATCH] Make a PDC view accessible directly from ItemStack
+
+
+diff --git a/src/main/java/io/papermc/paper/persistence/PaperPersistentDataContainerView.java b/src/main/java/io/papermc/paper/persistence/PaperPersistentDataContainerView.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..fac401280d3f3689b00e16c19155ca753faa06e0
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/persistence/PaperPersistentDataContainerView.java
+@@ -0,0 +1,86 @@
++package io.papermc.paper.persistence;
++
++import com.google.common.base.Preconditions;
++import java.io.ByteArrayOutputStream;
++import java.io.DataOutputStream;
++import java.io.IOException;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.nbt.NbtIo;
++import net.minecraft.nbt.Tag;
++import org.bukkit.NamespacedKey;
++import org.bukkit.craftbukkit.persistence.CraftPersistentDataAdapterContext;
++import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry;
++import org.bukkit.persistence.PersistentDataAdapterContext;
++import org.bukkit.persistence.PersistentDataType;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public abstract class PaperPersistentDataContainerView implements PersistentDataContainerView {
++
++ protected final CraftPersistentDataTypeRegistry registry;
++ protected final CraftPersistentDataAdapterContext adapterContext;
++
++ public PaperPersistentDataContainerView(final CraftPersistentDataTypeRegistry registry) {
++ this.registry = registry;
++ this.adapterContext = new CraftPersistentDataAdapterContext(this.registry);
++ }
++
++ public abstract @Nullable Tag getTag(final String key);
++
++ public abstract CompoundTag toTagCompound();
++
++ @Override
++ public <P, C> boolean has(final NamespacedKey key, final PersistentDataType<P, C> type) {
++ Preconditions.checkArgument(key != null, "The NamespacedKey key cannot be null");
++ Preconditions.checkArgument(type != null, "The provided type cannot be null");
++
++ final @Nullable Tag value = this.getTag(key.toString());
++ if (value == null) {
++ return false;
++ }
++
++ return this.registry.isInstanceOf(type, value);
++ }
++
++ @Override
++ public boolean has(final NamespacedKey key) {
++ Preconditions.checkArgument(key != null, "The provided key for the custom value was null"); // Paper
++ return this.getTag(key.toString()) != null;
++ }
++
++ @Override
++ public <P, C> @Nullable C get(final NamespacedKey key, final PersistentDataType<P, C> type) {
++ Preconditions.checkArgument(key != null, "The NamespacedKey key cannot be null");
++ Preconditions.checkArgument(type != null, "The provided type cannot be null");
++
++ final @Nullable Tag value = this.getTag(key.toString());
++ if (value == null) {
++ return null;
++ }
++
++ return type.fromPrimitive(this.registry.extract(type, value), this.adapterContext);
++ }
++
++ @Override
++ public <P, C> C getOrDefault(final NamespacedKey key, final PersistentDataType<P, C> type, final C defaultValue) {
++ final C c = this.get(key, type);
++ return c != null ? c : defaultValue;
++ }
++
++ @Override
++ public PersistentDataAdapterContext getAdapterContext() {
++ return this.adapterContext;
++ }
++
++ @Override
++ public byte[] serializeToBytes() throws IOException {
++ final net.minecraft.nbt.CompoundTag root = this.toTagCompound();
++ final ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream();
++ try (final DataOutputStream dataOutput = new DataOutputStream(byteArrayOutput)) {
++ NbtIo.write(root, dataOutput);
++ return byteArrayOutput.toByteArray();
++ }
++ }
++}
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
+index 814e8ece6821e359b504e8c4d140cc38700f2abe..32a41c8b324aad67b9dcf74387aef299e6478a64 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
+@@ -480,4 +480,63 @@ public final class CraftItemStack extends ItemStack {
+ return mirrored;
+ }
+ // Paper end
++
++ // Paper start - pdc
++ private net.minecraft.nbt.CompoundTag getPdcTag() {
++ if (this.handle == null) {
++ return new net.minecraft.nbt.CompoundTag();
++ }
++ final net.minecraft.world.item.component.CustomData customData = this.handle.getOrDefault(DataComponents.CUSTOM_DATA, net.minecraft.world.item.component.CustomData.EMPTY);
++ // getUnsafe is OK here because we are only ever *reading* the data so immutability is preserved
++ //noinspection deprecation
++ return customData.getUnsafe().getCompound("PublicBukkitValues");
++ }
++
++ private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry();
++ private final io.papermc.paper.persistence.PaperPersistentDataContainerView pdcView = new io.papermc.paper.persistence.PaperPersistentDataContainerView(REGISTRY) {
++
++ @Override
++ public net.minecraft.nbt.CompoundTag toTagCompound() {
++ return CraftItemStack.this.getPdcTag();
++ }
++
++ @Override
++ public net.minecraft.nbt.Tag getTag(final String key) {
++ return CraftItemStack.this.getPdcTag().get(key);
++ }
++
++ @Override
++ public java.util.Set<org.bukkit.NamespacedKey> getKeys() {
++ java.util.Set<org.bukkit.NamespacedKey> keys = new java.util.HashSet<>();
++ CraftItemStack.this.getPdcTag().getAllKeys().forEach(key -> {
++ final String[] keyData = key.split(":", 2);
++ if (keyData.length == 2) {
++ keys.add(new org.bukkit.NamespacedKey(keyData[0], keyData[1]));
++ }
++ });
++ return java.util.Collections.unmodifiableSet(keys);
++ };
++
++ @Override
++ public boolean isEmpty() {
++ return CraftItemStack.this.getPdcTag().isEmpty();
++ }
++
++ @Override
++ public void copyTo(final org.bukkit.persistence.PersistentDataContainer other, final boolean replace) {
++ Preconditions.checkArgument(other != null, "The target container cannot be null");
++ final org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer target = (org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer) other;
++ final net.minecraft.nbt.CompoundTag pdcTag = org.bukkit.craftbukkit.inventory.CraftItemStack.this.getPdcTag();
++ for (final String key : pdcTag.getAllKeys()) {
++ if (replace || !target.getRaw().containsKey(key)) {
++ target.getRaw().put(key, pdcTag.get(key).copy());
++ }
++ }
++ }
++ };
++ @Override
++ public io.papermc.paper.persistence.PersistentDataContainerView getPersistentDataContainer() {
++ return this.pdcView;
++ }
++ // Paper end - pdc
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java b/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java
+index f55fdd57ced259ad5a95878840e98ffaa3db2e05..9d867f1659433ea15f281c8b441db7e339013100 100644
+--- a/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java
++++ b/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java
+@@ -16,11 +16,10 @@ import org.bukkit.persistence.PersistentDataContainer;
+ import org.bukkit.persistence.PersistentDataType;
+ import org.jetbrains.annotations.NotNull;
+
+-public class CraftPersistentDataContainer implements PersistentDataContainer {
++public class CraftPersistentDataContainer extends io.papermc.paper.persistence.PaperPersistentDataContainerView implements PersistentDataContainer { // Paper - split up view and mutable
+
+ private final Map<String, Tag> customDataTags = new HashMap<>();
+- private final CraftPersistentDataTypeRegistry registry;
+- private final CraftPersistentDataAdapterContext adapterContext;
++ // Paper - move to PersistentDataContainerView
+
+ public CraftPersistentDataContainer(Map<String, Tag> customTags, CraftPersistentDataTypeRegistry registry) {
+ this(registry);
+@@ -28,10 +27,15 @@ public class CraftPersistentDataContainer implements PersistentDataContainer {
+ }
+
+ public CraftPersistentDataContainer(CraftPersistentDataTypeRegistry registry) {
+- this.registry = registry;
+- this.adapterContext = new CraftPersistentDataAdapterContext(this.registry);
++ super(registry); // Paper - move to PersistentDataContainerView
+ }
+
++ // Paper start
++ @Override
++ public Tag getTag(final String key) {
++ return this.customDataTags.get(key);
++ }
++ // Paper end
+
+ @Override
+ public <T, Z> void set(@NotNull NamespacedKey key, @NotNull PersistentDataType<T, Z> type, @NotNull Z value) {
+@@ -42,44 +46,7 @@ public class CraftPersistentDataContainer implements PersistentDataContainer {
+ this.customDataTags.put(key.toString(), this.registry.wrap(type, type.toPrimitive(value, this.adapterContext)));
+ }
+
+- @Override
+- public <T, Z> boolean has(@NotNull NamespacedKey key, @NotNull PersistentDataType<T, Z> type) {
+- Preconditions.checkArgument(key != null, "The NamespacedKey key cannot be null");
+- Preconditions.checkArgument(type != null, "The provided type cannot be null");
+-
+- Tag value = this.customDataTags.get(key.toString());
+- if (value == null) {
+- return false;
+- }
+-
+- return this.registry.isInstanceOf(type, value);
+- }
+-
+- @Override
+- public boolean has(NamespacedKey key) {
+- Preconditions.checkArgument(key != null, "The provided key for the custom value was null"); // Paper
+- return this.customDataTags.get(key.toString()) != null;
+- }
+-
+- @Override
+- public <T, Z> Z get(@NotNull NamespacedKey key, @NotNull PersistentDataType<T, Z> type) {
+- Preconditions.checkArgument(key != null, "The NamespacedKey key cannot be null");
+- Preconditions.checkArgument(type != null, "The provided type cannot be null");
+-
+- Tag value = this.customDataTags.get(key.toString());
+- if (value == null) {
+- return null;
+- }
+-
+- return type.fromPrimitive(this.registry.extract(type, value), this.adapterContext);
+- }
+-
+- @NotNull
+- @Override
+- public <T, Z> Z getOrDefault(@NotNull NamespacedKey key, @NotNull PersistentDataType<T, Z> type, @NotNull Z defaultValue) {
+- Z z = this.get(key, type);
+- return z != null ? z : defaultValue;
+- }
++ // Paper - move to PersistentDataContainerView
+
+ @NotNull
+ @Override
+@@ -186,16 +153,7 @@ public class CraftPersistentDataContainer implements PersistentDataContainer {
+ // Paper end
+
+ // Paper start - byte array serialization
+- @Override
+- public byte[] serializeToBytes() throws java.io.IOException {
+- final net.minecraft.nbt.CompoundTag root = this.toTagCompound();
+- final java.io.ByteArrayOutputStream byteArrayOutput = new java.io.ByteArrayOutputStream();
+- try (final java.io.DataOutputStream dataOutput = new java.io.DataOutputStream(byteArrayOutput)) {
+- net.minecraft.nbt.NbtIo.write(root, dataOutput);
+- return byteArrayOutput.toByteArray();
+- }
+- }
+-
++ // Paper - move to PersistentDataContainerView
+ @Override
+ public void readFromBytes(final byte[] bytes, final boolean clear) throws java.io.IOException {
+ if (clear) {