diff --git a/patches/api/General-ItemMeta-fixes.patch b/patches/api/General-ItemMeta-fixes.patch
index c9676b03fc..dbb6f9c61f 100644
--- a/patches/api/General-ItemMeta-fixes.patch
+++ b/patches/api/General-ItemMeta-fixes.patch
@@ -19,3 +19,55 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/inventory/meta/Damageable.java
++++ b/src/main/java/org/bukkit/inventory/meta/Damageable.java
+@@ -0,0 +0,0 @@ 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
+      */
+@@ -0,0 +0,0 @@ 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/Make-a-PDC-view-accessible-directly-from-ItemStack.patch b/patches/api/Make-a-PDC-view-accessible-directly-from-ItemStack.patch
new file mode 100644
index 0000000000..dc92802e3b
--- /dev/null
+++ b/patches/api/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 <jake.m.potrebic@gmail.com>
+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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/persistence/PersistentDataContainerView.java
+@@ -0,0 +0,0 @@
++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
++ */
++@ApiStatus.NonExtendable
++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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/persistence/PersistentDataViewHolder.java
+@@ -0,0 +0,0 @@
++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.
++ */
++@ApiStatus.NonExtendable
++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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/inventory/ItemStack.java
++++ b/src/main/java/org/bukkit/inventory/ItemStack.java
+@@ -0,0 +0,0 @@ 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;
+ 
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/persistence/PersistentDataContainer.java
++++ b/src/main/java/org/bukkit/persistence/PersistentDataContainer.java
+@@ -0,0 +0,0 @@ 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.
+@@ -0,0 +0,0 @@ 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.
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/persistence/PersistentDataHolder.java
++++ b/src/main/java/org/bukkit/persistence/PersistentDataHolder.java
+@@ -0,0 +0,0 @@ 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/api/Proxy-ItemStack-to-CraftItemStack.patch b/patches/api/Proxy-ItemStack-to-CraftItemStack.patch
new file mode 100644
index 0000000000..2f0df13ca8
--- /dev/null
+++ b/patches/api/Proxy-ItemStack-to-CraftItemStack.patch
@@ -0,0 +1,472 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <jake.m.potrebic@gmail.com>
+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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/UnsafeValues.java
++++ b/src/main/java/org/bukkit/UnsafeValues.java
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/inventory/ItemStack.java
++++ b/src/main/java/org/bukkit/inventory/ItemStack.java
+@@ -0,0 +0,0 @@ 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() {}
+@@ -0,0 +0,0 @@ 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);
+     }
+@@ -0,0 +0,0 @@ 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);
+     }
+@@ -0,0 +0,0 @@ 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);
+         }
+@@ -0,0 +0,0 @@ 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);
+-        }
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ 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
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ 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
+     /**
+@@ -0,0 +0,0 @@ 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
+ 
+@@ -0,0 +0,0 @@ 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
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ 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
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ 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
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ 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
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ 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
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ 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
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ 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
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ 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
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ 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
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ 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
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat
+      */
+     @NotNull
+     public static ItemStack empty() {
+-        return new ItemStack();
++        //noinspection deprecation
++        return Bukkit.getUnsafe().createEmptyStack(); // Paper - proxy ItemStack
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/test/java/org/bukkit/configuration/ConfigurationSectionTest.java
++++ b/src/test/java/org/bukkit/configuration/ConfigurationSectionTest.java
+@@ -0,0 +0,0 @@ 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";
+@@ -0,0 +0,0 @@ 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";
+@@ -0,0 +0,0 @@ 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/server/General-ItemMeta-fixes.patch b/patches/server/General-ItemMeta-fixes.patch
index 29feea2920..bf6e3c7701 100644
--- a/patches/server/General-ItemMeta-fixes.patch
+++ b/patches/server/General-ItemMeta-fixes.patch
@@ -846,6 +846,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
          <T> Applicator put(ItemMetaKeyType<T> key, T value) {
              this.builder.set(key.TYPE, value);
+@@ -0,0 +0,0 @@ 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();
 @@ -0,0 +0,0 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
              this.enchantments = new EnchantmentMap(meta.enchantments); // Paper
          }
@@ -876,6 +885,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          }
  
          for (Object obj : mods.keySet()) {
+@@ -0,0 +0,0 @@ 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);
+         }
+ 
 @@ -0,0 +0,0 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
      }
  
@@ -889,6 +907,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              return;
          }
  
+@@ -0,0 +0,0 @@ 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
 @@ -0,0 +0,0 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
  
      @Override
@@ -1037,6 +1064,18 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
                  return false;
 @@ -0,0 +0,0 @@ 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) {
 +        Preconditions.checkArgument(damage >= 0, "Damage cannot be negative"); // Paper
@@ -1044,6 +1083,21 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          this.damage = damage;
      }
  
++    // 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;
 @@ -0,0 +0,0 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
  
      @Override
@@ -1062,11 +1116,22 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
                  && (this.removedTags.equals(that.removedTags))
                  && (Objects.equals(this.customTag, that.customTag))
 @@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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
@@ -1081,6 +1146,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
                  clone.attributeModifiers = LinkedHashMultimap.create(this.attributeModifiers);
              }
              if (this.customTag != null) {
+@@ -0,0 +0,0 @@ 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);
+         }
+ 
 @@ -0,0 +0,0 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
      }
  
diff --git a/patches/server/ItemStack-repair-check-API.patch b/patches/server/ItemStack-repair-check-API.patch
index 2873b560cb..a70b46274c 100644
--- a/patches/server/ItemStack-repair-check-API.patch
+++ b/patches/server/ItemStack-repair-check-API.patch
@@ -69,11 +69,4 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        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/Make-a-PDC-view-accessible-directly-from-ItemStack.patch b/patches/server/Make-a-PDC-view-accessible-directly-from-ItemStack.patch
new file mode 100644
index 0000000000..cc96816461
--- /dev/null
+++ b/patches/server/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 <jake.m.potrebic@gmail.com>
+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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/persistence/PaperPersistentDataContainerView.java
+@@ -0,0 +0,0 @@
++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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java
++++ b/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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) {
diff --git a/patches/server/Proxy-ItemStack-to-CraftItemStack.patch b/patches/server/Proxy-ItemStack-to-CraftItemStack.patch
new file mode 100644
index 0000000000..bebcd41634
--- /dev/null
+++ b/patches/server/Proxy-ItemStack-to-CraftItemStack.patch
@@ -0,0 +1,300 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <jake.m.potrebic@gmail.com>
+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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
+@@ -0,0 +0,0 @@ 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);
+     }
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/legacy/MaterialRerouting.java
++++ b/src/main/java/org/bukkit/craftbukkit/legacy/MaterialRerouting.java
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+@@ -0,0 +0,0 @@ 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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/configuration/ConfigurationSectionTest.java
+@@ -0,0 +0,0 @@
++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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/configuration/MemorySectionTest.java
+@@ -0,0 +0,0 @@
++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");
++    }
++}