diff --git a/patches/api/0495-DataComponent-API.patch b/patches/api/0495-DataComponent-API.patch new file mode 100644 index 0000000000..9284c1e00b --- /dev/null +++ b/patches/api/0495-DataComponent-API.patch @@ -0,0 +1,4186 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> +Date: Sun, 28 Apr 2024 19:53:06 -0400 +Subject: [PATCH] DataComponent API + +Exposes the data component logic used by vanilla ItemStack to API +consumers as a version-specific API. +The types and methods introduced by this patch do not follow the general +API deprecation contracts and will be adapted to each new minecraft +release without backwards compatibility measures. + +diff --git a/src/main/java/io/papermc/paper/block/BlockPredicate.java b/src/main/java/io/papermc/paper/block/BlockPredicate.java +new file mode 100644 +index 0000000000000000000000000000000000000000..92ea82ee95c449916955631297a059f1b9198c9b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/BlockPredicate.java +@@ -0,0 +1,50 @@ ++package io.papermc.paper.block; ++ ++import io.papermc.paper.registry.set.RegistryKeySet; ++import org.bukkit.block.BlockType; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++import org.jspecify.annotations.Nullable; ++ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface BlockPredicate { ++ ++ static Builder predicate() { ++ // ++ record BlockPredicateImpl(@Nullable RegistryKeySet blocks) implements BlockPredicate { ++ } ++ ++ class BuilderImpl implements Builder { ++ ++ private @Nullable RegistryKeySet blocks; ++ ++ @Override ++ public Builder blocks(final @Nullable RegistryKeySet blocks) { ++ this.blocks = blocks; ++ return this; ++ } ++ ++ @Override ++ public BlockPredicate build() { ++ return new BlockPredicateImpl(this.blocks); ++ } ++ } ++ // ++ return new BuilderImpl(); ++ } ++ ++ @Nullable RegistryKeySet blocks(); ++ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder { ++ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder blocks(@Nullable RegistryKeySet blocks); ++ ++ BlockPredicate build(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/BuildableDataComponent.java b/src/main/java/io/papermc/paper/datacomponent/BuildableDataComponent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4d2ee71b82ff4a66c7f84e73c028f146e0f851ad +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/BuildableDataComponent.java +@@ -0,0 +1,19 @@ ++package io.papermc.paper.datacomponent; ++ ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface BuildableDataComponent, B extends DataComponentBuilder> { ++ ++ /** ++ * Creates a new builder from this data component. ++ * ++ * @return a new builder ++ */ ++ @Contract(value = "-> new", pure = true) ++ B toBuilder(); ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/DataComponentBuilder.java b/src/main/java/io/papermc/paper/datacomponent/DataComponentBuilder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9365e57499c8e337a40835b2ec9a92ebe4391bfc +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/DataComponentBuilder.java +@@ -0,0 +1,24 @@ ++package io.papermc.paper.datacomponent; ++ ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Base builder type for all component builders. ++ * ++ * @param built component type ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface DataComponentBuilder { ++ ++ /** ++ * Builds the immutable component value. ++ * ++ * @return a new component value ++ */ ++ @Contract(value = "-> new", pure = true) ++ C build(); ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/DataComponentType.java b/src/main/java/io/papermc/paper/datacomponent/DataComponentType.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e2266d86a4dd1bf20346e48c428f8baf8a84b76b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/DataComponentType.java +@@ -0,0 +1,30 @@ ++package io.papermc.paper.datacomponent; ++ ++import org.bukkit.Keyed; ++import org.jetbrains.annotations.ApiStatus; ++import org.jspecify.annotations.NullMarked; ++ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface DataComponentType extends Keyed { ++ ++ /** ++ * Checks if this data component type is persistent, or ++ * that it will be saved with any itemstack it's attached to. ++ * ++ * @return {@code true} if persistent, {@code false} otherwise ++ */ ++ boolean isPersistent(); ++ ++ @SuppressWarnings("unused") ++ @ApiStatus.NonExtendable ++ interface Valued extends DataComponentType { ++ ++ } ++ ++ @ApiStatus.NonExtendable ++ interface NonValued extends DataComponentType { ++ ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/DataComponentTypes.java b/src/main/java/io/papermc/paper/datacomponent/DataComponentTypes.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e79737ae012179fc7c89b14af8801b8b09fa042b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/DataComponentTypes.java +@@ -0,0 +1,344 @@ ++package io.papermc.paper.datacomponent; ++ ++import io.papermc.paper.datacomponent.item.BannerPatternLayers; ++import io.papermc.paper.datacomponent.item.BlockItemDataProperties; ++import io.papermc.paper.datacomponent.item.BundleContents; ++import io.papermc.paper.datacomponent.item.ChargedProjectiles; ++import io.papermc.paper.datacomponent.item.Consumable; ++import io.papermc.paper.datacomponent.item.CustomModelData; ++import io.papermc.paper.datacomponent.item.DamageResistant; ++import io.papermc.paper.datacomponent.item.DeathProtection; ++import io.papermc.paper.datacomponent.item.DyedItemColor; ++import io.papermc.paper.datacomponent.item.Enchantable; ++import io.papermc.paper.datacomponent.item.Equippable; ++import io.papermc.paper.datacomponent.item.Fireworks; ++import io.papermc.paper.datacomponent.item.FoodProperties; ++import io.papermc.paper.datacomponent.item.ItemAdventurePredicate; ++import io.papermc.paper.datacomponent.item.ItemArmorTrim; ++import io.papermc.paper.datacomponent.item.ItemAttributeModifiers; ++import io.papermc.paper.datacomponent.item.ItemContainerContents; ++import io.papermc.paper.datacomponent.item.ItemEnchantments; ++import io.papermc.paper.datacomponent.item.ItemLore; ++import io.papermc.paper.datacomponent.item.JukeboxPlayable; ++import io.papermc.paper.datacomponent.item.LodestoneTracker; ++import io.papermc.paper.datacomponent.item.MapDecorations; ++import io.papermc.paper.datacomponent.item.MapId; ++import io.papermc.paper.datacomponent.item.MapItemColor; ++import io.papermc.paper.datacomponent.item.OminousBottleAmplifier; ++import io.papermc.paper.datacomponent.item.PotDecorations; ++import io.papermc.paper.datacomponent.item.PotionContents; ++import io.papermc.paper.datacomponent.item.Repairable; ++import io.papermc.paper.datacomponent.item.ResolvableProfile; ++import io.papermc.paper.datacomponent.item.SeededContainerLoot; ++import io.papermc.paper.datacomponent.item.SuspiciousStewEffects; ++import io.papermc.paper.datacomponent.item.Tool; ++import io.papermc.paper.datacomponent.item.Unbreakable; ++import io.papermc.paper.datacomponent.item.UseCooldown; ++import io.papermc.paper.datacomponent.item.UseRemainder; ++import io.papermc.paper.datacomponent.item.WritableBookContent; ++import io.papermc.paper.datacomponent.item.WrittenBookContent; ++import io.papermc.paper.item.MapPostProcessing; ++import java.util.List; ++import net.kyori.adventure.key.Key; ++import net.kyori.adventure.text.Component; ++import org.bukkit.DyeColor; ++import org.bukkit.FireworkEffect; ++import org.bukkit.MusicInstrument; ++import org.bukkit.NamespacedKey; ++import org.bukkit.Registry; ++import org.bukkit.inventory.ItemRarity; ++import org.checkerframework.checker.index.qual.NonNegative; ++import org.checkerframework.checker.index.qual.Positive; ++import org.checkerframework.common.value.qual.IntRange; ++import org.jetbrains.annotations.ApiStatus; ++import org.jspecify.annotations.NullMarked; ++ ++import static java.util.Objects.requireNonNull; ++ ++/** ++ * All the different types of data that {@link org.bukkit.inventory.ItemStack ItemStacks} ++ * and {@link org.bukkit.inventory.ItemType ItemTypes} can have. ++ */ ++@NullMarked ++@ApiStatus.Experimental ++public final class DataComponentTypes { ++ ++ /** ++ * Controls the maximum stacking size of this item. ++ *
++ * Values greater than 1 are mutually exclusive with the {@link #MAX_DAMAGE} component. ++ */ ++ public static final DataComponentType.Valued<@IntRange(from = 1, to = 99) Integer> MAX_STACK_SIZE = valued("max_stack_size"); ++ /** ++ * Controls the maximum amount of damage than an item can take, ++ * if not present, the item cannot be damaged. ++ *
++ * Mutually exclusive with the {@link #MAX_STACK_SIZE} component greater than 1. ++ * ++ * @see #DAMAGE ++ */ ++ public static final DataComponentType.Valued<@Positive Integer> MAX_DAMAGE = valued("max_damage"); ++ /** ++ * The amount of durability removed from an item, ++ * for damageable items (with the {@link #MAX_DAMAGE} component), has an implicit default value of: {@code 0}. ++ * ++ * @see #MAX_DAMAGE ++ */ ++ public static final DataComponentType.Valued<@NonNegative Integer> DAMAGE = valued("damage"); ++ /** ++ * If set, the item will not lose any durability when used. ++ */ ++ public static final DataComponentType.Valued UNBREAKABLE = valued("unbreakable"); ++ /** ++ * Custom name override for an item (as set by renaming with an Anvil). ++ * ++ * @see #ITEM_NAME ++ */ ++ public static final DataComponentType.Valued CUSTOM_NAME = valued("custom_name"); ++ /** ++ * When present, replaces default item name with contained chat component. ++ *

++ * Differences from {@link #CUSTOM_NAME}: ++ *

    ++ *
  • can't be changed or removed in Anvil
  • ++ *
  • is not styled with italics when displayed to player
  • ++ *
  • does not show labels where applicable ++ * (for example: banner markers, names in item frames)
  • ++ *
++ * ++ * @see #CUSTOM_NAME ++ */ ++ public static final DataComponentType.Valued ITEM_NAME = valued("item_name"); ++ public static final DataComponentType.Valued ITEM_MODEL = valued("item_model"); ++ /** ++ * Additional lines to include in an item's tooltip. ++ */ ++ public static final DataComponentType.Valued LORE = valued("lore"); ++ /** ++ * Controls the color of the item name. ++ */ ++ public static final DataComponentType.Valued RARITY = valued("rarity"); ++ /** ++ * Controls the enchantments on an item. ++ *
++ * If not present on a non-enchantment book, this item will not work in an anvil. ++ * ++ * @see #STORED_ENCHANTMENTS ++ */ ++ public static final DataComponentType.Valued ENCHANTMENTS = valued("enchantments"); ++ /** ++ * Controls which blocks a player in Adventure mode can place on with this item. ++ */ ++ public static final DataComponentType.Valued CAN_PLACE_ON = valued("can_place_on"); ++ /** ++ * Controls which blocks a player in Adventure mode can break with this item. ++ */ ++ public static final DataComponentType.Valued CAN_BREAK = valued("can_break"); ++ /** ++ * Holds attribute modifiers applied to any item, ++ * if not set, has an implicit default value based on the item type's ++ * default attributes (e.g. attack damage for weapons). ++ */ ++ public static final DataComponentType.Valued ATTRIBUTE_MODIFIERS = valued("attribute_modifiers"); ++ /** ++ * Controls the minecraft:custom_model_data property in the item model. ++ */ ++ public static final DataComponentType.Valued CUSTOM_MODEL_DATA = valued("custom_model_data"); ++ /** ++ * If set, disables 'additional' tooltip part which comes from the item type ++ * (e.g. content of a shulker). ++ */ ++ public static final DataComponentType.NonValued HIDE_ADDITIONAL_TOOLTIP = unvalued("hide_additional_tooltip"); ++ /** ++ * If set, it will completely hide whole item tooltip (that includes item name). ++ */ ++ public static final DataComponentType.NonValued HIDE_TOOLTIP = unvalued("hide_tooltip"); ++ /** ++ * The additional experience cost required to modify an item in an Anvil. ++ * If not present, has an implicit default value of: {@code 0}. ++ */ ++ public static final DataComponentType.Valued<@NonNegative Integer> REPAIR_COST = valued("repair_cost"); ++ /** ++ * Causes an item to not be pickable in the creative menu, currently not very useful. ++ */ ++ // public static final DataComponentType.NonValued CREATIVE_SLOT_LOCK = unvalued("creative_slot_lock"); ++ /** ++ * Overrides the enchantment glint effect on an item. ++ * If not present, default behaviour is used. ++ */ ++ public static final DataComponentType.Valued ENCHANTMENT_GLINT_OVERRIDE = valued("enchantment_glint_override"); ++ /** ++ * Marks that a projectile item would be intangible when fired ++ * (i.e. can only be picked up by a creative mode player). ++ */ ++ public static final DataComponentType.NonValued INTANGIBLE_PROJECTILE = unvalued("intangible_projectile"); ++ /** ++ * Controls potential food benefits gained when consuming the item the component is applied on. ++ * Requires the {@link #CONSUMABLE} component to allow consumption in the first place. ++ */ ++ public static final DataComponentType.Valued FOOD = valued("food"); ++ public static final DataComponentType.Valued CONSUMABLE = valued("consumable"); ++ public static final DataComponentType.Valued USE_REMAINDER = valued("use_remainder"); ++ public static final DataComponentType.Valued USE_COOLDOWN = valued("use_cooldown"); ++ /** ++ * If present, this item will not burn in fire. ++ */ ++ public static final DataComponentType.Valued DAMAGE_RESISTANT = valued("damage_resistant"); ++ /** ++ * Controls the behavior of the item as a tool. ++ */ ++ public static final DataComponentType.Valued TOOL = valued("tool"); ++ public static final DataComponentType.Valued ENCHANTABLE = valued("enchantable"); ++ public static final DataComponentType.Valued EQUIPPABLE = valued("equippable"); ++ public static final DataComponentType.Valued REPAIRABLE = valued("repairable"); ++ public static final DataComponentType.NonValued GLIDER = unvalued("glider"); ++ public static final DataComponentType.Valued TOOLTIP_STYLE = valued("tooltip_style"); ++ public static final DataComponentType.Valued DEATH_PROTECTION = valued("death_protection"); ++ /** ++ * Stores list of enchantments and their levels for an Enchanted Book. ++ * Unlike {@link #ENCHANTMENTS}, the effects provided by enchantments ++ * do not apply from this component. ++ *
++ * If not present on an Enchanted Book, it will not work in an anvil. ++ *

++ * Has an undefined behaviour if present on an item that is not an Enchanted Book ++ * (currently the presence of this component allows enchantments from {@link #ENCHANTMENTS} ++ * to be applied as if this item was an Enchanted Book). ++ * ++ * @see #ENCHANTMENTS ++ */ ++ public static final DataComponentType.Valued STORED_ENCHANTMENTS = valued("stored_enchantments"); ++ /** ++ * Represents a color applied to a dyeable item (in the {@link io.papermc.paper.registry.keys.tags.ItemTypeTagKeys#DYEABLE} item tag). ++ */ ++ public static final DataComponentType.Valued DYED_COLOR = valued("dyed_color"); ++ /** ++ * Represents the tint of the decorations on the {@link org.bukkit.inventory.ItemType#FILLED_MAP} item. ++ */ ++ public static final DataComponentType.Valued MAP_COLOR = valued("map_color"); ++ /** ++ * References the shared map state holding map contents and markers for a {@link org.bukkit.inventory.ItemType#FILLED_MAP}. ++ */ ++ public static final DataComponentType.Valued MAP_ID = valued("map_id"); ++ /** ++ * Holds a list of markers to be placed on a {@link org.bukkit.inventory.ItemType#FILLED_MAP} (used for Explorer Maps). ++ */ ++ public static final DataComponentType.Valued MAP_DECORATIONS = valued("map_decorations"); ++ /** ++ * Internal map item state used in the map crafting recipe. ++ */ ++ public static final DataComponentType.Valued MAP_POST_PROCESSING = valued("map_post_processing"); ++ /** ++ * Holds all projectiles that have been loaded into a Crossbow. ++ * If not present, the Crossbow is not charged. ++ */ ++ public static final DataComponentType.Valued CHARGED_PROJECTILES = valued("charged_projectiles"); ++ /** ++ * Holds all items stored inside a Bundle. ++ * If removed, items cannot be added to the Bundle. ++ */ ++ public static final DataComponentType.Valued BUNDLE_CONTENTS = valued("bundle_contents"); ++ /** ++ * Holds the contents of a potion (Potion, Splash Potion, Lingering Potion), ++ * or potion applied to a Tipped Arrow. ++ */ ++ public static final DataComponentType.Valued POTION_CONTENTS = valued("potion_contents"); ++ /** ++ * Holds the effects that will be applied when consuming Suspicious Stew. ++ */ ++ public static final DataComponentType.Valued SUSPICIOUS_STEW_EFFECTS = valued("suspicious_stew_effects"); ++ /** ++ * Holds the contents in a Book and Quill. ++ */ ++ public static final DataComponentType.Valued WRITABLE_BOOK_CONTENT = valued("writable_book_content"); ++ /** ++ * Holds the contents and metadata of a Written Book. ++ */ ++ public static final DataComponentType.Valued WRITTEN_BOOK_CONTENT = valued("written_book_content"); ++ /** ++ * Holds the trims applied to an item in recipes ++ */ ++ public static final DataComponentType.Valued TRIM = valued("trim"); ++ // debug_stick_state - Block Property API ++ // entity_data ++ // bucket_entity_data ++ // block_entity_data ++ /** ++ * Holds the instrument type used by a Goat Horn. ++ */ ++ public static final DataComponentType.Valued INSTRUMENT = valued("instrument"); ++ /** ++ * Controls the amplifier amount for an Ominous Bottle's Bad Omen effect. ++ */ ++ public static final DataComponentType.Valued OMINOUS_BOTTLE_AMPLIFIER = valued("ominous_bottle_amplifier"); ++ /** ++ * List of recipes that should be unlocked when using the Knowledge Book item. ++ */ ++ public static final DataComponentType.Valued JUKEBOX_PLAYABLE = valued("jukebox_playable"); ++ public static final DataComponentType.Valued> RECIPES = valued("recipes"); ++ /** ++ * If present, specifies that the Compass is a Lodestone Compass. ++ */ ++ public static final DataComponentType.Valued LODESTONE_TRACKER = valued("lodestone_tracker"); ++ /** ++ * Stores the explosion crafted in a Firework Star. ++ */ ++ public static final DataComponentType.Valued FIREWORK_EXPLOSION = valued("firework_explosion"); ++ /** ++ * Stores all explosions crafted into a Firework Rocket, as well as flight duration. ++ */ ++ public static final DataComponentType.Valued FIREWORKS = valued("fireworks"); ++ /** ++ * Controls the skin displayed on a Player Head. ++ */ ++ public static final DataComponentType.Valued PROFILE = valued("profile"); ++ /** ++ * Controls the sound played by a Player Head when placed on a Note Block. ++ */ ++ public static final DataComponentType.Valued NOTE_BLOCK_SOUND = valued("note_block_sound"); ++ /** ++ * Stores the additional patterns applied to a Banner or Shield. ++ */ ++ public static final DataComponentType.Valued BANNER_PATTERNS = valued("banner_patterns"); ++ /** ++ * Stores the base color for a Shield. ++ */ ++ public static final DataComponentType.Valued BASE_COLOR = valued("base_color"); ++ /** ++ * Stores the Sherds applied to each side of a Decorated Pot. ++ */ ++ public static final DataComponentType.Valued POT_DECORATIONS = valued("pot_decorations"); ++ /** ++ * Holds the contents of container blocks (Chests, Shulker Boxes) in item form. ++ */ ++ public static final DataComponentType.Valued CONTAINER = valued("container"); ++ /** ++ * Holds block state properties to apply when placing a block. ++ */ ++ public static final DataComponentType.Valued BLOCK_DATA = valued("block_state"); ++ // bees ++ // /** ++ // * Holds the lock state of a container-like block, ++ // * copied to container block when placed. ++ // *
++ // * An item with a custom name of the same value must be used ++ // * to open this container. ++ // */ ++ // public static final DataComponentType.Valued LOCK = valued("lock"); ++ /** ++ * Holds the unresolved loot table and seed of a container-like block. ++ */ ++ public static final DataComponentType.Valued CONTAINER_LOOT = valued("container_loot"); ++ ++ private static DataComponentType.NonValued unvalued(final String name) { ++ return (DataComponentType.NonValued) requireNonNull(Registry.DATA_COMPONENT_TYPE.get(NamespacedKey.minecraft(name)), name + " unvalued data component type couldn't be found, this is a bug."); ++ } ++ ++ @SuppressWarnings("unchecked") ++ private static DataComponentType.Valued valued(final String name) { ++ return (DataComponentType.Valued) requireNonNull(Registry.DATA_COMPONENT_TYPE.get(NamespacedKey.minecraft(name)), name + " valued data component type couldn't be found, this is a bug."); ++ } ++ ++ private DataComponentTypes() { ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/BannerPatternLayers.java b/src/main/java/io/papermc/paper/datacomponent/item/BannerPatternLayers.java +new file mode 100644 +index 0000000000000000000000000000000000000000..12cfae82234b8c4cb231ab91e72ad82d28b85183 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/BannerPatternLayers.java +@@ -0,0 +1,66 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import java.util.Arrays; ++import java.util.List; ++import org.bukkit.block.banner.Pattern; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.Unmodifiable; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds the layers of patterns on a banner. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#BANNER_PATTERNS ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface BannerPatternLayers { ++ ++ @Contract(value = "_ -> new", pure = true) ++ static BannerPatternLayers bannerPatternLayers(final List patterns) { ++ return bannerPatternLayers().addAll(patterns).build(); ++ } ++ ++ @Contract(value = "-> new", pure = true) ++ static BannerPatternLayers.Builder bannerPatternLayers() { ++ return ItemComponentTypesBridge.bridge().bannerPatternLayers(); ++ } ++ ++ /** ++ * Gets the patterns on the banner. ++ * ++ * @return the patterns ++ */ ++ @Contract(pure = true) ++ @Unmodifiable List patterns(); ++ ++ /** ++ * Builder for {@link BannerPatternLayers}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ /** ++ * Adds a pattern to the banner. ++ * ++ * @param pattern the pattern ++ * @return the builder for chaining ++ * @see #patterns() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder add(Pattern pattern); ++ ++ /** ++ * Adds multiple patterns to the banner. ++ * ++ * @param patterns the patterns ++ * @return the builder for chaining ++ * @see #patterns() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addAll(List patterns); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/BlockItemDataProperties.java b/src/main/java/io/papermc/paper/datacomponent/item/BlockItemDataProperties.java +new file mode 100644 +index 0000000000000000000000000000000000000000..65f1bc8d1bea0042dca9683c439561132dbeea5c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/BlockItemDataProperties.java +@@ -0,0 +1,51 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import org.bukkit.block.BlockType; ++import org.bukkit.block.data.BlockData; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds the {@link BlockData} properties of a block item. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#BLOCK_DATA ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface BlockItemDataProperties { ++ ++ @Contract(value = "-> new", pure = true) ++ static BlockItemDataProperties.Builder blockItemStateProperties() { ++ return ItemComponentTypesBridge.bridge().blockItemStateProperties(); ++ } ++ ++ /** ++ * Creates a new {@link BlockData} instance for the given {@link BlockType}. ++ * ++ * @param blockType the block type ++ * @return the block data ++ */ ++ @Contract(pure = true) ++ BlockData createBlockData(BlockType blockType); ++ ++ /** ++ * Applies the properties to the given {@link BlockData}. Doesn't ++ * mutate the parameter, but returns a new instance with the properties applied. ++ * ++ * @param blockData the block data to apply the properties to ++ * @return the block data with the properties applied ++ */ ++ @Contract(pure = true) ++ BlockData applyTo(BlockData blockData); ++ ++ /** ++ * Builder for {@link BlockItemDataProperties}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ // building this requires BlockProperty API, so an empty builder for now (essentially read-only) ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/BundleContents.java b/src/main/java/io/papermc/paper/datacomponent/item/BundleContents.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c0f671aef8225c87632d2368d1b28fc8b1bce686 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/BundleContents.java +@@ -0,0 +1,66 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import java.util.Arrays; ++import java.util.List; ++import org.bukkit.inventory.ItemStack; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.Unmodifiable; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds all items stored inside of a Bundle. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#BUNDLE_CONTENTS ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface BundleContents { ++ ++ @Contract(value = "_ -> new", pure = true) ++ static BundleContents bundleContents(final List contents) { ++ return ItemComponentTypesBridge.bridge().bundleContents().addAll(contents).build(); ++ } ++ ++ @Contract(value = "-> new", pure = true) ++ static BundleContents.Builder bundleContents() { ++ return ItemComponentTypesBridge.bridge().bundleContents(); ++ } ++ ++ /** ++ * Lists the items that are currently stored inside of this component. ++ * ++ * @return items ++ */ ++ @Contract(value = "-> new", pure = true) ++ @Unmodifiable List contents(); ++ ++ /** ++ * Builder for {@link BundleContents}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ /** ++ * Adds an item to this builder. ++ * ++ * @param stack item ++ * @return the builder for chaining ++ * @see #contents() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder add(ItemStack stack); ++ ++ /** ++ * Adds items to this builder. ++ * ++ * @param stacks items ++ * @return the builder for chaining ++ * @see #contents() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addAll(List stacks); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/ChargedProjectiles.java b/src/main/java/io/papermc/paper/datacomponent/item/ChargedProjectiles.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d0a6e7db06f540e13ac00e8da3acabd9f7838f1f +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ChargedProjectiles.java +@@ -0,0 +1,66 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import java.util.Arrays; ++import java.util.List; ++import org.bukkit.inventory.ItemStack; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.Unmodifiable; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds all projectiles that have been loaded into a Crossbow. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#CHARGED_PROJECTILES ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface ChargedProjectiles { ++ ++ @Contract(value = "_ -> new", pure = true) ++ static ChargedProjectiles chargedProjectiles(final List projectiles) { ++ return chargedProjectiles().addAll(projectiles).build(); ++ } ++ ++ @Contract(value = "-> new", pure = true) ++ static ChargedProjectiles.Builder chargedProjectiles() { ++ return ItemComponentTypesBridge.bridge().chargedProjectiles(); ++ } ++ ++ /** ++ * Lists the projectiles that are currently loaded into this component. ++ * ++ * @return the loaded projectiles ++ */ ++ @Contract(value = "-> new", pure = true) ++ @Unmodifiable List projectiles(); ++ ++ /** ++ * Builder for {@link ChargedProjectiles}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ /** ++ * Adds a projectile to be loaded in this builder. ++ * ++ * @param stack projectile ++ * @return the builder for chaining ++ * @see #projectiles() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder add(ItemStack stack); ++ ++ /** ++ * Adds projectiles to be loaded in this builder. ++ * ++ * @param stacks projectiles ++ * @return the builder for chaining ++ * @see #projectiles() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addAll(List stacks); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/Consumable.java b/src/main/java/io/papermc/paper/datacomponent/item/Consumable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a448fedb63ffce18b9f6a1bd0fecfc5cd90224a6 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/Consumable.java +@@ -0,0 +1,70 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.BuildableDataComponent; ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import io.papermc.paper.datacomponent.item.consumable.ConsumeEffect; ++import io.papermc.paper.datacomponent.item.consumable.ItemUseAnimation; ++import java.util.Collection; ++import java.util.List; ++import net.kyori.adventure.key.Key; ++import org.checkerframework.checker.index.qual.NonNegative; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.Unmodifiable; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds the properties for this item for when it is consumed. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#CONSUMABLE ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface Consumable extends BuildableDataComponent { ++ ++ @Contract(value = "-> new", pure = true) ++ static Consumable.Builder consumable() { ++ return ItemComponentTypesBridge.bridge().consumable(); ++ } ++ ++ @Contract(pure = true) ++ @NonNegative float consumeSeconds(); ++ ++ @Contract(pure = true) ++ ItemUseAnimation animation(); ++ ++ @Contract(pure = true) ++ Key sound(); ++ ++ @Contract(pure = true) ++ boolean hasConsumeParticles(); ++ ++ @Contract(pure = true) ++ @Unmodifiable List consumeEffects(); ++ ++ /** ++ * Builder for {@link Consumable}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder consumeSeconds(@NonNegative float consumeSeconds); ++ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder animation(ItemUseAnimation animation); ++ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder sound(Key sound); ++ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder hasConsumeParticles(boolean hasConsumeParticles); ++ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addEffect(ConsumeEffect effect); ++ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addEffects(List effects); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/CustomModelData.java b/src/main/java/io/papermc/paper/datacomponent/item/CustomModelData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d416c9d25b3ab88bf1e208c6faf92a8e2378c376 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/CustomModelData.java +@@ -0,0 +1,28 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds the custom model data. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#CUSTOM_MODEL_DATA ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface CustomModelData { ++ ++ @Contract(value = "_ -> new", pure = true) ++ static CustomModelData customModelData(final int id) { ++ return ItemComponentTypesBridge.bridge().customModelData(id); ++ } ++ ++ /** ++ * Gets the custom model data id. ++ * ++ * @return the id ++ */ ++ @Contract(pure = true) ++ int id(); ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/DamageResistant.java b/src/main/java/io/papermc/paper/datacomponent/item/DamageResistant.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6cbd73cb2a11f4858b44a2f57d2fe0acb1eb9fb5 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/DamageResistant.java +@@ -0,0 +1,30 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.registry.tag.TagKey; ++import org.bukkit.damage.DamageType; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds the contents of damage types that the item entity containing this item is invincible to. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#DAMAGE_RESISTANT ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface DamageResistant { ++ ++ @Contract(value = "_ -> new", pure = true) ++ static DamageResistant damageResistant(final TagKey types) { ++ return ItemComponentTypesBridge.bridge().damageResistant(types); ++ } ++ ++ /** ++ * The types that this damage type is invincible tp. ++ * ++ * @return item ++ */ ++ @Contract(value = "-> new", pure = true) ++ TagKey types(); ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/DeathProtection.java b/src/main/java/io/papermc/paper/datacomponent/item/DeathProtection.java +new file mode 100644 +index 0000000000000000000000000000000000000000..87c2220708af7db06348994ad5940c7cecd9f691 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/DeathProtection.java +@@ -0,0 +1,48 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import io.papermc.paper.datacomponent.item.consumable.ConsumeEffect; ++import java.util.Arrays; ++import java.util.Collection; ++import java.util.List; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.Unmodifiable; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Sets whether this item should protect the entity upon death, and what effects should be played. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#DEATH_PROTECTION ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface DeathProtection { ++ ++ @Contract(value = "_ -> new", pure = true) ++ static DeathProtection deathProtection(final List deathEffects) { ++ return deathProtection().addEffects(deathEffects).build(); ++ } ++ ++ @Contract(value = "-> new", pure = true) ++ static DeathProtection.Builder deathProtection() { ++ return ItemComponentTypesBridge.bridge().deathProtection(); ++ } ++ ++ @Contract(value = "-> new", pure = true) ++ @Unmodifiable List deathEffects(); ++ ++ /** ++ * Builder for {@link DeathProtection}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addEffect(ConsumeEffect effect); ++ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addEffects(List effects); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/DyedItemColor.java b/src/main/java/io/papermc/paper/datacomponent/item/DyedItemColor.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d80581fc8b894cc4d4af9741244b1bb03468b263 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/DyedItemColor.java +@@ -0,0 +1,53 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import org.bukkit.Color; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Represents a color applied to a dyeable item. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#DYED_COLOR ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface DyedItemColor extends ShownInTooltip { ++ ++ @Contract(value = "_, _ -> new", pure = true) ++ static DyedItemColor dyedItemColor(final Color color, final boolean showInTooltip) { ++ return dyedItemColor().color(color).showInTooltip(showInTooltip).build(); ++ } ++ ++ @Contract(value = "-> new", pure = true) ++ static DyedItemColor.Builder dyedItemColor() { ++ return ItemComponentTypesBridge.bridge().dyedItemColor(); ++ } ++ ++ /** ++ * Color of the item. ++ * ++ * @return color ++ */ ++ @Contract(value = "-> new", pure = true) ++ Color color(); ++ ++ /** ++ * Builder for {@link DyedItemColor}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends ShownInTooltip.Builder, DataComponentBuilder { ++ ++ /** ++ * Sets the color of this builder. ++ * ++ * @param color color ++ * @return the builder for chaining ++ * @see #color() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder color(Color color); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/Enchantable.java b/src/main/java/io/papermc/paper/datacomponent/item/Enchantable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5169b9cd73dc0ffc8297f8d5f63d3d707a47d279 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/Enchantable.java +@@ -0,0 +1,31 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import org.checkerframework.checker.index.qual.Positive; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds if an item is enchantable, allowing for enchantments of the type to be seen in an enchanting table. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#ENCHANTABLE ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface Enchantable { ++ ++ @Contract(value = "_ -> new", pure = true) ++ static Enchantable enchantable(final @Positive int level) { ++ return ItemComponentTypesBridge.bridge().enchantable(level); ++ } ++ ++ /** ++ * Gets the current enchantment value level allowed, ++ * a higher value allows enchantments with a higher cost to be picked. ++ * ++ * @see Minecraft Wiki ++ * @return the value ++ */ ++ @Contract(pure = true) ++ @Positive int value(); ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/Equippable.java b/src/main/java/io/papermc/paper/datacomponent/item/Equippable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7d84217814bba4ce826e33755fee0d5c3b280009 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/Equippable.java +@@ -0,0 +1,170 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.BuildableDataComponent; ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import io.papermc.paper.registry.set.RegistryKeySet; ++import net.kyori.adventure.key.Key; ++import org.bukkit.entity.EntityType; ++import org.bukkit.inventory.EquipmentSlot; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++import org.jspecify.annotations.Nullable; ++ ++ ++/** ++ * Holds the equippable properties of an item. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#EQUIPPABLE ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface Equippable extends BuildableDataComponent { ++ ++ /** ++ * Creates a new {@link Equippable.Builder} instance. ++ * @param slot The slot for the new equippable to be equippable in. ++ * ++ * @return a new builder ++ */ ++ @Contract(value = "_ -> new", pure = true) ++ static Equippable.Builder equippable(final EquipmentSlot slot) { ++ return ItemComponentTypesBridge.bridge().equippable(slot); ++ } ++ ++ /** ++ * Gets the equipment slot this item can be equipped in. ++ * ++ * @return the equipment slot ++ */ ++ @Contract(pure = true) ++ EquipmentSlot slot(); ++ ++ /** ++ * Gets the equip sound key. ++ * ++ * @return the equip sound key ++ */ ++ @Contract(pure = true) ++ Key equipSound(); ++ ++ /** ++ * Gets the model key if present. ++ * ++ * @return the model key or null ++ */ ++ @Contract(pure = true) ++ @Nullable Key model(); ++ ++ /** ++ * Gets the camera overlay key if present. ++ * ++ * @return the camera overlay key or null ++ */ ++ @Contract(pure = true) ++ @Nullable Key cameraOverlay(); ++ ++ /** ++ * Gets the set of allowed entities that can equip this item. ++ * May be null if all entities are allowed. ++ * ++ * @return the set of allowed entities ++ */ ++ @Contract(pure = true) ++ @Nullable RegistryKeySet allowedEntities(); ++ ++ /** ++ * Checks if the item is dispensable. ++ * ++ * @return true if dispensable, false otherwise ++ */ ++ @Contract(pure = true) ++ boolean dispensable(); ++ ++ /** ++ * Checks if the item is swappable. ++ * ++ * @return true if swappable, false otherwise ++ */ ++ @Contract(pure = true) ++ boolean swappable(); ++ ++ /** ++ * Checks if the item takes damage when the wearer is hurt. ++ * ++ * @return true if it damages on hurt, false otherwise ++ */ ++ @Contract(pure = true) ++ boolean damageOnHurt(); ++ ++ /** ++ * Builder for {@link Equippable}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ /** ++ * Sets the equip sound key for this item. ++ * ++ * @param equipSound the equip sound key ++ * @return the builder for chaining ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder equipSound(Key equipSound); ++ ++ /** ++ * Sets the model key for this item. ++ * ++ * @param model the model key, nullable ++ * @return the builder for chaining ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder model(@Nullable Key model); ++ ++ /** ++ * Sets the camera overlay key for this item. ++ * ++ * @param cameraOverlay the camera overlay key, nullable ++ * @return the builder for chaining ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder cameraOverlay(@Nullable Key cameraOverlay); ++ ++ /** ++ * Sets the allowed entities that can equip this item. ++ * ++ * @param allowedEntities the set of allowed entity types, or null if any ++ * @return the builder for chaining ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder allowedEntities(@Nullable RegistryKeySet allowedEntities); ++ ++ /** ++ * Sets whether the item is dispensable. ++ * ++ * @param dispensable true if dispensable ++ * @return the builder for chaining ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder dispensable(boolean dispensable); ++ ++ /** ++ * Sets whether the item is swappable. ++ * ++ * @param swappable true if swappable ++ * @return the builder for chaining ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder swappable(boolean swappable); ++ ++ /** ++ * Sets whether the item takes damage when the wearer is hurt. ++ * ++ * @param damageOnHurt true if it damages on hurt ++ * @return the builder for chaining ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder damageOnHurt(boolean damageOnHurt); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/Fireworks.java b/src/main/java/io/papermc/paper/datacomponent/item/Fireworks.java +new file mode 100644 +index 0000000000000000000000000000000000000000..72aa1b4bda2693e0cd78d93449dda23bd1b74062 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/Fireworks.java +@@ -0,0 +1,84 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import java.util.List; ++import org.bukkit.FireworkEffect; ++import org.checkerframework.common.value.qual.IntRange; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.Unmodifiable; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Stores all explosions crafted into a Firework Rocket, as well as flight duration. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#FIREWORKS ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface Fireworks { ++ ++ @Contract(value = "_, _ -> new", pure = true) ++ static Fireworks fireworks(final List effects, final int flightDuration) { ++ return fireworks().addEffects(effects).flightDuration(flightDuration).build(); ++ } ++ ++ @Contract(value = "-> new", pure = true) ++ static Fireworks.Builder fireworks() { ++ return ItemComponentTypesBridge.bridge().fireworks(); ++ } ++ ++ /** ++ * Lists the effects stored in this component. ++ * ++ * @return the effects ++ */ ++ @Contract(pure = true) ++ @Unmodifiable List effects(); ++ ++ /** ++ * Number of gunpowder in this component. ++ * ++ * @return the flight duration ++ */ ++ @Contract(pure = true) ++ @IntRange(from = 0, to = 255) int flightDuration(); ++ ++ /** ++ * Builder for {@link Fireworks}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ /** ++ * Sets the number of gunpowder used in this builder. ++ * ++ * @param duration duration ++ * @return the builder for chaining ++ * @see #flightDuration() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder flightDuration(@IntRange(from = 0, to = 255) int duration); ++ ++ /** ++ * Adds an explosion to this builder. ++ * ++ * @param effect effect ++ * @return the builder for chaining ++ * @see #effects() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addEffect(FireworkEffect effect); ++ ++ /** ++ * Adds explosions to this builder. ++ * ++ * @param effects effects ++ * @return the builder for chaining ++ * @see #effects() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addEffects(List effects); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/FoodProperties.java b/src/main/java/io/papermc/paper/datacomponent/item/FoodProperties.java +new file mode 100644 +index 0000000000000000000000000000000000000000..369208e15a0e7fc91a9505fef2097c4283445e4a +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/FoodProperties.java +@@ -0,0 +1,87 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.BuildableDataComponent; ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import org.checkerframework.checker.index.qual.NonNegative; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds the food properties of an item. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#FOOD ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface FoodProperties extends BuildableDataComponent { ++ ++ @Contract(value = "-> new", pure = true) ++ static FoodProperties.Builder food() { ++ return ItemComponentTypesBridge.bridge().food(); ++ } ++ ++ /** ++ * Number of food points to restore when eaten. ++ * ++ * @return the nutrition ++ */ ++ @Contract(pure = true) ++ @NonNegative int nutrition(); ++ ++ /** ++ * Amount of saturation to restore when eaten. ++ * ++ * @return the saturation ++ */ ++ @Contract(pure = true) ++ float saturation(); ++ ++ /** ++ * If {@code true}, this food can be eaten even if not hungry. ++ * ++ * @return can always be eaten ++ */ ++ @Contract(pure = true) ++ boolean canAlwaysEat(); ++ ++ /** ++ * Builder for {@link FoodProperties}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ /** ++ * Set if this food can always be eaten, even if the ++ * player is not hungry. ++ * ++ * @param canAlwaysEat true to allow always eating ++ * @return the builder for chaining ++ * @see #canAlwaysEat() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder canAlwaysEat(boolean canAlwaysEat); ++ ++ /** ++ * Sets the saturation of the food. ++ * ++ * @param saturation the saturation ++ * @return the builder for chaining ++ * @see #saturation() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder saturation(float saturation); ++ ++ /** ++ * Sets the nutrition of the food. ++ * ++ * @param nutrition the nutrition, must be non-negative ++ * @return the builder for chaining ++ * @see #nutrition() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder nutrition(@NonNegative int nutrition); ++ ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/ItemAdventurePredicate.java b/src/main/java/io/papermc/paper/datacomponent/item/ItemAdventurePredicate.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f5061d1f349b35e5ec57d2d1c64eafb096141404 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ItemAdventurePredicate.java +@@ -0,0 +1,65 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.block.BlockPredicate; ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import java.util.List; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.Unmodifiable; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Controls which blocks a player in Adventure mode can do a certain action with this item. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#CAN_BREAK ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#CAN_PLACE_ON ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface ItemAdventurePredicate extends ShownInTooltip { ++ ++ @Contract(value = "_ -> new", pure = true) ++ static ItemAdventurePredicate itemAdventurePredicate(final List predicates) { ++ return itemAdventurePredicate().addPredicates(predicates).build(); ++ } ++ ++ @Contract(value = "-> new", pure = true) ++ static ItemAdventurePredicate.Builder itemAdventurePredicate() { ++ return ItemComponentTypesBridge.bridge().itemAdventurePredicate(); ++ } ++ ++ /** ++ * List of block predicates that control if the action is allowed. ++ * ++ * @return predicates ++ */ ++ @Contract(pure = true) ++ @Unmodifiable List predicates(); ++ ++ /** ++ * Builder for {@link ItemAdventurePredicate}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends ShownInTooltip.Builder, DataComponentBuilder { ++ /** ++ * Adds a block predicate to this builder. ++ * ++ * @param predicate predicate ++ * @return the builder for chaining ++ * @see #predicates() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addPredicate(BlockPredicate predicate); ++ ++ /** ++ * Adds block predicates to this builder. ++ * ++ * @param predicates predicates ++ * @return the builder for chaining ++ * @see #predicates() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addPredicates(List predicates); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/ItemArmorTrim.java b/src/main/java/io/papermc/paper/datacomponent/item/ItemArmorTrim.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0309ae59ab7945ddfb5410930d161e2ce3d1878a +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ItemArmorTrim.java +@@ -0,0 +1,53 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import org.bukkit.inventory.meta.trim.ArmorTrim; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds the trims applied to an item. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#TRIM ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface ItemArmorTrim extends ShownInTooltip { ++ ++ @Contract(value = "_, _ -> new", pure = true) ++ static ItemArmorTrim itemArmorTrim(final ArmorTrim armorTrim, final boolean showInTooltip) { ++ return itemArmorTrim(armorTrim).showInTooltip(showInTooltip).build(); ++ } ++ ++ @Contract(value = "_ -> new", pure = true) ++ static ItemArmorTrim.Builder itemArmorTrim(final ArmorTrim armorTrim) { ++ return ItemComponentTypesBridge.bridge().itemArmorTrim(armorTrim); ++ } ++ ++ /** ++ * Armor trim present on this item. ++ * ++ * @return trim ++ */ ++ @Contract(pure = true) ++ ArmorTrim armorTrim(); ++ ++ /** ++ * Builder for {@link ItemArmorTrim}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends ShownInTooltip.Builder, DataComponentBuilder { ++ ++ /** ++ * Sets the armor trim for this builder. ++ * ++ * @param armorTrim trim ++ * @return the builder for chaining ++ * @see #armorTrim() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder armorTrim(ArmorTrim armorTrim); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/ItemAttributeModifiers.java b/src/main/java/io/papermc/paper/datacomponent/item/ItemAttributeModifiers.java +new file mode 100644 +index 0000000000000000000000000000000000000000..948505d38121d54df62e6a67d4597bc7d42c356f +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ItemAttributeModifiers.java +@@ -0,0 +1,98 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import java.util.List; ++import org.bukkit.attribute.Attribute; ++import org.bukkit.attribute.AttributeModifier; ++import org.bukkit.inventory.EquipmentSlotGroup; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.Unmodifiable; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds attribute modifiers applied to any item. ++ * ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#ATTRIBUTE_MODIFIERS ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface ItemAttributeModifiers extends ShownInTooltip { ++ ++ @Contract(value = "-> new", pure = true) ++ static ItemAttributeModifiers.Builder itemAttributes() { ++ return ItemComponentTypesBridge.bridge().modifiers(); ++ } ++ ++ /** ++ * Lists the attribute modifiers that are present on this item. ++ * ++ * @return modifiers ++ */ ++ @Contract(pure = true) ++ @Unmodifiable List modifiers(); ++ ++ /** ++ * Holds an attribute entry. ++ */ ++ @ApiStatus.NonExtendable ++ interface Entry { ++ ++ /** ++ * Gets the target attribute for the paired modifier. ++ * ++ * @return the attribute ++ */ ++ @Contract(pure = true) ++ Attribute attribute(); ++ ++ /** ++ * The modifier for the paired attribute. ++ * ++ * @return the modifier ++ */ ++ @Contract(pure = true) ++ AttributeModifier modifier(); ++ ++ /** ++ * Gets the slot group for this attribute. ++ * ++ * @return the slot group ++ */ ++ default EquipmentSlotGroup getGroup() { ++ return this.modifier().getSlotGroup(); ++ } ++ } ++ ++ /** ++ * Builder for {@link ItemAttributeModifiers}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends ShownInTooltip.Builder, DataComponentBuilder { ++ ++ /** ++ * Adds a modifier to this builder. ++ * ++ * @param attribute attribute ++ * @param modifier modifier ++ * @return the builder for chaining ++ * @see #modifiers() ++ */ ++ @Contract(value = "_, _, _ -> this", mutates = "this") ++ Builder addModifier(Attribute attribute, AttributeModifier modifier); ++ ++ /** ++ * Adds a modifier to this builder. ++ * ++ * @param attribute attribute ++ * @param modifier modifier ++ * @param equipmentSlotGroup the slot group this modifier applies to (overrides any slot group in the modifier) ++ * @return the builder for chaining ++ * @see #modifiers() ++ */ ++ @Contract(value = "_, _, _ -> this", mutates = "this") ++ Builder addModifier(Attribute attribute, AttributeModifier modifier, EquipmentSlotGroup equipmentSlotGroup); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/ItemComponentTypesBridge.java b/src/main/java/io/papermc/paper/datacomponent/item/ItemComponentTypesBridge.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1ce34642371a65590ce1ac74b402ccfc301671d7 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ItemComponentTypesBridge.java +@@ -0,0 +1,112 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import com.destroystokyo.paper.profile.PlayerProfile; ++import io.papermc.paper.registry.set.RegistryKeySet; ++import io.papermc.paper.registry.tag.TagKey; ++import io.papermc.paper.text.Filtered; ++import java.util.Optional; ++import java.util.ServiceLoader; ++import net.kyori.adventure.key.Key; ++import net.kyori.adventure.util.TriState; ++import org.bukkit.JukeboxSong; ++import org.bukkit.block.BlockType; ++import org.bukkit.damage.DamageType; ++import org.bukkit.inventory.EquipmentSlot; ++import org.bukkit.inventory.ItemStack; ++import org.bukkit.inventory.ItemType; ++import org.bukkit.inventory.meta.trim.ArmorTrim; ++import org.bukkit.map.MapCursor; ++import org.jetbrains.annotations.ApiStatus; ++import org.jspecify.annotations.NullMarked; ++import org.jspecify.annotations.Nullable; ++ ++@NullMarked ++@ApiStatus.Internal ++interface ItemComponentTypesBridge { ++ ++ Optional BRIDGE = ServiceLoader.load(ItemComponentTypesBridge.class).findFirst(); ++ ++ static ItemComponentTypesBridge bridge() { ++ return BRIDGE.orElseThrow(); ++ } ++ ++ ChargedProjectiles.Builder chargedProjectiles(); ++ ++ PotDecorations.Builder potDecorations(); ++ ++ Unbreakable.Builder unbreakable(); ++ ++ ItemLore.Builder lore(); ++ ++ ItemEnchantments.Builder enchantments(); ++ ++ ItemAttributeModifiers.Builder modifiers(); ++ ++ FoodProperties.Builder food(); ++ ++ DyedItemColor.Builder dyedItemColor(); ++ ++ PotionContents.Builder potionContents(); ++ ++ BundleContents.Builder bundleContents(); ++ ++ SuspiciousStewEffects.Builder suspiciousStewEffects(); ++ ++ MapItemColor.Builder mapItemColor(); ++ ++ MapDecorations.Builder mapDecorations(); ++ ++ MapDecorations.DecorationEntry decorationEntry(MapCursor.Type type, double x, double z, float rotation); ++ ++ SeededContainerLoot.Builder seededContainerLoot(Key lootTableKey); ++ ++ WrittenBookContent.Builder writtenBookContent(Filtered title, String author); ++ ++ WritableBookContent.Builder writeableBookContent(); ++ ++ ItemArmorTrim.Builder itemArmorTrim(ArmorTrim armorTrim); ++ ++ LodestoneTracker.Builder lodestoneTracker(); ++ ++ Fireworks.Builder fireworks(); ++ ++ ResolvableProfile.Builder resolvableProfile(); ++ ++ ResolvableProfile resolvableProfile(PlayerProfile profile); ++ ++ BannerPatternLayers.Builder bannerPatternLayers(); ++ ++ BlockItemDataProperties.Builder blockItemStateProperties(); ++ ++ ItemContainerContents.Builder itemContainerContents(); ++ ++ JukeboxPlayable.Builder jukeboxPlayable(JukeboxSong song); ++ ++ Tool.Builder tool(); ++ ++ Tool.Rule rule(RegistryKeySet blocks, @Nullable Float speed, TriState correctForDrops); ++ ++ ItemAdventurePredicate.Builder itemAdventurePredicate(); ++ ++ CustomModelData customModelData(int id); ++ ++ MapId mapId(int id); ++ ++ UseRemainder useRemainder(ItemStack itemStack); ++ ++ Consumable.Builder consumable(); ++ ++ UseCooldown.Builder useCooldown(final float seconds); ++ ++ DamageResistant damageResistant(TagKey types); ++ ++ Enchantable enchantable(int level); ++ ++ Repairable repairable(RegistryKeySet types); ++ ++ Equippable.Builder equippable(EquipmentSlot slot); ++ ++ DeathProtection.Builder deathProtection(); ++ ++ OminousBottleAmplifier ominousBottleAmplifier(int amplifier); ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/ItemContainerContents.java b/src/main/java/io/papermc/paper/datacomponent/item/ItemContainerContents.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7d1c973ba566752d7a85496327b1352d973f2218 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ItemContainerContents.java +@@ -0,0 +1,63 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import java.util.Arrays; ++import java.util.List; ++import org.bukkit.inventory.ItemStack; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.Unmodifiable; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds the contents of an item container. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#CONTAINER ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface ItemContainerContents { ++ ++ @Contract(value = "_ -> new", pure = true) ++ static ItemContainerContents containerContents(final List contents) { ++ return containerContents().addAll(contents).build(); ++ } ++ ++ @Contract(value = "-> new", pure = true) ++ static ItemContainerContents.Builder containerContents() { ++ return ItemComponentTypesBridge.bridge().itemContainerContents(); ++ } ++ ++ /** ++ * Gets the contents of the container. ++ * ++ * @return the contents ++ */ ++ @Contract(value = "-> new", pure = true) ++ @Unmodifiable List contents(); ++ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ /** ++ * Adds an item stack to the container. ++ * ++ * @param stack the item stack ++ * @return the builder for chaining ++ * @see #contents() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder add(ItemStack stack); ++ ++ /** ++ * Adds item stacks to the container. ++ * ++ * @param stacks the item stacks ++ * @return the builder for chaining ++ * @see #contents() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addAll(List stacks); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/ItemEnchantments.java b/src/main/java/io/papermc/paper/datacomponent/item/ItemEnchantments.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fca271ea198209bd48cd02f4476e89e5e3e9f396 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ItemEnchantments.java +@@ -0,0 +1,68 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import java.util.Map; ++import org.bukkit.enchantments.Enchantment; ++import org.checkerframework.common.value.qual.IntRange; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.Unmodifiable; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Stores a list of enchantments and their levels on an item. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#ENCHANTMENTS ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#STORED_ENCHANTMENTS ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface ItemEnchantments extends ShownInTooltip { ++ ++ @Contract(value = "_, _ -> new", pure = true) ++ static ItemEnchantments itemEnchantments(final Map enchantments, final boolean showInTooltip) { ++ return itemEnchantments().addAll(enchantments).showInTooltip(showInTooltip).build(); ++ } ++ ++ @Contract(value = "-> new", pure = true) ++ static ItemEnchantments.Builder itemEnchantments() { ++ return ItemComponentTypesBridge.bridge().enchantments(); ++ } ++ ++ /** ++ * Enchantments currently present on this item. ++ * ++ * @return enchantments ++ */ ++ @Contract(pure = true) ++ @Unmodifiable Map enchantments(); ++ ++ /** ++ * Builder for {@link ItemEnchantments}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends ShownInTooltip.Builder, DataComponentBuilder { ++ ++ /** ++ * Adds an enchantment with the given level to this component. ++ * ++ * @param enchantment enchantment ++ * @param level level ++ * @return the builder for chaining ++ * @see #enchantments() ++ */ ++ @Contract(value = "_, _ -> this", mutates = "this") ++ Builder add(Enchantment enchantment, @IntRange(from = 1, to = 255) int level); ++ ++ /** ++ * Adds enchantments with the given level to this component. ++ * ++ * @param enchantments enchantments ++ * @return the builder for chaining ++ * @see #enchantments() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addAll(Map enchantments); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/ItemLore.java b/src/main/java/io/papermc/paper/datacomponent/item/ItemLore.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3be62f6005e0343c3a6ebd04e3ee824e0b969113 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ItemLore.java +@@ -0,0 +1,84 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import java.util.List; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.ComponentLike; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.Unmodifiable; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Additional lines to include in an item's tooltip. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#LORE ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface ItemLore { ++ ++ @Contract(value = "_ -> new", pure = true) ++ static ItemLore lore(final List lines) { ++ return lore().lines(lines).build(); ++ } ++ ++ @Contract(value = "-> new", pure = true) ++ static ItemLore.Builder lore() { ++ return ItemComponentTypesBridge.bridge().lore(); ++ } ++ ++ /** ++ * Lists the components that are added to an item's tooltip. ++ * ++ * @return component list ++ */ ++ @Contract(pure = true) ++ @Unmodifiable List lines(); ++ ++ /** ++ * Lists the styled components (example: italicized and purple) that are added to an item's tooltip. ++ * ++ * @return component list ++ */ ++ @Contract(pure = true) ++ @Unmodifiable List styledLines(); ++ ++ /** ++ * Builder for {@link ItemLore}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ /** ++ * Sets the components of this lore. ++ * ++ * @param lines components ++ * @return the builder for chaining ++ * @see #lines() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder lines(List lines); ++ ++ /** ++ * Adds a component to the lore. ++ * ++ * @param line component ++ * @return the builder for chaining ++ * @see #lines() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addLine(ComponentLike line); ++ ++ /** ++ * Adds components to the lore. ++ * ++ * @param lines components ++ * @return the builder for chaining ++ * @see #lines() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addLines(List lines); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/JukeboxPlayable.java b/src/main/java/io/papermc/paper/datacomponent/item/JukeboxPlayable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c59942df7101c7630eabeb247b9690b9c4c76da4 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/JukeboxPlayable.java +@@ -0,0 +1,43 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import org.bukkit.JukeboxSong; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds the jukebox song for an item. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#JUKEBOX_PLAYABLE ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface JukeboxPlayable extends ShownInTooltip { ++ ++ @Contract(value = "_ -> new", pure = true) ++ static JukeboxPlayable.Builder jukeboxPlayable(final JukeboxSong song) { ++ return ItemComponentTypesBridge.bridge().jukeboxPlayable(song); ++ } ++ ++ @Contract(pure = true) ++ JukeboxSong jukeboxSong(); ++ ++ /** ++ * Builder for {@link JukeboxPlayable}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends ShownInTooltip.Builder, DataComponentBuilder { ++ ++ /** ++ * Sets the jukebox song. ++ * ++ * @param song the song ++ * @return the builder for chaining ++ * @see #jukeboxSong() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder jukeboxSong(JukeboxSong song); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/LodestoneTracker.java b/src/main/java/io/papermc/paper/datacomponent/item/LodestoneTracker.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b919672ceea74ae09324653847b30fde293054d8 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/LodestoneTracker.java +@@ -0,0 +1,72 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import org.bukkit.Location; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++import org.jspecify.annotations.Nullable; ++ ++/** ++ * If present, specifies the target Lodestone that a Compass should point towards. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#LODESTONE_TRACKER ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface LodestoneTracker { ++ ++ @Contract(value = "_, _ -> new", pure = true) ++ static LodestoneTracker lodestoneTracker(final @Nullable Location location, final boolean tracked) { ++ return lodestoneTracker().location(location).tracked(tracked).build(); ++ } ++ ++ @Contract(value = "-> new", pure = true) ++ static LodestoneTracker.Builder lodestoneTracker() { ++ return ItemComponentTypesBridge.bridge().lodestoneTracker(); ++ } ++ ++ /** ++ * The location that the compass should point towards. ++ * ++ * @return location ++ */ ++ @Contract(value = "-> new", pure = true) ++ @Nullable Location location(); ++ ++ /** ++ * If {@code true}, when the Lodestone at the target position is removed, the component will be removed. ++ * ++ * @return tracked ++ */ ++ @Contract(pure = true) ++ boolean tracked(); ++ ++ /** ++ * Builder for {@link LodestoneTracker}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ /** ++ * Sets the location to point towards for this builder. ++ * ++ * @param location location to point towards ++ * @return the builder for chaining ++ * @see #location() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder location(@Nullable Location location); ++ ++ /** ++ * Sets if this location lodestone is tracked for this builder. ++ * ++ * @param tracked is tracked ++ * @return the builder for chaining ++ * @see #tracked() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder tracked(boolean tracked); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/MapDecorations.java b/src/main/java/io/papermc/paper/datacomponent/item/MapDecorations.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1e611f1f918c33f8d89ad23cf2fc44a127af233c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/MapDecorations.java +@@ -0,0 +1,121 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import java.util.Map; ++import org.bukkit.map.MapCursor; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.Unmodifiable; ++import org.jspecify.annotations.NullMarked; ++import org.jspecify.annotations.Nullable; ++ ++/** ++ * Holds a list of markers to be placed on a Filled Map (used for Explorer Maps). ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#MAP_DECORATIONS ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface MapDecorations { ++ ++ @Contract(value = "_ -> new", pure = true) ++ static MapDecorations mapDecorations(final Map entries) { ++ return mapDecorations().putAll(entries).build(); ++ } ++ ++ @Contract(value = "-> new", pure = true) ++ static MapDecorations.Builder mapDecorations() { ++ return ItemComponentTypesBridge.bridge().mapDecorations(); ++ } ++ ++ @Contract(value = "_, _, _, _ -> new", pure = true) ++ static DecorationEntry decorationEntry(final MapCursor.Type type, final double x, final double z, final float rotation) { ++ return ItemComponentTypesBridge.bridge().decorationEntry(type, x, z, rotation); ++ } ++ ++ /** ++ * Gets the decoration entry with the given id. ++ * ++ * @param id id ++ * @return decoration entry, or {@code null} if not present ++ */ ++ @Contract(pure = true) ++ @Nullable DecorationEntry decoration(String id); ++ ++ /** ++ * Gets the decoration entries. ++ * ++ * @return the decoration entries ++ */ ++ @Contract(pure = true) ++ @Unmodifiable Map decorations(); ++ ++ /** ++ * Decoration present on the map. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface DecorationEntry { ++ ++ /** ++ * Type of decoration. ++ * ++ * @return type ++ */ ++ @Contract(pure = true) ++ MapCursor.Type type(); ++ ++ /** ++ * X world coordinate of the decoration. ++ * ++ * @return x coordinate ++ */ ++ @Contract(pure = true) ++ double x(); ++ ++ /** ++ * Z world coordinate of the decoration. ++ * ++ * @return z coordinate ++ */ ++ @Contract(pure = true) ++ double z(); ++ ++ /** ++ * Clockwise rotation from north in degrees. ++ * ++ * @return rotation ++ */ ++ @Contract(pure = true) ++ float rotation(); ++ } ++ ++ /** ++ * Builder for {@link MapDecorations}. ++ */ ++ @ApiStatus.NonExtendable ++ @ApiStatus.Experimental ++ interface Builder extends DataComponentBuilder { ++ ++ /** ++ * Puts the decoration with the given id in this builder. ++ * ++ * @param id id ++ * @param entry decoration ++ * @return the builder for chaining ++ * @see #decorations() ++ */ ++ @Contract(value = "_, _ -> this", mutates = "this") ++ MapDecorations.Builder put(String id, DecorationEntry entry); ++ ++ /** ++ * Puts all the decoration with the given id in this builder. ++ * ++ * @param entries decorations ++ * @return the builder for chaining ++ * @see #decorations() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ MapDecorations.Builder putAll(Map entries); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/MapId.java b/src/main/java/io/papermc/paper/datacomponent/item/MapId.java +new file mode 100644 +index 0000000000000000000000000000000000000000..045bfe0ce5080b57a40be03a65b1a2aaf9089120 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/MapId.java +@@ -0,0 +1,28 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * References the shared map state holding map contents and markers for a Filled Map. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#MAP_ID ++ */ ++@NullMarked ++@ApiStatus.NonExtendable ++@ApiStatus.Experimental ++public interface MapId { ++ ++ @Contract(value = "_ -> new", pure = true) ++ static MapId mapId(final int id) { ++ return ItemComponentTypesBridge.bridge().mapId(id); ++ } ++ ++ /** ++ * The map id. ++ * ++ * @return id ++ */ ++ @Contract(pure = true) ++ int id(); ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/MapItemColor.java b/src/main/java/io/papermc/paper/datacomponent/item/MapItemColor.java +new file mode 100644 +index 0000000000000000000000000000000000000000..87845d19a25ed2ae79b868fcfe40b88a2dc83f97 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/MapItemColor.java +@@ -0,0 +1,43 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import org.bukkit.Color; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Represents the tint of the decorations on the Filled Map item. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#MAP_COLOR ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface MapItemColor { ++ ++ @Contract(value = "-> new", pure = true) ++ static MapItemColor.Builder mapItemColor() { ++ return ItemComponentTypesBridge.bridge().mapItemColor(); ++ } ++ ++ /** ++ * The tint to apply. ++ * ++ * @return color ++ */ ++ Color color(); ++ ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ /** ++ * Sets the tint color of this map. ++ * ++ * @param color tint color ++ * @return the builder for chaining ++ * @see #color() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder color(Color color); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/OminousBottleAmplifier.java b/src/main/java/io/papermc/paper/datacomponent/item/OminousBottleAmplifier.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4f16e08f04c2cea24f3cb132ff21f4bdd6b70582 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/OminousBottleAmplifier.java +@@ -0,0 +1,29 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import org.checkerframework.common.value.qual.IntRange; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds the ominous bottle amplifier. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#OMINOUS_BOTTLE_AMPLIFIER ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface OminousBottleAmplifier { ++ ++ @Contract(value = "_ -> new", pure = true) ++ static OminousBottleAmplifier amplifier(final @IntRange(from = 0, to = 4) int amplifier) { ++ return ItemComponentTypesBridge.bridge().ominousBottleAmplifier(amplifier); ++ } ++ ++ /** ++ * Gets the bottle amplifier. ++ * ++ * @return the amplifier ++ */ ++ @Contract(pure = true) ++ @IntRange(from = 0, to = 4) int amplifier(); ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PotDecorations.java b/src/main/java/io/papermc/paper/datacomponent/item/PotDecorations.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6da78b8735a6cadd1282fa2fafd8b0f74f087fb4 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PotDecorations.java +@@ -0,0 +1,109 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import org.bukkit.inventory.ItemType; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++import org.jspecify.annotations.Nullable; ++ ++// CONTRIBUTORS: LEAVE THIS AS ITEM TYPE!!! ++/** ++ * Holds the item types for the decorations on a flower pot. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#POT_DECORATIONS ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface PotDecorations { ++ ++ @Contract(value = "_, _, _, _ -> new", pure = true) ++ static PotDecorations potDecorations(final @Nullable ItemType back, final @Nullable ItemType left, final @Nullable ItemType right, final @Nullable ItemType front) { ++ return potDecorations().back(back).left(left).right(right).front(front).build(); ++ } ++ ++ @Contract(value = "-> new", pure = true) ++ static PotDecorations.Builder potDecorations() { ++ return ItemComponentTypesBridge.bridge().potDecorations(); ++ } ++ ++ /** ++ * Get the item type for the back. ++ * ++ * @return the back item type. ++ */ ++ @Contract(pure = true) ++ @Nullable ItemType back(); ++ ++ /** ++ * Get the item type for the left. ++ * ++ * @return the left item type. ++ */ ++ @Contract(pure = true) ++ @Nullable ItemType left(); ++ ++ /** ++ * Get the item type for the right. ++ * ++ * @return the right item type. ++ */ ++ @Contract(pure = true) ++ @Nullable ItemType right(); ++ ++ /** ++ * Get the item type for the front. ++ * ++ * @return the front item type. ++ */ ++ @Contract(pure = true) ++ @Nullable ItemType front(); ++ ++ /** ++ * Builder for {@link PotDecorations}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ /** ++ * Set the {@link ItemType} for the back. ++ * ++ * @param back item for the back ++ * @return the builder for chaining ++ * @see #back() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder back(@Nullable ItemType back); ++ ++ /** ++ * Set the {@link ItemType} for the left. ++ * ++ * @param left item for the left ++ * @return the builder for chaining ++ * @see #left() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder left(@Nullable ItemType left); ++ ++ /** ++ * Set the {@link ItemType} for the right. ++ * ++ * @param right item for the right ++ * @return the builder for chaining ++ * @see #right() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder right(@Nullable ItemType right); ++ ++ /** ++ * Set the {@link ItemType} for the front. ++ * ++ * @param front item for the front ++ * @return the builder for chaining ++ * @see #front() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder front(@Nullable ItemType front); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PotionContents.java b/src/main/java/io/papermc/paper/datacomponent/item/PotionContents.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7cf05b382319064d45433a7e2678f65c25d11b14 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PotionContents.java +@@ -0,0 +1,120 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import java.util.List; ++import org.bukkit.Color; ++import org.bukkit.potion.PotionEffect; ++import org.bukkit.potion.PotionType; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.Unmodifiable; ++import org.jspecify.annotations.NullMarked; ++import org.jspecify.annotations.Nullable; ++ ++/** ++ * Holds the contents of a potion (Potion, Splash Potion, Lingering Potion), or potion applied to a Tipped Arrow. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#POTION_CONTENTS ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface PotionContents { ++ ++ @Contract(value = "-> new", pure = true) ++ static PotionContents.Builder potionContents() { ++ return ItemComponentTypesBridge.bridge().potionContents(); ++ } ++ ++ /** ++ * The potion type in this item: the item will inherit all effects from this. ++ * ++ * @return potion type, or {@code null} if not present ++ */ ++ @Contract(pure = true) ++ @Nullable PotionType potion(); ++ ++ /** ++ * Overrides the visual color of the potion. ++ * ++ * @return color override, or {@code null} if not present ++ * @apiNote alpha channel of the color is only relevant ++ * for Tipped Arrow ++ */ ++ @Contract(pure = true) ++ @Nullable Color customColor(); ++ ++ /** ++ * Additional list of effect instances that this item should apply. ++ * ++ * @return effects ++ */ ++ @Contract(pure = true) ++ @Unmodifiable List customEffects(); ++ ++ /** ++ * Suffix to the translation key of the potion item. ++ * ++ * @return translation key suffix, or {@code null} if not present ++ * @apiNote This is used in the display of tipped arrow and potion items. ++ */ ++ @Contract(pure = true) ++ @Nullable String customName(); ++ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ /** ++ * Sets the potion type for this builder. ++ * ++ * @param type builder ++ * @return the builder for chaining ++ * @see #potion() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder potion(@Nullable PotionType type); ++ ++ /** ++ * Sets the color override for this builder. ++ * ++ * @param color color ++ * @return the builder for chaining ++ * @see #customColor() ++ * @apiNote alpha channel of the color is supported only for Tipped Arrow ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder customColor(@Nullable Color color); ++ ++ /** ++ * Sets the suffix to the translation key of the potion item. ++ * ++ * @param name name ++ * @return the builder for chaining ++ * @see #customName() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder customName(@Nullable String name); ++ ++ /** ++ * Adds a custom effect instance to this builder. ++ * ++ * @param effect effect ++ * @see #customEffects() ++ * @return the builder for chaining ++ * @see #customEffects() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addCustomEffect(PotionEffect effect); ++ ++ /** ++ * Adds custom effect instances to this builder. ++ * ++ * @param effects effects ++ * @see #customEffects() ++ * @return the builder for chaining ++ * @see #customEffects() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addCustomEffects(List effects); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/Repairable.java b/src/main/java/io/papermc/paper/datacomponent/item/Repairable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ff84d9123aab0ad2f93b397e20a37f21894547a3 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/Repairable.java +@@ -0,0 +1,30 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.registry.set.RegistryKeySet; ++import org.bukkit.inventory.ItemType; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds if this item is repairable, and what item types it can be repaired with. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#REPAIRABLE ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface Repairable { ++ ++ @Contract(value = "_ -> new", pure = true) ++ static Repairable repairable(final RegistryKeySet types) { ++ return ItemComponentTypesBridge.bridge().repairable(types); ++ } ++ ++ /** ++ * The types that this item is repairable to. ++ * ++ * @return item ++ */ ++ @Contract(value = "-> new", pure = true) ++ RegistryKeySet types(); ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/ResolvableProfile.java b/src/main/java/io/papermc/paper/datacomponent/item/ResolvableProfile.java +new file mode 100644 +index 0000000000000000000000000000000000000000..dc6cd191553e7ca5b6c5768f594924e4c39fcbbe +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ResolvableProfile.java +@@ -0,0 +1,123 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import com.destroystokyo.paper.profile.PlayerProfile; ++import com.destroystokyo.paper.profile.ProfileProperty; ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import java.util.Collection; ++import java.util.UUID; ++import java.util.concurrent.CompletableFuture; ++import org.intellij.lang.annotations.Pattern; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.Unmodifiable; ++import org.jspecify.annotations.NullMarked; ++import org.jspecify.annotations.Nullable; ++ ++/** ++ * Holds player profile data that can be resolved to a {@link PlayerProfile}. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#PROFILE ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface ResolvableProfile { ++ ++ @Contract(value = "_ -> new", pure = true) ++ static ResolvableProfile resolvableProfile(final PlayerProfile profile) { ++ return ItemComponentTypesBridge.bridge().resolvableProfile(profile); ++ } ++ ++ @Contract(value = "-> new", pure = true) ++ static ResolvableProfile.Builder resolvableProfile() { ++ return ItemComponentTypesBridge.bridge().resolvableProfile(); ++ } ++ ++ @Contract(pure = true) ++ @Nullable UUID uuid(); ++ ++ @Contract(pure = true) ++ @Nullable String name(); ++ ++ @Contract(pure = true) ++ @Unmodifiable Collection properties(); ++ ++ /** ++ * Produces an updated player profile based on this. ++ *

++ * This tries to produce a completed profile by filling in missing ++ * properties (name, unique id, textures, etc.), and updates existing ++ * properties (e.g. name, textures, etc.) to their official and up-to-date ++ * values. This operation does not alter the current profile, but produces a ++ * new updated {@link PlayerProfile}. ++ *

++ * If no player exists for the unique id or name of this profile, this ++ * operation yields a profile that is equal to the current profile, which ++ * might not be complete. ++ *

++ * This is an asynchronous operation: Updating the profile can result in an ++ * outgoing connection in another thread in order to fetch the latest ++ * profile properties. The returned {@link CompletableFuture} will be ++ * completed once the updated profile is available. In order to not block ++ * the server's main thread, you should not wait for the result of the ++ * returned CompletableFuture on the server's main thread. Instead, if you ++ * want to do something with the updated player profile on the server's main ++ * thread once it is available, you could do something like this: ++ *

++     * profile.resolve().thenAcceptAsync(updatedProfile -> {
++     *     // Do something with the updated profile:
++     *     // ...
++     * }, runnable -> Bukkit.getScheduler().runTask(plugin, runnable));
++     * 
++ */ ++ @Contract(pure = true) ++ CompletableFuture resolve(); ++ ++ /** ++ * Builder for {@link ResolvableProfile}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ /** ++ * Sets the name for this profile. Must be 16-or-less ++ * characters and not contain invalid characters. ++ * ++ * @param name the name ++ * @return the builder for chaining ++ * @see #name() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder name(@Pattern("^[!-~]{0,16}$") @Nullable String name); ++ ++ /** ++ * Sets the UUID for this profile. ++ * ++ * @param uuid the UUID ++ * @return the builder for chaining ++ * @see #uuid() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder uuid(@Nullable UUID uuid); ++ ++ /** ++ * Adds a property to this profile. ++ * ++ * @param property the property ++ * @return the builder for chaining ++ * @see #properties() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addProperty(ProfileProperty property); ++ ++ /** ++ * Adds properties to this profile. ++ * ++ * @param properties the properties ++ * @return the builder for chaining ++ * @see #properties() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addProperties(Collection properties); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/SeededContainerLoot.java b/src/main/java/io/papermc/paper/datacomponent/item/SeededContainerLoot.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f79af65e8f3f8ffbb9be1cf1c6b537cd1e2b1031 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/SeededContainerLoot.java +@@ -0,0 +1,71 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import net.kyori.adventure.key.Key; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds the loot table and seed for a container. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#CONTAINER_LOOT ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface SeededContainerLoot { ++ ++ @Contract(value = "_, _ -> new", pure = true) ++ static SeededContainerLoot seededContainerLoot(final Key lootTableKey, final long seed) { ++ return SeededContainerLoot.seededContainerLoot(lootTableKey).seed(seed).build(); ++ } ++ ++ @Contract(value = "_ -> new", pure = true) ++ static SeededContainerLoot.Builder seededContainerLoot(final Key lootTableKey) { ++ return ItemComponentTypesBridge.bridge().seededContainerLoot(lootTableKey); ++ } ++ ++ /** ++ * Gets the loot table key. ++ * ++ * @return the loot table key ++ */ ++ @Contract(pure = true) ++ Key lootTable(); ++ ++ /** ++ * Gets the loot table seed. ++ * ++ * @return the seed ++ */ ++ @Contract(pure = true) ++ long seed(); ++ ++ /** ++ * Builder for {@link SeededContainerLoot}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ /** ++ * Sets the loot table key. ++ * ++ * @param key the loot table key ++ * @return the builder for chaining ++ * @see #lootTable() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder lootTable(Key key); ++ ++ /** ++ * Sets the loot table seed. ++ * ++ * @param seed the seed ++ * @return the builder for chaining ++ * @see #seed() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder seed(long seed); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/ShownInTooltip.java b/src/main/java/io/papermc/paper/datacomponent/item/ShownInTooltip.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7e058aebcbd768517f6db51540598721cdae4425 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ShownInTooltip.java +@@ -0,0 +1,52 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds the state of whether a data component should be shown ++ * in an item's tooltip. ++ * @param the data component type ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface ShownInTooltip { ++ ++ /** ++ * Gets if the data component should be shown in the item's tooltip. ++ * ++ * @return {@code true} to show in the tooltip ++ */ ++ @Contract(pure = true) ++ boolean showInTooltip(); ++ ++ /** ++ * Returns a copy of this data component with the specified ++ * show-in-tooltip state. ++ * @param showInTooltip {@code true} to show in the tooltip ++ * @return the new data component ++ */ ++ @Contract(value = "_ -> new", pure = true) ++ T showInTooltip(boolean showInTooltip); ++ ++ /** ++ * A builder for creating a {@link ShownInTooltip} data component. ++ * @param builder type ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder { ++ ++ /** ++ * Sets if the data component should be shown in the item's tooltip. ++ * ++ * @param showInTooltip {@code true} to show in the tooltip ++ * @return the builder for chaining ++ * @see #showInTooltip() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ B showInTooltip(boolean showInTooltip); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/SuspiciousStewEffects.java b/src/main/java/io/papermc/paper/datacomponent/item/SuspiciousStewEffects.java +new file mode 100644 +index 0000000000000000000000000000000000000000..422bb5ccc42b94b60fba6714e9e6fb8aced6eb0c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/SuspiciousStewEffects.java +@@ -0,0 +1,67 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import io.papermc.paper.potion.SuspiciousEffectEntry; ++import java.util.Arrays; ++import java.util.Collection; ++import java.util.List; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.Unmodifiable; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds the effects that will be applied when consuming Suspicious Stew. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#SUSPICIOUS_STEW_EFFECTS ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface SuspiciousStewEffects { ++ ++ @Contract(value = "_ -> new", pure = true) ++ static SuspiciousStewEffects suspiciousStewEffects(final Collection effects) { ++ return suspiciousStewEffects().addAll(effects).build(); ++ } ++ ++ @Contract(value = "-> new", pure = true) ++ static SuspiciousStewEffects.Builder suspiciousStewEffects() { ++ return ItemComponentTypesBridge.bridge().suspiciousStewEffects(); ++ } ++ ++ /** ++ * Effects that will be applied when consuming Suspicious Stew. ++ * ++ * @return effects ++ */ ++ @Contract(pure = true) ++ @Unmodifiable List effects(); ++ ++ /** ++ * Builder for {@link SuspiciousStewEffects}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ /** ++ * Adds an effect applied to this builder. ++ * ++ * @param entry effect ++ * @return the builder for chaining ++ * @see #effects() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder add(SuspiciousEffectEntry entry); ++ ++ /** ++ * Adds effects applied to this builder. ++ * ++ * @param entries effect ++ * @return the builder for chaining ++ * @see #effects() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addAll(Collection entries); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/Tool.java b/src/main/java/io/papermc/paper/datacomponent/item/Tool.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4e87feb83204266e1fefdafe7b7e5ac53da3160e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/Tool.java +@@ -0,0 +1,149 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import io.papermc.paper.registry.set.RegistryKeySet; ++import java.util.Collection; ++import java.util.List; ++import net.kyori.adventure.util.TriState; ++import org.bukkit.block.BlockType; ++import org.checkerframework.checker.index.qual.NonNegative; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.Unmodifiable; ++import org.jspecify.annotations.NullMarked; ++import org.jspecify.annotations.Nullable; ++ ++/** ++ * Controls the behavior of the item as a tool. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#TOOL ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface Tool { ++ ++ @Contract(value = "-> new", pure = true) ++ static Tool.Builder tool() { ++ return ItemComponentTypesBridge.bridge().tool(); ++ } ++ ++ /** ++ * Creates a mining rule that specifies how an item interacts with certain block types. ++ * ++ *

This method allows you to define a rule for a set of block types, optionally setting a custom mining speed ++ * and determining whether the item should correct for drops when mining these blocks.

++ * ++ * @param blocks The set of block types this rule applies to. ++ * @param speed The custom mining speed multiplier for these blocks. If {@code null}, the default speed is used. ++ * @param correctForDrops A {@link TriState} indicating how to handle item drops: ++ *
    ++ *
  • {@link TriState#TRUE} - Items will be dropped.
  • ++ *
  • {@link TriState#FALSE} - Items will not be dropped.
  • ++ *
  • {@link TriState#NOT_SET} - The default drop behavior is used.
  • ++ *
++ * @return A new {@link Rule} instance representing the mining rule. ++ */ ++ static Rule rule(final RegistryKeySet blocks, final @Nullable Float speed, final TriState correctForDrops) { ++ return ItemComponentTypesBridge.bridge().rule(blocks, speed, correctForDrops); ++ } ++ ++ /** ++ * Mining speed to use if no rules match and don't override mining speed. ++ * ++ * @return default mining speed ++ */ ++ @Contract(pure = true) ++ float defaultMiningSpeed(); ++ ++ /** ++ * Amount of durability to remove each time a block is mined with this tool. ++ * ++ * @return durability ++ */ ++ @Contract(pure = true) ++ @NonNegative int damagePerBlock(); ++ ++ /** ++ * List of rule entries. ++ * ++ * @return rules ++ */ ++ @Contract(pure = true) ++ @Unmodifiable List rules(); ++ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Rule { ++ ++ /** ++ * Blocks to match. ++ * ++ * @return blocks ++ */ ++ RegistryKeySet blocks(); ++ ++ /** ++ * Overrides the mining speed if present and matched. ++ *

++ * {@code true} will cause the block to mine at its most efficient speed, and drop items if the targeted block requires that. ++ * ++ * @return speed override ++ */ ++ @Nullable Float speed(); ++ ++ /** ++ * Overrides whether this tool is considered 'correct' if present and matched. ++ * ++ * @return a tri-state ++ */ ++ TriState correctForDrops(); ++ } ++ ++ /** ++ * Builder for {@link Tool}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ /** ++ * Controls the amount of durability to remove each time a block is mined with this tool. ++ * ++ * @param damage durability to remove ++ * @return the builder for chaining ++ * @see #damagePerBlock() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder damagePerBlock(@NonNegative int damage); ++ ++ /** ++ * Controls mining speed to use if no rules match and don't override mining speed. ++ * ++ * @param miningSpeed mining speed ++ * @return the builder for chaining ++ * @see #defaultMiningSpeed() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder defaultMiningSpeed(float miningSpeed); ++ ++ /** ++ * Adds a rule to the tool that controls the breaking speed / damage per block if matched. ++ * ++ * @param rule rule ++ * @return the builder for chaining ++ * @see #rules() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addRule(Rule rule); ++ ++ /** ++ * Adds rules to the tool that control the breaking speed / damage per block if matched. ++ * ++ * @param rules rules ++ * @return the builder for chaining ++ * @see #rules() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addRules(Collection rules); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/Unbreakable.java b/src/main/java/io/papermc/paper/datacomponent/item/Unbreakable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..498eb479dce406d2b0b470b327eac8279a0d98bc +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/Unbreakable.java +@@ -0,0 +1,34 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * If set, the item will not lose any durability when used. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#UNBREAKABLE ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface Unbreakable extends ShownInTooltip { ++ ++ @Contract(value = "_ -> new", pure = true) ++ static Unbreakable unbreakable(final boolean showInTooltip) { ++ return unbreakable().showInTooltip(showInTooltip).build(); ++ } ++ ++ @Contract(value = "-> new", pure = true) ++ static Unbreakable.Builder unbreakable() { ++ return ItemComponentTypesBridge.bridge().unbreakable(); ++ } ++ ++ /** ++ * Builder for {@link Unbreakable}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends ShownInTooltip.Builder, DataComponentBuilder { ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/UseCooldown.java b/src/main/java/io/papermc/paper/datacomponent/item/UseCooldown.java +new file mode 100644 +index 0000000000000000000000000000000000000000..57fc55ad1def2bb14fc0a95ee3c0c157b0ac53fb +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/UseCooldown.java +@@ -0,0 +1,65 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import net.kyori.adventure.key.Key; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++import org.jspecify.annotations.Nullable; ++ ++/** ++ * Holds the contents of cooldown information when an item is used. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#USE_COOLDOWN ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface UseCooldown { ++ ++ /** ++ * Creates a new builder for use cooldown. ++ * ++ * @param seconds the duration in seconds; must be positive ++ * @return builder ++ */ ++ @Contract(value = "_ -> new", pure = true) ++ static UseCooldown.Builder useCooldown(final float seconds) { ++ return ItemComponentTypesBridge.bridge().useCooldown(seconds); ++ } ++ ++ /** ++ * The amount of seconds the cooldown will be active for. ++ * ++ * @return cooldown seconds ++ */ ++ @Contract(pure = true) ++ float seconds(); ++ ++ /** ++ * The unique resource location to identify this cooldown group. ++ *

++ * This allows items to share cooldowns with other items in the same cooldown group, if present. ++ * ++ * @return cooldown group, or null if not present ++ */ ++ @Contract(pure = true) ++ @Nullable Key cooldownGroup(); ++ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ /** ++ * Sets a unique resource location for this cooldown group. ++ *

++ * This allows items to share cooldowns with other items in the same cooldown group. ++ *

++ * ++ * @param key the unique resource location; can be null ++ * @return the builder for chaining ++ * @see #cooldownGroup() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder cooldownGroup(@Nullable Key key); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/UseRemainder.java b/src/main/java/io/papermc/paper/datacomponent/item/UseRemainder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..50e42e073311332142980828d0beec1828570512 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/UseRemainder.java +@@ -0,0 +1,31 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import org.bukkit.inventory.ItemStack; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds the contents of item transformation information when an item is used. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#USE_REMAINDER ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface UseRemainder { ++ ++ @Contract(value = "_ -> new", pure = true) ++ static UseRemainder useRemainder(final ItemStack itemStack) { ++ return ItemComponentTypesBridge.bridge().useRemainder(itemStack); ++ } ++ ++ /** ++ * The item that the item that is consumed is transformed into. ++ * ++ * @return item ++ */ ++ @Contract(value = "-> new", pure = true) ++ ItemStack transformInto(); ++ ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/WritableBookContent.java b/src/main/java/io/papermc/paper/datacomponent/item/WritableBookContent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..828d3bb1c763e0f3c89a73d6b70d1f006258644f +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/WritableBookContent.java +@@ -0,0 +1,80 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import io.papermc.paper.text.Filtered; ++import java.util.List; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.Unmodifiable; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds the pages for a writable book. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#WRITABLE_BOOK_CONTENT ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface WritableBookContent { ++ ++ @Contract(value = "-> new", pure = true) ++ static WritableBookContent.Builder writeableBookContent() { ++ return ItemComponentTypesBridge.bridge().writeableBookContent(); ++ } ++ ++ /** ++ * Holds the pages that can be written to for this component. ++ * ++ * @return pages, as filtered objects ++ */ ++ @Contract(pure = true) ++ @Unmodifiable List> pages(); ++ ++ /** ++ * Builder for {@link WritableBookContent}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ /** ++ * Adds a page that can be written to for this builder. ++ * ++ * @param page page ++ * @return the builder for chaining ++ * @see #pages() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addPage(String page); ++ ++ /** ++ * Adds pages that can be written to for this builder. ++ * ++ * @param pages pages ++ * @return the builder for chaining ++ * @see #pages() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addPages(List pages); ++ ++ /** ++ * Adds a filterable page that can be written to for this builder. ++ * ++ * @param page page ++ * @return the builder for chaining ++ * @see #pages() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addFilteredPage(Filtered page); ++ ++ /** ++ * Adds filterable pages that can be written to for this builder. ++ * ++ * @param pages pages ++ * @return the builder for chaining ++ * @see #pages() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addFilteredPages(List> pages); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/WrittenBookContent.java b/src/main/java/io/papermc/paper/datacomponent/item/WrittenBookContent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..979bc05009b84b6fcdb59938cceace351e61c78b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/WrittenBookContent.java +@@ -0,0 +1,172 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.DataComponentBuilder; ++import io.papermc.paper.text.Filtered; ++import java.util.List; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.ComponentLike; ++import org.checkerframework.common.value.qual.IntRange; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jetbrains.annotations.Unmodifiable; ++import org.jspecify.annotations.NullMarked; ++ ++/** ++ * Holds the contents and metadata of a Written Book. ++ * @see io.papermc.paper.datacomponent.DataComponentTypes#WRITTEN_BOOK_CONTENT ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface WrittenBookContent { ++ ++ @Contract(value = "_, _ -> new", pure = true) ++ static WrittenBookContent.Builder writtenBookContent(final String title, final String author) { ++ return writtenBookContent(Filtered.of(title, null), author); ++ } ++ ++ @Contract(value = "_, _ -> new", pure = true) ++ static WrittenBookContent.Builder writtenBookContent(final Filtered title, final String author) { ++ return ItemComponentTypesBridge.bridge().writtenBookContent(title, author); ++ } ++ ++ /** ++ * Title of this book. ++ * ++ * @return title ++ */ ++ @Contract(pure = true) ++ Filtered title(); ++ ++ /** ++ * Player name of the author of this book. ++ * ++ * @return author ++ */ ++ @Contract(pure = true) ++ String author(); ++ ++ /** ++ * The number of times this book has been copied (0 = original). ++ * ++ * @return generation ++ */ ++ @Contract(pure = true) ++ @IntRange(from = 0, to = 3) int generation(); ++ ++ /** ++ * Gets the pages of this book. ++ * ++ * @return pages ++ */ ++ @Contract(pure = true) ++ @Unmodifiable List> pages(); ++ ++ /** ++ * If the chat components in this book have already been resolved (entity selectors, scores substituted). ++ * If {@code false}, will be resolved when opened by a player. ++ * ++ * @return resolved ++ */ ++ @Contract(pure = true) ++ boolean resolved(); ++ ++ /** ++ * Builder for {@link WrittenBookContent}. ++ */ ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface Builder extends DataComponentBuilder { ++ ++ /** ++ * Sets the title of this book. ++ * ++ * @param title the title ++ * @return the builder for chaining ++ * @see #title() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder title(String title); ++ ++ /** ++ * Sets the filterable title of this book. ++ * ++ * @param title the title ++ * @return the builder for chaining ++ * @see #title() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder filteredTitle(Filtered title); ++ ++ /** ++ * Sets the author of this book. ++ * ++ * @param author the author ++ * @return the builder for chaining ++ * @see #author() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder author(String author); ++ ++ /** ++ * Sets the generation of this book. ++ * ++ * @param generation the generation, [0-3] ++ * @return the builder for chaining ++ * @see #generation() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder generation(@IntRange(from = 0, to = 3) int generation); ++ ++ /** ++ * Sets if the chat components in this book have already been resolved (entity selectors, scores substituted). ++ * If {@code false}, will be resolved when opened by a player. ++ * ++ * @param resolved resolved ++ * @return the builder for chaining ++ * @see #resolved() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder resolved(boolean resolved); ++ ++ /** ++ * Adds a page to this book. ++ * ++ * @param page the page ++ * @return the builder for chaining ++ * @see #pages() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addPage(ComponentLike page); ++ ++ /** ++ * Adds pages to this book. ++ * ++ * @param page the pages ++ * @return the builder for chaining ++ * @see #pages() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addPages(List page); ++ ++ /** ++ * Adds a filterable page to this book. ++ * ++ * @param page the page ++ * @return the builder for chaining ++ * @see #pages() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addFilteredPage(Filtered page); ++ ++ /** ++ * Adds filterable pages to this book. ++ * ++ * @param pages the pages ++ * @return the builder for chaining ++ * @see #pages() ++ */ ++ @Contract(value = "_ -> this", mutates = "this") ++ Builder addFilteredPages(List> pages); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/ConsumableTypesBridge.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/ConsumableTypesBridge.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a845ccfc21f101f0632249745bbd8b334f85e72c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/ConsumableTypesBridge.java +@@ -0,0 +1,32 @@ ++package io.papermc.paper.datacomponent.item.consumable; ++ ++import io.papermc.paper.registry.set.RegistryKeySet; ++import java.util.List; ++import java.util.Optional; ++import java.util.ServiceLoader; ++import net.kyori.adventure.key.Key; ++import org.bukkit.potion.PotionEffect; ++import org.bukkit.potion.PotionEffectType; ++import org.jetbrains.annotations.ApiStatus; ++import org.jspecify.annotations.NullMarked; ++ ++@NullMarked ++@ApiStatus.Internal ++interface ConsumableTypesBridge { ++ ++ Optional BRIDGE = ServiceLoader.load(ConsumableTypesBridge.class).findFirst(); ++ ++ static ConsumableTypesBridge bridge() { ++ return BRIDGE.orElseThrow(); ++ } ++ ++ ConsumeEffect.ApplyStatusEffects applyStatusEffects(List effectList, float probability); ++ ++ ConsumeEffect.RemoveStatusEffects removeStatusEffects(RegistryKeySet potionEffectTypeTagKey); ++ ++ ConsumeEffect.ClearAllStatusEffects clearAllStatusEffects(); ++ ++ ConsumeEffect.PlaySound playSoundEffect(Key sound); ++ ++ ConsumeEffect.TeleportRandomly teleportRandomlyEffect(float diameter); ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/ConsumeEffect.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/ConsumeEffect.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ff1a14e19c21dd22f249503a0b738f190a75aca0 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/ConsumeEffect.java +@@ -0,0 +1,150 @@ ++package io.papermc.paper.datacomponent.item.consumable; ++ ++import io.papermc.paper.registry.set.RegistryKeySet; ++import net.kyori.adventure.key.Key; ++import org.bukkit.potion.PotionEffect; ++import org.bukkit.potion.PotionEffectType; ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++import java.util.List; ++ ++/** ++ * Effect that occurs when consuming an item. ++ */ ++@NullMarked ++@ApiStatus.Experimental ++@ApiStatus.NonExtendable ++public interface ConsumeEffect { ++ ++ /** ++ * Creates a consume effect that randomly teleports the entity on consumption. ++ * ++ * @param diameter diameter of random teleportation ++ * @return the effect ++ */ ++ @Contract(value = "_ -> new", pure = true) ++ static TeleportRandomly teleportRandomlyEffect(final float diameter) { ++ return ConsumableTypesBridge.bridge().teleportRandomlyEffect(diameter); ++ } ++ ++ /** ++ * Creates a consume effect that gives status effects on consumption. ++ * ++ * @param key the sound effect to play ++ * @return the effect ++ */ ++ @Contract(value = "_ -> new", pure = true) ++ static RemoveStatusEffects removeEffects(final RegistryKeySet key) { ++ return ConsumableTypesBridge.bridge().removeStatusEffects(key); ++ } ++ ++ /** ++ * Creates a consume effect that plays a sound on consumption. ++ * ++ * @param key the sound effect to play ++ * @return the effect ++ */ ++ @Contract(value = "_ -> new", pure = true) ++ static PlaySound playSoundConsumeEffect(final Key key) { ++ return ConsumableTypesBridge.bridge().playSoundEffect(key); ++ } ++ ++ /** ++ * Creates a consume effect that clears all status effects. ++ * ++ * @return effect instance ++ */ ++ @Contract(value = "-> new", pure = true) ++ static ClearAllStatusEffects clearAllStatusEffects() { ++ return ConsumableTypesBridge.bridge().clearAllStatusEffects(); ++ } ++ ++ /** ++ * Creates a consume effect that gives status effects on consumption. ++ * ++ * @param effects the potion effects to apply ++ * @param probability the probability of these effects being applied, between 0 and 1 inclusive. ++ * @return the effect ++ */ ++ @Contract(value = "_, _ -> new", pure = true) ++ static ApplyStatusEffects applyStatusEffects(final List effects, final float probability) { ++ return ConsumableTypesBridge.bridge().applyStatusEffects(effects, probability); ++ } ++ ++ @NullMarked ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface TeleportRandomly extends ConsumeEffect { ++ ++ /** ++ * The max range that the entity can be teleported to. ++ * ++ * @return teleportation diameter ++ */ ++ float diameter(); ++ } ++ ++ /** ++ * Represents a consumable effect that removes status effects on consumption ++ */ ++ @NullMarked ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface RemoveStatusEffects extends ConsumeEffect { ++ ++ /** ++ * Potion effects to remove ++ * ++ * @return effects ++ */ ++ RegistryKeySet removeEffects(); ++ } ++ ++ /** ++ * Represents a consumable effect that plays a sound on consumption. ++ */ ++ @NullMarked ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface PlaySound extends ConsumeEffect { ++ ++ /** ++ * Sound effect to play in the world ++ * ++ * @return sound effect ++ */ ++ Key sound(); ++ } ++ ++ /** ++ * Represents a consumable effect that clears all effects on consumption. ++ */ ++ @NullMarked ++ interface ClearAllStatusEffects extends ConsumeEffect { ++ ++ } ++ ++ /** ++ * Represents a consumable effect that applies effects based on a probability on consumption. ++ */ ++ @NullMarked ++ @ApiStatus.Experimental ++ @ApiStatus.NonExtendable ++ interface ApplyStatusEffects extends ConsumeEffect { ++ ++ /** ++ * Effect instances to grant ++ * ++ * @return effect ++ */ ++ List effects(); ++ ++ /** ++ * Float between 0 and 1, chance for the effect to be applied. ++ * ++ * @return chance ++ */ ++ float probability(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/ItemUseAnimation.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/ItemUseAnimation.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8cd6dbe4ea5ee3270b9428a9c29cbd88823d9f6c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/ItemUseAnimation.java +@@ -0,0 +1,17 @@ ++package io.papermc.paper.datacomponent.item.consumable; ++ ++/** ++ * Represents the hand animation that is used when a player is consuming this item. ++ */ ++public enum ItemUseAnimation { ++ NONE, ++ EAT, ++ DRINK, ++ BLOCK, ++ BOW, ++ SPEAR, ++ CROSSBOW, ++ SPYGLASS, ++ TOOT_HORN, ++ BRUSH ++} +diff --git a/src/main/java/io/papermc/paper/item/MapPostProcessing.java b/src/main/java/io/papermc/paper/item/MapPostProcessing.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5843768d0be2ae4a0219636ed7640727808da567 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/item/MapPostProcessing.java +@@ -0,0 +1,6 @@ ++package io.papermc.paper.item; ++ ++public enum MapPostProcessing { ++ LOCK, ++ SCALE ++} +diff --git a/src/main/java/io/papermc/paper/registry/RegistryKey.java b/src/main/java/io/papermc/paper/registry/RegistryKey.java +index 9b39e33514b15a9d07104e2ad826d0da11f569d6..e5319bdb9f75358b8bb0ac35373125a7d94edfa6 100644 +--- a/src/main/java/io/papermc/paper/registry/RegistryKey.java ++++ b/src/main/java/io/papermc/paper/registry/RegistryKey.java +@@ -1,5 +1,6 @@ + package io.papermc.paper.registry; + ++import io.papermc.paper.datacomponent.DataComponentType; + import net.kyori.adventure.key.Keyed; + import org.bukkit.Art; + import org.bukkit.Fluid; +@@ -124,6 +125,11 @@ public sealed interface RegistryKey extends Keyed permits RegistryKeyImpl { + * @see io.papermc.paper.registry.keys.SoundEventKeys + */ + RegistryKey SOUND_EVENT = create("sound_event"); ++ /** ++ * Built-in registry for data component types. ++ * ++ */ ++ RegistryKey DATA_COMPONENT_TYPE = create("data_component_type"); + + + +diff --git a/src/main/java/io/papermc/paper/text/Filtered.java b/src/main/java/io/papermc/paper/text/Filtered.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9e892621354c784632204559f9fdf0827b3bc4f1 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/text/Filtered.java +@@ -0,0 +1,32 @@ ++package io.papermc.paper.text; ++ ++import org.jetbrains.annotations.ApiStatus; ++import org.jetbrains.annotations.Contract; ++import org.jspecify.annotations.NullMarked; ++import org.jspecify.annotations.Nullable; ++ ++/** ++ * Denotes that this type is filterable by the client, and may be shown differently ++ * depending on the player's set configuration. ++ * ++ * @param type of value ++ */ ++@ApiStatus.Experimental ++@NullMarked ++public interface Filtered { ++ ++ @Contract(value = "_, _ -> new", pure = true) ++ static Filtered of(final T raw, final @Nullable T filtered) { ++ @ApiStatus.Internal ++ record Instance(T raw, @Nullable T filtered) implements Filtered {} ++ ++ return new Instance<>(raw, filtered); ++ } ++ ++ @Contract(pure = true) ++ T raw(); ++ ++ @Contract(pure = true) ++ @Nullable ++ T filtered(); ++} +diff --git a/src/main/java/org/bukkit/Material.java b/src/main/java/org/bukkit/Material.java +index 615eb24ffdd8f6d55ccd4f21760b809c1098bc68..c7ce8fa1ff9feda66d5a4e497112a24ff51c9d2b 100644 +--- a/src/main/java/org/bukkit/Material.java ++++ b/src/main/java/org/bukkit/Material.java +@@ -137,7 +137,7 @@ import org.jetbrains.annotations.Nullable; + @SuppressWarnings({"DeprecatedIsStillUsed", "deprecation"}) // Paper + public enum Material implements Keyed, Translatable, net.kyori.adventure.translation.Translatable { // Paper + // +- AIR(9648, 0), ++ AIR(9648, 64), // Paper - air stacks to 64 + STONE(22948), + GRANITE(21091), + POLISHED_GRANITE(5477), +@@ -5784,6 +5784,7 @@ public enum Material implements Keyed, Translatable, net.kyori.adventure.transla + */ + @ApiStatus.Internal + @Nullable ++ @org.jetbrains.annotations.Contract(pure = true) // Paper + public ItemType asItemType() { + return itemType.get(); + } +@@ -5796,7 +5797,47 @@ public enum Material implements Keyed, Translatable, net.kyori.adventure.transla + */ + @ApiStatus.Internal + @Nullable ++ @org.jetbrains.annotations.Contract(pure = true) // Paper + public BlockType asBlockType() { + return blockType.get(); + } ++ ++ // Paper start - data component API ++ /** ++ * Gets the default value of the data component type for this item type. ++ * ++ * @param type the data component type ++ * @param the value type ++ * @return the default value or {@code null} if there is none ++ * @see #hasDefaultData(io.papermc.paper.datacomponent.DataComponentType) for DataComponentType.NonValued ++ * @throws IllegalArgumentException if {@link #isItem()} is {@code false} ++ */ ++ public @Nullable T getDefaultData(final io.papermc.paper.datacomponent.DataComponentType.@NotNull Valued type) { ++ Preconditions.checkArgument(this.asItemType() != null); ++ return this.asItemType().getDefaultData(type); ++ } ++ ++ /** ++ * Checks if the data component type has a default value for this item type. ++ * ++ * @param type the data component type ++ * @return {@code true} if there is a default value ++ * @throws IllegalArgumentException if {@link #isItem()} is {@code false} ++ */ ++ public boolean hasDefaultData(final io.papermc.paper.datacomponent.@NotNull DataComponentType type) { ++ Preconditions.checkArgument(this.asItemType() != null); ++ return this.asItemType().hasDefaultData(type); ++ } ++ ++ /** ++ * Gets the default data component types for this item type. ++ * ++ * @return an immutable set of data component types ++ * @throws IllegalArgumentException if {@link #isItem()} is {@code false} ++ */ ++ public java.util.@org.jetbrains.annotations.Unmodifiable @NotNull Set getDefaultDataTypes() { ++ Preconditions.checkArgument(this.asItemType() != null); ++ return this.asItemType().getDefaultDataTypes(); ++ } ++ // Paper end - data component API + } +diff --git a/src/main/java/org/bukkit/Registry.java b/src/main/java/org/bukkit/Registry.java +index 7cf7c6d05aa6cbf3f0c8612831404552c6a7b84a..c60e31425efd7b863941f5538faef6c0552290ae 100644 +--- a/src/main/java/org/bukkit/Registry.java ++++ b/src/main/java/org/bukkit/Registry.java +@@ -376,6 +376,7 @@ public interface Registry extends Iterable { + */ + Registry POTION_EFFECT_TYPE = EFFECT; + // Paper end - potion effect type registry ++ Registry DATA_COMPONENT_TYPE = io.papermc.paper.registry.RegistryAccess.registryAccess().getRegistry(io.papermc.paper.registry.RegistryKey.DATA_COMPONENT_TYPE); // Paper + /** + * Get the object by its key. + * +diff --git a/src/main/java/org/bukkit/block/BlockType.java b/src/main/java/org/bukkit/block/BlockType.java +index ed534fe4983873a2d5f623f0d9d5e3ce254615eb..f019d490794b49d21057820bab047e2f909934a1 100644 +--- a/src/main/java/org/bukkit/block/BlockType.java ++++ b/src/main/java/org/bukkit/block/BlockType.java +@@ -128,7 +128,7 @@ import org.jetbrains.annotations.Nullable; + * official replacement for the aforementioned enum. Entirely incompatible + * changes may occur. Do not use this API in plugins. + */ +-@ApiStatus.Internal ++@org.jetbrains.annotations.ApiStatus.Experimental // Paper - data component API - already required for data component API + public interface BlockType extends Keyed, Translatable, net.kyori.adventure.translation.Translatable, io.papermc.paper.world.flag.FeatureDependant { // Paper - add translatable & feature flag API + + /** +diff --git a/src/main/java/org/bukkit/inventory/ItemStack.java b/src/main/java/org/bukkit/inventory/ItemStack.java +index b59222b8c262545d100a9fd28b3bf1d2a4cf4eb0..6e4ca7d95953a25c0aaafd35e54ef9254a1b5f0b 100644 +--- a/src/main/java/org/bukkit/inventory/ItemStack.java ++++ b/src/main/java/org/bukkit/inventory/ItemStack.java +@@ -1137,4 +1137,185 @@ public class ItemStack implements Cloneable, ConfigurationSerializable, Translat + return Bukkit.getUnsafe().computeTooltipLines(this, tooltipContext, player); + } + // Paper end - expose itemstack tooltip lines ++ ++ // Paper start - data component API ++ /** ++ * Gets the value for the data component type on this stack. ++ * ++ * @param type the data component type ++ * @param the value type ++ * @return the value for the data component type, or {@code null} if not set or marked as removed ++ * @see #hasData(io.papermc.paper.datacomponent.DataComponentType) for DataComponentType.NonValued ++ */ ++ @org.jetbrains.annotations.Contract(pure = true) ++ @org.jetbrains.annotations.ApiStatus.Experimental ++ public @Nullable T getData(final io.papermc.paper.datacomponent.DataComponentType.@NotNull Valued type) { ++ return this.craftDelegate.getData(type); ++ } ++ ++ /** ++ * Gets the value for the data component type on this stack with ++ * a fallback value. ++ * ++ * @param type the data component type ++ * @param fallback the fallback value if the value isn't present ++ * @param the value type ++ * @return the value for the data component type or the fallback value ++ */ ++ @Utility ++ @org.jetbrains.annotations.Contract(value = "_, !null -> !null", pure = true) ++ @org.jetbrains.annotations.ApiStatus.Experimental ++ public @Nullable T getDataOrDefault(final io.papermc.paper.datacomponent.DataComponentType.@NotNull Valued type, final @Nullable T fallback) { ++ final T object = this.getData(type); ++ return object != null ? object : fallback; ++ } ++ ++ /** ++ * Checks if the data component type is set on the itemstack. ++ * ++ * @param type the data component type ++ * @return {@code true} if set, {@code false} otherwise ++ */ ++ @org.jetbrains.annotations.Contract(pure = true) ++ @org.jetbrains.annotations.ApiStatus.Experimental ++ public boolean hasData(final io.papermc.paper.datacomponent.@NotNull DataComponentType type) { ++ return this.craftDelegate.hasData(type); ++ } ++ ++ /** ++ * Gets all the data component types set on this stack. ++ * ++ * @return an immutable set of data component types ++ */ ++ @org.jetbrains.annotations.Contract("-> new") ++ @org.jetbrains.annotations.ApiStatus.Experimental ++ public java.util.@org.jetbrains.annotations.Unmodifiable Set getDataTypes() { ++ return this.craftDelegate.getDataTypes(); ++ } ++ ++ /** ++ * Sets the value of the data component type for this itemstack. To ++ * reset the value to the default for the {@link #getType() item type}, use ++ * {@link #resetData(io.papermc.paper.datacomponent.DataComponentType)}. To mark the data component type ++ * as removed, use {@link #unsetData(io.papermc.paper.datacomponent.DataComponentType)}. ++ * ++ * @param type the data component type ++ * @param valueBuilder value builder ++ * @param value type ++ */ ++ @Utility ++ @org.jetbrains.annotations.ApiStatus.Experimental ++ public void setData(final io.papermc.paper.datacomponent.DataComponentType.@NotNull Valued type, final @NotNull io.papermc.paper.datacomponent.DataComponentBuilder valueBuilder) { ++ this.setData(type, valueBuilder.build()); ++ } ++ ++ // /** ++ // * Modifies the value of the specified data component type for this item stack based on the result ++ // * of applying a given function to the current value. ++ // * ++ // *

If the function returns {@code null}, the data component type will be reset using ++ // * {@link #unsetData(DataComponentType)}. Otherwise, the ++ // * component value will be updated with the new result using {@link #setData(DataComponentType.Valued, Object)}.

++ // * ++ // * @param the type of the data component's value ++ // * @param type the data component type to be modified ++ // * @param consumer a function that takes the current component value (can be {@code null}) and ++ // * returns the modified value (or {@code null} to unset) ++ // */ ++ // @Utility ++ // public void editData(final io.papermc.paper.datacomponent.DataComponentType.@NotNull Valued type, final @NotNull java.util.function.Function<@Nullable T, @Nullable T> consumer) { ++ // T value = getData(type); ++ // T newType = consumer.apply(value); ++ // if (newType == null) { ++ // unsetData(type); ++ // } else { ++ // setData(type, newType); ++ // } ++ // } ++ ++ /** ++ * Sets the value of the data component type for this itemstack. To ++ * reset the value to the default for the {@link #getType() item type}, use ++ * {@link #resetData(io.papermc.paper.datacomponent.DataComponentType)}. To mark the data component type ++ * as removed, use {@link #unsetData(io.papermc.paper.datacomponent.DataComponentType)}. ++ * ++ * @param type the data component type ++ * @param value value to set ++ * @param value type ++ */ ++ @org.jetbrains.annotations.ApiStatus.Experimental ++ public void setData(final io.papermc.paper.datacomponent.DataComponentType.@NotNull Valued type, final @NotNull T value) { ++ this.craftDelegate.setData(type, value); ++ } ++ ++ /** ++ * Marks this non-valued data component type as present in this itemstack. ++ * ++ * @param type the data component type ++ */ ++ @org.jetbrains.annotations.ApiStatus.Experimental ++ public void setData(final io.papermc.paper.datacomponent.DataComponentType.@NotNull NonValued type) { ++ this.craftDelegate.setData(type); ++ } ++ ++ /** ++ * Marks this data component as removed for this itemstack. ++ * ++ * @param type the data component type ++ */ ++ @org.jetbrains.annotations.ApiStatus.Experimental ++ public void unsetData(final io.papermc.paper.datacomponent.@NotNull DataComponentType type) { ++ this.craftDelegate.unsetData(type); ++ } ++ ++ /** ++ * Resets the value of this component to be the default ++ * value for the item type from {@link Material#getDefaultData(io.papermc.paper.datacomponent.DataComponentType.Valued)}. ++ * ++ * @param type the data component type ++ */ ++ @org.jetbrains.annotations.ApiStatus.Experimental ++ public void resetData(final io.papermc.paper.datacomponent.@NotNull DataComponentType type) { ++ this.craftDelegate.resetData(type); ++ } ++ ++ /** ++ * Checks if the data component type is overridden from the default for the ++ * item type. ++ * ++ * @param type the data component type ++ * @return {@code true} if the data type is overridden ++ */ ++ @org.jetbrains.annotations.ApiStatus.Experimental ++ public boolean isDataOverridden(final io.papermc.paper.datacomponent.@NotNull DataComponentType type) { ++ return this.craftDelegate.isDataOverridden(type); ++ } ++ ++ /** ++ * Checks if this itemstack matches another given itemstack excluding the provided components. ++ * This is useful if you are wanting to ignore certain properties of itemstacks, such as durability. ++ * ++ * @param item the item to compare ++ * @param excludeTypes the data component types to ignore ++ * @return {@code true} if the provided item is equal, ignoring the provided components ++ */ ++ @org.jetbrains.annotations.ApiStatus.Experimental ++ public boolean matchesWithoutData(final @NotNull ItemStack item, final @NotNull java.util.Set excludeTypes) { ++ return this.matchesWithoutData(item, excludeTypes, false); ++ } ++ ++ /** ++ * Checks if this itemstack matches another given itemstack excluding the provided components. ++ * This is useful if you are wanting to ignore certain properties of itemstacks, such as durability. ++ * ++ * @param item the item to compare ++ * @param excludeTypes the data component types to ignore ++ * @param ignoreCount ignore the count of the item ++ * @return {@code true} if the provided item is equal, ignoring the provided components ++ */ ++ @org.jetbrains.annotations.ApiStatus.Experimental ++ public boolean matchesWithoutData(final @NotNull ItemStack item, final @NotNull java.util.Set excludeTypes, final boolean ignoreCount) { ++ return this.craftDelegate.matchesWithoutData(item, excludeTypes, ignoreCount); ++ } ++ // Paper end - data component API + } +diff --git a/src/main/java/org/bukkit/inventory/ItemType.java b/src/main/java/org/bukkit/inventory/ItemType.java +index 72803c00e4af576f286d2af34bf300ee554a7f3c..2a3c4f055d0e4ef009caed95152570660ab100d5 100644 +--- a/src/main/java/org/bukkit/inventory/ItemType.java ++++ b/src/main/java/org/bukkit/inventory/ItemType.java +@@ -2483,4 +2483,33 @@ public interface ItemType extends Keyed, Translatable, net.kyori.adventure.trans + */ + @Nullable ItemRarity getItemRarity(); + // Paper end - expand ItemRarity API ++ // Paper start - data component API ++ /** ++ * Gets the default value of the data component type for this item type. ++ * ++ * @param type the data component type ++ * @param the value type ++ * @return the default value or {@code null} if there is none ++ * @see #hasDefaultData(io.papermc.paper.datacomponent.DataComponentType) for DataComponentType.NonValued ++ */ ++ @org.jetbrains.annotations.ApiStatus.Experimental ++ @Nullable T getDefaultData(io.papermc.paper.datacomponent.DataComponentType.@NotNull Valued type); ++ ++ /** ++ * Checks if the data component type has a default value for this item type. ++ * ++ * @param type the data component type ++ * @return {@code true} if there is a default value ++ */ ++ @org.jetbrains.annotations.ApiStatus.Experimental ++ boolean hasDefaultData(io.papermc.paper.datacomponent.@NotNull DataComponentType type); ++ ++ /** ++ * Gets the default data component types for this item type. ++ * ++ * @return an immutable set of data component types ++ */ ++ @org.jetbrains.annotations.ApiStatus.Experimental ++ java.util.@org.jetbrains.annotations.Unmodifiable @NotNull Set getDefaultDataTypes(); ++ // Paper end - data component API + } diff --git a/patches/api/0495-Optimise-color-distance-check-in-MapPalette-by-remov.patch b/patches/api/0496-Optimise-color-distance-check-in-MapPalette-by-remov.patch similarity index 100% rename from patches/api/0495-Optimise-color-distance-check-in-MapPalette-by-remov.patch rename to patches/api/0496-Optimise-color-distance-check-in-MapPalette-by-remov.patch diff --git a/patches/server/0009-MC-Utils.patch b/patches/server/0009-MC-Utils.patch index 4f57aa7015..108f5992a4 100644 --- a/patches/server/0009-MC-Utils.patch +++ b/patches/server/0009-MC-Utils.patch @@ -4691,16 +4691,21 @@ index 0000000000000000000000000000000000000000..197224e31175252d8438a8df585bbb65 +} diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java new file mode 100644 -index 0000000000000000000000000000000000000000..422bc104e5bdd4ae786b14d97eb779dc76bfad69 +index 0000000000000000000000000000000000000000..e85e544506b4c762503a1cb490e6c0f5b1d563f4 --- /dev/null +++ b/src/main/java/io/papermc/paper/util/MCUtil.java -@@ -0,0 +1,190 @@ +@@ -0,0 +1,220 @@ +package io.papermc.paper.util; + ++import com.google.common.collect.Collections2; ++import com.google.common.collect.Lists; +import com.google.common.util.concurrent.ThreadFactoryBuilder; ++import io.papermc.paper.adventure.PaperAdventure; +import io.papermc.paper.math.BlockPosition; +import io.papermc.paper.math.FinePosition; +import io.papermc.paper.math.Position; ++import java.util.Collection; ++import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; @@ -4709,11 +4714,17 @@ index 0000000000000000000000000000000000000000..422bc104e5bdd4ae786b14d97eb779dc +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; ++import java.util.function.Function; +import java.util.function.Supplier; ++import net.kyori.adventure.key.Key; +import net.minecraft.core.BlockPos; ++import net.minecraft.core.Holder; +import net.minecraft.core.Vec3i; ++import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceKey; ++import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; ++import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; @@ -4884,6 +4895,25 @@ index 0000000000000000000000000000000000000000..422bc104e5bdd4ae786b14d97eb779dc + public static NamespacedKey fromResourceKey(final ResourceKey key) { + return CraftNamespacedKey.fromMinecraft(key.location()); + } ++ ++ public static Holder keyToSound(Key key) { ++ ResourceLocation soundId = PaperAdventure.asVanilla(key); ++ return BuiltInRegistries.SOUND_EVENT.wrapAsHolder(BuiltInRegistries.SOUND_EVENT.getOptional(soundId).orElse(SoundEvent.createVariableRangeEvent(soundId))); ++ } ++ ++ public static List transformUnmodifiable(final List nms, final Function converter) { ++ return Collections.unmodifiableList(Lists.transform(nms, converter::apply)); ++ } ++ ++ public static Collection transformUnmodifiable(final Collection nms, final Function converter) { ++ return Collections.unmodifiableCollection(Collections2.transform(nms, converter::apply)); ++ } ++ ++ public static > void addAndConvert(final C target, final Collection toAdd, final Function converter) { ++ for (final A value : toAdd) { ++ target.add(converter.apply(value)); ++ } ++ } +} diff --git a/src/main/java/io/papermc/paper/util/StackWalkerUtil.java b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java new file mode 100644 @@ -6309,6 +6339,28 @@ index f6e6f0ddef6693c58f28b89cf3df005a8d47e08d..101eea3452c9e387e770b716543c3a4f public static net.minecraft.world.item.ItemStack asNMSCopy(ItemStack original) { if (original instanceof CraftItemStack) { CraftItemStack stack = (CraftItemStack) original; +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftLocation.java b/src/main/java/org/bukkit/craftbukkit/util/CraftLocation.java +index 097996d3955ab5126b71f7bff1dd2c62becb5ffd..a8b46ea5e4b6260c2728c67e8651b74fe6356605 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftLocation.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftLocation.java +@@ -40,6 +40,17 @@ public final class CraftLocation { + return new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()); + } + ++ // Paper start ++ public static net.minecraft.core.GlobalPos toGlobalPos(Location location) { ++ return net.minecraft.core.GlobalPos.of(((org.bukkit.craftbukkit.CraftWorld) location.getWorld()).getHandle().dimension(), toBlockPosition(location)); ++ } ++ ++ public static Location fromGlobalPos(net.minecraft.core.GlobalPos globalPos) { ++ BlockPos pos = globalPos.pos(); ++ return new org.bukkit.Location(net.minecraft.server.MinecraftServer.getServer().getLevel(globalPos.dimension()).getWorld(), pos.getX(), pos.getY(), pos.getZ()); ++ } ++ // Paper end ++ + public static Vec3 toVec3D(Location location) { + return new Vec3(location.getX(), location.getY(), location.getZ()); + } diff --git a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java index b8a865305cc61954aeebff4a7cd1d1973c5f46ab..e444662ee4d9405eeea7caa41b9cd6b36586d840 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java diff --git a/patches/server/0010-Adventure.patch b/patches/server/0010-Adventure.patch index b22e85c55b..f7659f6916 100644 --- a/patches/server/0010-Adventure.patch +++ b/patches/server/0010-Adventure.patch @@ -1161,10 +1161,10 @@ index 0000000000000000000000000000000000000000..2fd6c3e65354071af71c7d8ebb97b559 +} diff --git a/src/main/java/io/papermc/paper/adventure/PaperAdventure.java b/src/main/java/io/papermc/paper/adventure/PaperAdventure.java new file mode 100644 -index 0000000000000000000000000000000000000000..161bc8c577643094d824ea96fb6974c76e5e77f0 +index 0000000000000000000000000000000000000000..610003a668c4a7fe53e3477accc6bafb8479b936 --- /dev/null +++ b/src/main/java/io/papermc/paper/adventure/PaperAdventure.java -@@ -0,0 +1,479 @@ +@@ -0,0 +1,483 @@ +package io.papermc.paper.adventure; + +import com.mojang.brigadier.StringReader; @@ -1314,6 +1314,10 @@ index 0000000000000000000000000000000000000000..161bc8c577643094d824ea96fb6974c7 + + // Key + ++ public static Key asAdventure(final ResourceLocation key) { ++ return Key.key(key.getNamespace(), key.getPath()); ++ } ++ + public static ResourceLocation asVanilla(final Key key) { + return ResourceLocation.fromNamespaceAndPath(key.namespace(), key.value()); + } @@ -2704,7 +2708,7 @@ index 709330ca9caa82a6de71767b3d5c32f97ea1d68b..daf20aa9a83a2583c0c61a4123cc2e52 public boolean logIPs() { diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index 32e7c6e6f09e53fe8b5ade22dad8142cd09e0163..4303bde198050cd037f006234d269af406606eff 100644 +index 785c7e11f92610be58b624d252d1858658496af7..99607edee9fba8df87f0525bf0eadee865ab38a4 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java @@ -190,6 +190,7 @@ import net.minecraft.world.item.trading.MerchantOffers; @@ -3639,7 +3643,7 @@ index 8b58884d6cb1088a2fffb36a99bfe4dc568326d1..9a79b948264150d0f7a843a8ddd2ea92 // Paper end } diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java -index 94004204b6cdbbbf35263faae56e3e06cb6b650c..2e33acc428dbfd3e123dfd6ef90bc020b8a08daf 100644 +index 39126769594a535ebd4bf7052f42e9eda9d3e10a..41ceea1093edbf777f9ebe252114be7f75438420 100644 --- a/src/main/java/org/bukkit/craftbukkit/Main.java +++ b/src/main/java/org/bukkit/craftbukkit/Main.java @@ -20,6 +20,12 @@ public class Main { @@ -5202,7 +5206,7 @@ index c71a4971f127fdfc753306019313ce1a31201120..fd3b12477c30d1eabdbe57ea77902793 + // Paper end } diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java -index 5dea83c1a0de4e5123430bf2f902a78969021167..2e524520308dda7ce7df98d91c89a9cfe7542862 100644 +index 2d68086ef09b5f2d6b3d3e6200e621c23747b25a..f3789ea732b17a311d8203a58a97d11370ec7863 100644 --- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java +++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java @@ -1097,6 +1097,18 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { diff --git a/patches/server/1036-DataComponent-API.patch b/patches/server/1036-DataComponent-API.patch new file mode 100644 index 0000000000..744404bca8 --- /dev/null +++ b/patches/server/1036-DataComponent-API.patch @@ -0,0 +1,4804 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> +Date: Sun, 28 Apr 2024 19:53:01 -0400 +Subject: [PATCH] DataComponent API + +Exposes the data component logic used by vanilla ItemStack to API +consumers as a version-specific API. +The types and methods introduced by this patch do not follow the general +API deprecation contracts and will be adapted to each new minecraft +release without backwards compatibility measures. + +== AT == +public net/minecraft/world/item/component/ItemContainerContents MAX_SIZE +public net/minecraft/world/item/component/ItemContainerContents items + +diff --git a/src/main/java/io/papermc/paper/datacomponent/ComponentAdapter.java b/src/main/java/io/papermc/paper/datacomponent/ComponentAdapter.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4a49f65cae1354afbcd4afda07581790e06094be +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/ComponentAdapter.java +@@ -0,0 +1,36 @@ ++package io.papermc.paper.datacomponent; ++ ++import java.util.function.Function; ++import net.minecraft.core.component.DataComponentType; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.util.NullOps; ++import net.minecraft.util.Unit; ++import org.bukkit.craftbukkit.CraftRegistry; ++ ++public record ComponentAdapter( ++ DataComponentType type, ++ Function apiToVanilla, ++ Function vanillaToApi, ++ boolean codecValidation ++) { ++ static final Function API_TO_UNIT_CONVERTER = $ -> Unit.INSTANCE; ++ ++ public boolean isValued() { ++ return this.apiToVanilla != API_TO_UNIT_CONVERTER; ++ } ++ ++ public NMS toVanilla(final API value) { ++ final NMS nms = this.apiToVanilla.apply(value); ++ if (this.codecValidation) { ++ this.type.codecOrThrow().encodeStart(CraftRegistry.getMinecraftRegistry().createSerializationContext(NullOps.INSTANCE), nms).ifError(error -> { ++ throw new IllegalArgumentException("Failed to encode data component %s (%s)".formatted(BuiltInRegistries.DATA_COMPONENT_TYPE.getKey(this.type), error.message())); ++ }); ++ } ++ ++ return nms; ++ } ++ ++ public API fromVanilla(final NMS value) { ++ return this.vanillaToApi.apply(value); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/ComponentAdapters.java b/src/main/java/io/papermc/paper/datacomponent/ComponentAdapters.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c11de1f077c51483c61af6492f998781df866b88 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/ComponentAdapters.java +@@ -0,0 +1,168 @@ ++package io.papermc.paper.datacomponent; ++ ++import io.papermc.paper.adventure.PaperAdventure; ++import io.papermc.paper.datacomponent.item.PaperBannerPatternLayers; ++import io.papermc.paper.datacomponent.item.PaperBlockItemDataProperties; ++import io.papermc.paper.datacomponent.item.PaperBundleContents; ++import io.papermc.paper.datacomponent.item.PaperChargedProjectiles; ++import io.papermc.paper.datacomponent.item.PaperConsumable; ++import io.papermc.paper.datacomponent.item.PaperCustomModelData; ++import io.papermc.paper.datacomponent.item.PaperDamageResistant; ++import io.papermc.paper.datacomponent.item.PaperDeathProtection; ++import io.papermc.paper.datacomponent.item.PaperDyedItemColor; ++import io.papermc.paper.datacomponent.item.PaperEnchantable; ++import io.papermc.paper.datacomponent.item.PaperEquippable; ++import io.papermc.paper.datacomponent.item.PaperFireworks; ++import io.papermc.paper.datacomponent.item.PaperFoodProperties; ++import io.papermc.paper.datacomponent.item.PaperItemAdventurePredicate; ++import io.papermc.paper.datacomponent.item.PaperItemArmorTrim; ++import io.papermc.paper.datacomponent.item.PaperItemAttributeModifiers; ++import io.papermc.paper.datacomponent.item.PaperItemContainerContents; ++import io.papermc.paper.datacomponent.item.PaperItemEnchantments; ++import io.papermc.paper.datacomponent.item.PaperItemLore; ++import io.papermc.paper.datacomponent.item.PaperItemTool; ++import io.papermc.paper.datacomponent.item.PaperJukeboxPlayable; ++import io.papermc.paper.datacomponent.item.PaperLodestoneTracker; ++import io.papermc.paper.datacomponent.item.PaperMapDecorations; ++import io.papermc.paper.datacomponent.item.PaperMapId; ++import io.papermc.paper.datacomponent.item.PaperMapItemColor; ++import io.papermc.paper.datacomponent.item.PaperOminousBottleAmplifier; ++import io.papermc.paper.datacomponent.item.PaperPotDecorations; ++import io.papermc.paper.datacomponent.item.PaperPotionContents; ++import io.papermc.paper.datacomponent.item.PaperRepairable; ++import io.papermc.paper.datacomponent.item.PaperResolvableProfile; ++import io.papermc.paper.datacomponent.item.PaperSeededContainerLoot; ++import io.papermc.paper.datacomponent.item.PaperSuspiciousStewEffects; ++import io.papermc.paper.datacomponent.item.PaperUnbreakable; ++import io.papermc.paper.datacomponent.item.PaperUseCooldown; ++import io.papermc.paper.datacomponent.item.PaperUseRemainder; ++import io.papermc.paper.datacomponent.item.PaperWritableBookContent; ++import io.papermc.paper.datacomponent.item.PaperWrittenBookContent; ++import io.papermc.paper.registry.PaperRegistries; ++import java.util.HashMap; ++import java.util.Map; ++import java.util.function.Function; ++import net.minecraft.core.component.DataComponentType; ++import net.minecraft.core.component.DataComponents; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.resources.ResourceKey; ++import net.minecraft.util.Unit; ++import net.minecraft.world.item.Rarity; ++import net.minecraft.world.item.component.MapPostProcessing; ++import org.bukkit.DyeColor; ++import org.bukkit.craftbukkit.CraftMusicInstrument; ++import org.bukkit.craftbukkit.inventory.CraftMetaFirework; ++import org.bukkit.craftbukkit.util.CraftNamespacedKey; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.bukkit.inventory.ItemRarity; ++ ++import static io.papermc.paper.datacomponent.ComponentUtils.transform; ++ ++public final class ComponentAdapters { ++ ++ static final Function UNIT_TO_API_CONVERTER = $ -> { ++ throw new UnsupportedOperationException("Cannot convert the Unit type to an API value"); ++ }; ++ ++ static final Map>, ComponentAdapter> ADAPTERS = new HashMap<>(); ++ ++ public static void bootstrap() { ++ registerIdentity(DataComponents.MAX_STACK_SIZE); ++ registerIdentity(DataComponents.MAX_DAMAGE); ++ registerIdentity(DataComponents.DAMAGE); ++ register(DataComponents.UNBREAKABLE, PaperUnbreakable::new); ++ register(DataComponents.CUSTOM_NAME, PaperAdventure::asAdventure, PaperAdventure::asVanilla); ++ register(DataComponents.ITEM_NAME, PaperAdventure::asAdventure, PaperAdventure::asVanilla); ++ register(DataComponents.ITEM_MODEL, CraftNamespacedKey::fromMinecraft, CraftNamespacedKey::toMinecraft); ++ register(DataComponents.LORE, PaperItemLore::new); ++ register(DataComponents.RARITY, nms -> ItemRarity.valueOf(nms.name()), api -> Rarity.valueOf(api.name())); ++ register(DataComponents.ENCHANTMENTS, PaperItemEnchantments::new); ++ register(DataComponents.CAN_PLACE_ON, PaperItemAdventurePredicate::new); ++ register(DataComponents.CAN_BREAK, PaperItemAdventurePredicate::new); ++ register(DataComponents.ATTRIBUTE_MODIFIERS, PaperItemAttributeModifiers::new); ++ register(DataComponents.CUSTOM_MODEL_DATA, PaperCustomModelData::new); ++ registerUntyped(DataComponents.HIDE_ADDITIONAL_TOOLTIP); ++ registerUntyped(DataComponents.HIDE_TOOLTIP); ++ registerIdentity(DataComponents.REPAIR_COST); ++ // registerUntyped(DataComponents.CREATIVE_SLOT_LOCK); ++ registerIdentity(DataComponents.ENCHANTMENT_GLINT_OVERRIDE); ++ registerUntyped(DataComponents.INTANGIBLE_PROJECTILE); ++ register(DataComponents.FOOD, PaperFoodProperties::new); ++ register(DataComponents.CONSUMABLE, PaperConsumable::new); ++ register(DataComponents.USE_REMAINDER, PaperUseRemainder::new); ++ register(DataComponents.USE_COOLDOWN, PaperUseCooldown::new); ++ register(DataComponents.DAMAGE_RESISTANT, PaperDamageResistant::new); ++ register(DataComponents.TOOL, PaperItemTool::new); ++ register(DataComponents.ENCHANTABLE, PaperEnchantable::new); ++ register(DataComponents.EQUIPPABLE, PaperEquippable::new); ++ register(DataComponents.REPAIRABLE, PaperRepairable::new); ++ registerUntyped(DataComponents.GLIDER); ++ register(DataComponents.TOOLTIP_STYLE, PaperAdventure::asAdventure, PaperAdventure::asVanilla); ++ register(DataComponents.DEATH_PROTECTION, PaperDeathProtection::new); ++ register(DataComponents.STORED_ENCHANTMENTS, PaperItemEnchantments::new); ++ register(DataComponents.DYED_COLOR, PaperDyedItemColor::new); ++ register(DataComponents.MAP_COLOR, PaperMapItemColor::new); ++ register(DataComponents.MAP_ID, PaperMapId::new); ++ register(DataComponents.MAP_DECORATIONS, PaperMapDecorations::new); ++ register(DataComponents.MAP_POST_PROCESSING, nms -> io.papermc.paper.item.MapPostProcessing.valueOf(nms.name()), api -> MapPostProcessing.valueOf(api.name())); ++ register(DataComponents.CHARGED_PROJECTILES, PaperChargedProjectiles::new); ++ register(DataComponents.BUNDLE_CONTENTS, PaperBundleContents::new); ++ register(DataComponents.POTION_CONTENTS, PaperPotionContents::new); ++ register(DataComponents.SUSPICIOUS_STEW_EFFECTS, PaperSuspiciousStewEffects::new); ++ register(DataComponents.WRITTEN_BOOK_CONTENT, PaperWrittenBookContent::new); ++ register(DataComponents.WRITABLE_BOOK_CONTENT, PaperWritableBookContent::new); ++ register(DataComponents.TRIM, PaperItemArmorTrim::new); ++ // debug stick state ++ // entity data ++ // bucket entity data ++ // block entity data ++ register(DataComponents.INSTRUMENT, CraftMusicInstrument::minecraftHolderToBukkit, CraftMusicInstrument::bukkitToMinecraftHolder); ++ register(DataComponents.OMINOUS_BOTTLE_AMPLIFIER, PaperOminousBottleAmplifier::new); ++ register(DataComponents.JUKEBOX_PLAYABLE, PaperJukeboxPlayable::new); ++ register(DataComponents.RECIPES, nms -> transform(nms, PaperRegistries::fromNms), api -> transform(api, PaperRegistries::toNms)); ++ register(DataComponents.LODESTONE_TRACKER, PaperLodestoneTracker::new); ++ register(DataComponents.FIREWORK_EXPLOSION, CraftMetaFirework::getEffect, CraftMetaFirework::getExplosion); ++ register(DataComponents.FIREWORKS, PaperFireworks::new); ++ register(DataComponents.PROFILE, PaperResolvableProfile::new); ++ register(DataComponents.NOTE_BLOCK_SOUND, PaperAdventure::asAdventure, PaperAdventure::asVanilla); ++ register(DataComponents.BANNER_PATTERNS, PaperBannerPatternLayers::new); ++ register(DataComponents.BASE_COLOR, nms -> DyeColor.getByWoolData((byte) nms.getId()), api -> net.minecraft.world.item.DyeColor.byId(api.getWoolData())); ++ register(DataComponents.POT_DECORATIONS, PaperPotDecorations::new); ++ register(DataComponents.CONTAINER, PaperItemContainerContents::new); ++ register(DataComponents.BLOCK_STATE, PaperBlockItemDataProperties::new); ++ // bees ++ // register(DataComponents.LOCK, PaperLockCode::new); ++ register(DataComponents.CONTAINER_LOOT, PaperSeededContainerLoot::new); ++ ++ // TODO: REMOVE THIS... we want to build the PR... so lets just make things UNTYPED! ++ for (final Map.Entry>, DataComponentType> componentType : BuiltInRegistries.DATA_COMPONENT_TYPE.entrySet()) { ++ if (!ADAPTERS.containsKey(componentType.getKey())) { ++ registerUntyped((DataComponentType) componentType.getValue()); ++ } ++ } ++ } ++ ++ public static void registerUntyped(final DataComponentType type) { ++ registerInternal(type, UNIT_TO_API_CONVERTER, ComponentAdapter.API_TO_UNIT_CONVERTER, false); ++ } ++ ++ private static void registerIdentity(final DataComponentType type) { ++ registerInternal(type, Function.identity(), Function.identity(), true); ++ } ++ ++ private static > void register(final DataComponentType type, final Function vanillaToApi) { ++ registerInternal(type, vanillaToApi, Handleable::getHandle, false); ++ } ++ ++ private static void register(final DataComponentType type, final Function vanillaToApi, final Function apiToVanilla) { ++ registerInternal(type, vanillaToApi, apiToVanilla, false); ++ } ++ ++ private static void registerInternal(final DataComponentType type, final Function vanillaToApi, final Function apiToVanilla, final boolean codecValidation) { ++ final ResourceKey> key = BuiltInRegistries.DATA_COMPONENT_TYPE.getResourceKey(type).orElseThrow(); ++ if (ADAPTERS.containsKey(key)) { ++ throw new IllegalStateException("Duplicate adapter registration for " + key); ++ } ++ ADAPTERS.put(key, new ComponentAdapter<>(type, apiToVanilla, vanillaToApi, codecValidation && !type.isTransient())); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/ComponentUtils.java b/src/main/java/io/papermc/paper/datacomponent/ComponentUtils.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c95ffef54d7149cd8bb220533dddade515e48c8c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/ComponentUtils.java +@@ -0,0 +1,39 @@ ++package io.papermc.paper.datacomponent; ++ ++import com.google.common.collect.Collections2; ++import com.google.common.collect.Lists; ++import io.papermc.paper.adventure.PaperAdventure; ++import java.util.Collection; ++import java.util.Collections; ++import java.util.List; ++import java.util.function.Function; ++import net.kyori.adventure.key.Key; ++import net.minecraft.core.Holder; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.sounds.SoundEvent; ++ ++public final class ComponentUtils { ++ ++ private ComponentUtils() { ++ } ++ ++ public static Holder keyToSound(Key key) { ++ ResourceLocation soundId = PaperAdventure.asVanilla(key); ++ return BuiltInRegistries.SOUND_EVENT.wrapAsHolder(BuiltInRegistries.SOUND_EVENT.getOptional(soundId).orElse(SoundEvent.createVariableRangeEvent(soundId))); ++ } ++ ++ public static List transform(final List nms, final Function converter) { ++ return Collections.unmodifiableList(Lists.transform(nms, converter::apply)); ++ } ++ ++ public static Collection transform(final Collection nms, final Function converter) { ++ return Collections.unmodifiableCollection(Collections2.transform(nms, converter::apply)); ++ } ++ ++ public static > void addAndConvert(final C target, final Collection toAdd, final Function converter) { ++ for (final A value : toAdd) { ++ target.add(converter.apply(value)); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/PaperComponentType.java b/src/main/java/io/papermc/paper/datacomponent/PaperComponentType.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f0d4ec462eee47840e91bac888ae46045b493f07 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/PaperComponentType.java +@@ -0,0 +1,109 @@ ++package io.papermc.paper.datacomponent; ++ ++import java.util.Collections; ++import java.util.HashSet; ++import java.util.Set; ++import net.minecraft.core.component.DataComponentMap; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.core.registries.Registries; ++import org.bukkit.NamespacedKey; ++import org.bukkit.Registry; ++import org.bukkit.craftbukkit.CraftRegistry; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.jspecify.annotations.Nullable; ++ ++public abstract class PaperComponentType implements DataComponentType, Handleable> { ++ ++ static { ++ ComponentAdapters.bootstrap(); ++ } ++ ++ public static net.minecraft.core.component.DataComponentType bukkitToMinecraft(final DataComponentType type) { ++ return CraftRegistry.bukkitToMinecraft(type); ++ } ++ ++ public static DataComponentType minecraftToBukkit(final net.minecraft.core.component.DataComponentType type) { ++ return CraftRegistry.minecraftToBukkit(type, Registries.DATA_COMPONENT_TYPE, Registry.DATA_COMPONENT_TYPE); ++ } ++ ++ public static Set minecraftToBukkit(final Set> nmsTypes) { ++ final Set types = new HashSet<>(nmsTypes.size()); ++ for (final net.minecraft.core.component.DataComponentType nmsType : nmsTypes) { ++ types.add(PaperComponentType.minecraftToBukkit(nmsType)); ++ } ++ return Collections.unmodifiableSet(types); ++ } ++ ++ public static @Nullable B convertDataComponentValue(final DataComponentMap map, final PaperComponentType.ValuedImpl type) { ++ final net.minecraft.core.component.DataComponentType nms = bukkitToMinecraft(type); ++ final M nmsValue = map.get(nms); ++ if (nmsValue == null) { ++ return null; ++ } ++ return type.getAdapter().fromVanilla(nmsValue); ++ } ++ ++ private final NamespacedKey key; ++ private final net.minecraft.core.component.DataComponentType type; ++ private final ComponentAdapter adapter; ++ ++ public PaperComponentType(final NamespacedKey key, final net.minecraft.core.component.DataComponentType type, final ComponentAdapter adapter) { ++ this.key = key; ++ this.type = type; ++ this.adapter = adapter; ++ } ++ ++ @Override ++ public NamespacedKey getKey() { ++ return this.key; ++ } ++ ++ @Override ++ public boolean isPersistent() { ++ return !this.type.isTransient(); ++ } ++ ++ public ComponentAdapter getAdapter() { ++ return this.adapter; ++ } ++ ++ @Override ++ public net.minecraft.core.component.DataComponentType getHandle() { ++ return this.type; ++ } ++ ++ @SuppressWarnings("unchecked") ++ public static DataComponentType of(final NamespacedKey key, final net.minecraft.core.component.DataComponentType type) { ++ final ComponentAdapter adapter = (ComponentAdapter) ComponentAdapters.ADAPTERS.get(BuiltInRegistries.DATA_COMPONENT_TYPE.getResourceKey(type).orElseThrow()); ++ if (adapter == null) { ++ throw new IllegalArgumentException("No adapter found for " + key); ++ } ++ if (adapter.isValued()) { ++ return new ValuedImpl<>(key, type, adapter); ++ } else { ++ return new NonValuedImpl<>(key, type, adapter); ++ } ++ } ++ ++ public static final class NonValuedImpl extends PaperComponentType implements NonValued { ++ ++ NonValuedImpl( ++ final NamespacedKey key, ++ final net.minecraft.core.component.DataComponentType type, ++ final ComponentAdapter adapter ++ ) { ++ super(key, type, adapter); ++ } ++ } ++ ++ public static final class ValuedImpl extends PaperComponentType implements Valued { ++ ++ ValuedImpl( ++ final NamespacedKey key, ++ final net.minecraft.core.component.DataComponentType type, ++ final ComponentAdapter adapter ++ ) { ++ super(key, type, adapter); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/ItemComponentTypesBridgesImpl.java b/src/main/java/io/papermc/paper/datacomponent/item/ItemComponentTypesBridgesImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..14d039995a16a2c85569ca09e7f825c7de42fd6b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ItemComponentTypesBridgesImpl.java +@@ -0,0 +1,239 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import com.destroystokyo.paper.profile.PlayerProfile; ++import com.google.common.base.Preconditions; ++import io.papermc.paper.registry.PaperRegistries; ++import io.papermc.paper.registry.set.PaperRegistrySets; ++import io.papermc.paper.registry.set.RegistryKeySet; ++import io.papermc.paper.registry.tag.TagKey; ++import io.papermc.paper.text.Filtered; ++import net.kyori.adventure.key.Key; ++import net.kyori.adventure.util.TriState; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.core.registries.Registries; ++import net.minecraft.world.item.component.OminousBottleAmplifier; ++import org.bukkit.JukeboxSong; ++import org.bukkit.block.BlockType; ++import org.bukkit.craftbukkit.inventory.CraftItemStack; ++import org.bukkit.damage.DamageType; ++import org.bukkit.inventory.EquipmentSlot; ++import org.bukkit.inventory.ItemStack; ++import org.bukkit.inventory.ItemType; ++import org.bukkit.inventory.meta.trim.ArmorTrim; ++import org.bukkit.map.MapCursor; ++import org.jspecify.annotations.Nullable; ++ ++public final class ItemComponentTypesBridgesImpl implements ItemComponentTypesBridge { ++ ++ @Override ++ public ChargedProjectiles.Builder chargedProjectiles() { ++ return new PaperChargedProjectiles.BuilderImpl(); ++ } ++ ++ @Override ++ public PotDecorations.Builder potDecorations() { ++ return new PaperPotDecorations.BuilderImpl(); ++ } ++ ++ @Override ++ public Unbreakable.Builder unbreakable() { ++ return new PaperUnbreakable.BuilderImpl(); ++ } ++ ++ @Override ++ public ItemLore.Builder lore() { ++ return new PaperItemLore.BuilderImpl(); ++ } ++ ++ @Override ++ public ItemEnchantments.Builder enchantments() { ++ return new PaperItemEnchantments.BuilderImpl(); ++ } ++ ++ @Override ++ public ItemAttributeModifiers.Builder modifiers() { ++ return new PaperItemAttributeModifiers.BuilderImpl(); ++ } ++ ++ @Override ++ public FoodProperties.Builder food() { ++ return new PaperFoodProperties.BuilderImpl(); ++ } ++ ++ @Override ++ public DyedItemColor.Builder dyedItemColor() { ++ return new PaperDyedItemColor.BuilderImpl(); ++ } ++ ++ @Override ++ public PotionContents.Builder potionContents() { ++ return new PaperPotionContents.BuilderImpl(); ++ } ++ ++ @Override ++ public BundleContents.Builder bundleContents() { ++ return new PaperBundleContents.BuilderImpl(); ++ } ++ ++ @Override ++ public SuspiciousStewEffects.Builder suspiciousStewEffects() { ++ return new PaperSuspiciousStewEffects.BuilderImpl(); ++ } ++ ++ @Override ++ public MapItemColor.Builder mapItemColor() { ++ return new PaperMapItemColor.BuilderImpl(); ++ } ++ ++ @Override ++ public MapDecorations.Builder mapDecorations() { ++ return new PaperMapDecorations.BuilderImpl(); ++ } ++ ++ @Override ++ public MapDecorations.DecorationEntry decorationEntry(final MapCursor.Type type, final double x, final double z, final float rotation) { ++ return PaperMapDecorations.PaperDecorationEntry.toApi(type, x, z, rotation); ++ } ++ ++ @Override ++ public SeededContainerLoot.Builder seededContainerLoot(final Key lootTableKey) { ++ return new PaperSeededContainerLoot.BuilderImpl(lootTableKey); ++ } ++ ++ @Override ++ public ItemContainerContents.Builder itemContainerContents() { ++ return new PaperItemContainerContents.BuilderImpl(); ++ } ++ ++ @Override ++ public JukeboxPlayable.Builder jukeboxPlayable(final JukeboxSong song) { ++ return new PaperJukeboxPlayable.BuilderImpl(song); ++ } ++ ++ @Override ++ public Tool.Builder tool() { ++ return new PaperItemTool.BuilderImpl(); ++ } ++ ++ @Override ++ public Tool.Rule rule(final RegistryKeySet blocks, final @Nullable Float speed, final TriState correctForDrops) { ++ return PaperItemTool.PaperRule.fromUnsafe(blocks, speed, correctForDrops); ++ } ++ ++ @Override ++ public ItemAdventurePredicate.Builder itemAdventurePredicate() { ++ return new PaperItemAdventurePredicate.BuilderImpl(); ++ } ++ ++ @Override ++ public WrittenBookContent.Builder writtenBookContent(final Filtered title, final String author) { ++ return new PaperWrittenBookContent.BuilderImpl(title, author); ++ } ++ ++ @Override ++ public WritableBookContent.Builder writeableBookContent() { ++ return new PaperWritableBookContent.BuilderImpl(); ++ } ++ ++ @Override ++ public ItemArmorTrim.Builder itemArmorTrim(final ArmorTrim armorTrim) { ++ return new PaperItemArmorTrim.BuilderImpl(armorTrim); ++ } ++ ++ @Override ++ public LodestoneTracker.Builder lodestoneTracker() { ++ return new PaperLodestoneTracker.BuilderImpl(); ++ } ++ ++ @Override ++ public Fireworks.Builder fireworks() { ++ return new PaperFireworks.BuilderImpl(); ++ } ++ ++ @Override ++ public ResolvableProfile.Builder resolvableProfile() { ++ return new PaperResolvableProfile.BuilderImpl(); ++ } ++ ++ @Override ++ public ResolvableProfile resolvableProfile(final PlayerProfile profile) { ++ return PaperResolvableProfile.toApi(profile); ++ } ++ ++ @Override ++ public BannerPatternLayers.Builder bannerPatternLayers() { ++ return new PaperBannerPatternLayers.BuilderImpl(); ++ } ++ ++ @Override ++ public BlockItemDataProperties.Builder blockItemStateProperties() { ++ return new PaperBlockItemDataProperties.BuilderImpl(); ++ } ++ ++ @Override ++ public MapId mapId(final int id) { ++ return new PaperMapId(new net.minecraft.world.level.saveddata.maps.MapId(id)); ++ } ++ ++ @Override ++ public UseRemainder useRemainder(final ItemStack itemStack) { ++ Preconditions.checkNotNull(itemStack); ++ Preconditions.checkArgument(!itemStack.isEmpty(), "Remaining item cannot be empty!"); ++ return new PaperUseRemainder( ++ new net.minecraft.world.item.component.UseRemainder(CraftItemStack.asNMSCopy(itemStack)) ++ ); ++ } ++ ++ @Override ++ public Consumable.Builder consumable() { ++ return new PaperConsumable.BuilderImpl(); ++ } ++ ++ @Override ++ public UseCooldown.Builder useCooldown(final float seconds) { ++ Preconditions.checkArgument(seconds > 0, "seconds must be positive, was %s", seconds); ++ return new PaperUseCooldown.BuilderImpl(seconds); ++ } ++ ++ @Override ++ public DamageResistant damageResistant(final TagKey types) { ++ return new PaperDamageResistant(new net.minecraft.world.item.component.DamageResistant(PaperRegistries.toNms(types))); ++ } ++ ++ @Override ++ public Enchantable enchantable(final int level) { ++ return new PaperEnchantable(new net.minecraft.world.item.enchantment.Enchantable(level)); ++ } ++ ++ @Override ++ public Repairable repairable(final RegistryKeySet types) { ++ return new PaperRepairable(new net.minecraft.world.item.enchantment.Repairable( ++ PaperRegistrySets.convertToNms(Registries.ITEM, BuiltInRegistries.BUILT_IN_CONVERSIONS.lookup(), types) ++ )); ++ } ++ ++ @Override ++ public Equippable.Builder equippable(EquipmentSlot slot) { ++ return new PaperEquippable.BuilderImpl(slot); ++ } ++ ++ @Override ++ public DeathProtection.Builder deathProtection() { ++ return new PaperDeathProtection.BuilderImpl(); ++ } ++ ++ @Override ++ public CustomModelData customModelData(final int id) { ++ return new PaperCustomModelData(new net.minecraft.world.item.component.CustomModelData(id)); ++ } ++ ++ @Override ++ public PaperOminousBottleAmplifier ominousBottleAmplifier(final int amplifier) { ++ Preconditions.checkArgument(OminousBottleAmplifier.MIN_AMPLIFIER <= amplifier && amplifier <= OminousBottleAmplifier.MAX_AMPLIFIER, ++ "amplifier must be between %s-%s, was %s", OminousBottleAmplifier.MIN_AMPLIFIER, OminousBottleAmplifier.MAX_AMPLIFIER, amplifier ++ ); ++ return new PaperOminousBottleAmplifier( ++ new OminousBottleAmplifier(amplifier) ++ ); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperBannerPatternLayers.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperBannerPatternLayers.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9fde759d57bb9f54e32ce2e7ac36876079013c2b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperBannerPatternLayers.java +@@ -0,0 +1,62 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.registry.RegistryAccess; ++import io.papermc.paper.registry.RegistryKey; ++import io.papermc.paper.util.MCUtil; ++import java.util.List; ++import java.util.Objects; ++import java.util.Optional; ++import org.bukkit.DyeColor; ++import org.bukkit.block.banner.Pattern; ++import org.bukkit.block.banner.PatternType; ++import org.bukkit.craftbukkit.CraftRegistry; ++import org.bukkit.craftbukkit.block.banner.CraftPatternType; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.jetbrains.annotations.Unmodifiable; ++ ++public record PaperBannerPatternLayers( ++ net.minecraft.world.level.block.entity.BannerPatternLayers impl ++) implements BannerPatternLayers, Handleable { ++ ++ private static List convert(final net.minecraft.world.level.block.entity.BannerPatternLayers nmsPatterns) { ++ return MCUtil.transformUnmodifiable(nmsPatterns.layers(), input -> { ++ final Optional type = CraftRegistry.unwrapAndConvertHolder(RegistryAccess.registryAccess().getRegistry(RegistryKey.BANNER_PATTERN), input.pattern()); ++ return new Pattern(Objects.requireNonNull(DyeColor.getByWoolData((byte) input.color().getId())), type.orElseThrow(() -> new IllegalStateException("Inline banner patterns are not supported yet in the API!"))); ++ }); ++ } ++ ++ @Override ++ public net.minecraft.world.level.block.entity.BannerPatternLayers getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public @Unmodifiable List patterns() { ++ return convert(impl); ++ } ++ ++ static final class BuilderImpl implements BannerPatternLayers.Builder { ++ ++ private final net.minecraft.world.level.block.entity.BannerPatternLayers.Builder builder = new net.minecraft.world.level.block.entity.BannerPatternLayers.Builder(); ++ ++ @Override ++ public BannerPatternLayers.Builder add(final Pattern pattern) { ++ this.builder.add( ++ CraftPatternType.bukkitToMinecraftHolder(pattern.getPattern()), ++ net.minecraft.world.item.DyeColor.byId(pattern.getColor().getWoolData()) ++ ); ++ return this; ++ } ++ ++ @Override ++ public BannerPatternLayers.Builder addAll(final List patterns) { ++ patterns.forEach(this::add); ++ return this; ++ } ++ ++ @Override ++ public BannerPatternLayers build() { ++ return new PaperBannerPatternLayers(this.builder.build()); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperBlockItemDataProperties.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperBlockItemDataProperties.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5757e16c5948a6897bc61005ea7260940a49abfe +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperBlockItemDataProperties.java +@@ -0,0 +1,50 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; ++import java.util.Map; ++import net.minecraft.world.item.component.BlockItemStateProperties; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.state.BlockState; ++import org.bukkit.block.BlockType; ++import org.bukkit.block.data.BlockData; ++import org.bukkit.craftbukkit.block.CraftBlockType; ++import org.bukkit.craftbukkit.block.data.CraftBlockData; ++import org.bukkit.craftbukkit.util.Handleable; ++ ++public record PaperBlockItemDataProperties( ++ BlockItemStateProperties impl ++) implements BlockItemDataProperties, Handleable { ++ ++ @Override ++ public BlockData createBlockData(final BlockType blockType) { ++ final Block block = CraftBlockType.bukkitToMinecraftNew(blockType); ++ final BlockState defaultState = block.defaultBlockState(); ++ return this.impl.apply(defaultState).createCraftBlockData(); ++ } ++ ++ @Override ++ public BlockData applyTo(final BlockData blockData) { ++ final BlockState state = ((CraftBlockData) blockData).getState(); ++ return this.impl.apply(state).createCraftBlockData(); ++ } ++ ++ @Override ++ public BlockItemStateProperties getHandle() { ++ return this.impl; ++ } ++ ++ static final class BuilderImpl implements BlockItemDataProperties.Builder { ++ ++ private final Map properties = new Object2ObjectOpenHashMap<>(); ++ ++ // TODO when BlockProperty API is merged ++ ++ @Override ++ public BlockItemDataProperties build() { ++ if (this.properties.isEmpty()) { ++ return new PaperBlockItemDataProperties(BlockItemStateProperties.EMPTY); ++ } ++ return new PaperBlockItemDataProperties(new BlockItemStateProperties(new Object2ObjectOpenHashMap<>(this.properties))); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperBundleContents.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperBundleContents.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a59a98bdb15d2f4595d5ea651bfdf62542d80b2b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperBundleContents.java +@@ -0,0 +1,51 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import com.google.common.base.Preconditions; ++import io.papermc.paper.util.MCUtil; ++import it.unimi.dsi.fastutil.objects.ObjectArrayList; ++import java.util.List; ++import org.bukkit.craftbukkit.inventory.CraftItemStack; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.bukkit.inventory.ItemStack; ++ ++public record PaperBundleContents( ++ net.minecraft.world.item.component.BundleContents impl ++) implements BundleContents, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.component.BundleContents getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public List contents() { ++ return MCUtil.transformUnmodifiable((List) this.impl.items(), CraftItemStack::asBukkitCopy); ++ } ++ ++ static final class BuilderImpl implements BundleContents.Builder { ++ ++ private final List items = new ObjectArrayList<>(); ++ ++ @Override ++ public BundleContents.Builder add(final ItemStack stack) { ++ Preconditions.checkNotNull(stack, "stack cannot be null"); ++ Preconditions.checkArgument(!stack.isEmpty(), "stack cannot be empty"); ++ this.items.add(CraftItemStack.asNMSCopy(stack)); ++ return this; ++ } ++ ++ @Override ++ public BundleContents.Builder addAll(final List stacks) { ++ stacks.forEach(this::add); ++ return this; ++ } ++ ++ @Override ++ public BundleContents build() { ++ if (this.items.isEmpty()) { ++ return new PaperBundleContents(net.minecraft.world.item.component.BundleContents.EMPTY); ++ } ++ return new PaperBundleContents(new net.minecraft.world.item.component.BundleContents(new ObjectArrayList<>(this.items))); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperChargedProjectiles.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperChargedProjectiles.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3aa8b905748f2b82e1c464272d4b9da0c20086ad +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperChargedProjectiles.java +@@ -0,0 +1,51 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import com.google.common.base.Preconditions; ++import io.papermc.paper.util.MCUtil; ++import java.util.ArrayList; ++import java.util.List; ++import org.bukkit.craftbukkit.inventory.CraftItemStack; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.bukkit.inventory.ItemStack; ++ ++public record PaperChargedProjectiles( ++ net.minecraft.world.item.component.ChargedProjectiles impl ++) implements ChargedProjectiles, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.component.ChargedProjectiles getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public List projectiles() { ++ return MCUtil.transformUnmodifiable(this.impl.getItems() /*makes copies internally*/, CraftItemStack::asCraftMirror); ++ } ++ ++ static final class BuilderImpl implements ChargedProjectiles.Builder { ++ ++ private final List items = new ArrayList<>(); ++ ++ @Override ++ public ChargedProjectiles.Builder add(final ItemStack stack) { ++ Preconditions.checkNotNull(stack, "stack cannot be null"); ++ Preconditions.checkArgument(!stack.isEmpty(), "stack cannot be empty"); ++ this.items.add(CraftItemStack.asNMSCopy(stack)); ++ return this; ++ } ++ ++ @Override ++ public ChargedProjectiles.Builder addAll(final List stacks) { ++ stacks.forEach(this::add); ++ return this; ++ } ++ ++ @Override ++ public ChargedProjectiles build() { ++ if (this.items.isEmpty()) { ++ return new PaperChargedProjectiles(net.minecraft.world.item.component.ChargedProjectiles.EMPTY); ++ } ++ return new PaperChargedProjectiles(net.minecraft.world.item.component.ChargedProjectiles.of(this.items)); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperConsumable.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperConsumable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9badd3859745c0090c782fdccdd6fe8830f36b49 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperConsumable.java +@@ -0,0 +1,134 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import com.google.common.base.Preconditions; ++import io.papermc.paper.adventure.PaperAdventure; ++import io.papermc.paper.datacomponent.item.consumable.ConsumeEffect; ++import io.papermc.paper.datacomponent.item.consumable.ItemUseAnimation; ++import io.papermc.paper.datacomponent.item.consumable.PaperConsumableEffects; ++import io.papermc.paper.util.MCUtil; ++import it.unimi.dsi.fastutil.objects.ObjectArrayList; ++import java.util.List; ++import net.kyori.adventure.key.Key; ++import net.minecraft.core.Holder; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.sounds.SoundEvent; ++import net.minecraft.sounds.SoundEvents; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.checkerframework.checker.index.qual.NonNegative; ++import org.jetbrains.annotations.Unmodifiable; ++ ++public record PaperConsumable( ++ net.minecraft.world.item.component.Consumable impl ++) implements Consumable, Handleable { ++ ++ private static final ItemUseAnimation[] VALUES = ItemUseAnimation.values(); ++ ++ @Override ++ public net.minecraft.world.item.component.Consumable getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public @NonNegative float consumeSeconds() { ++ return this.impl.consumeSeconds(); ++ } ++ ++ @Override ++ public ItemUseAnimation animation() { ++ return VALUES[this.impl.animation().ordinal()]; ++ } ++ ++ @Override ++ public Key sound() { ++ return PaperAdventure.asAdventure(this.impl.sound().value().location()); ++ } ++ ++ @Override ++ public boolean hasConsumeParticles() { ++ return this.impl.hasConsumeParticles(); ++ } ++ ++ @Override ++ public @Unmodifiable List consumeEffects() { ++ return MCUtil.transformUnmodifiable(impl.onConsumeEffects(), PaperConsumableEffects::fromNms); ++ } ++ ++ @Override ++ public Consumable.Builder toBuilder() { ++ return new BuilderImpl() ++ .consumeSeconds(this.consumeSeconds()) ++ .animation(this.animation()) ++ .sound(this.sound()) ++ .addEffects(this.consumeEffects()); ++ } ++ ++ static final class BuilderImpl implements Builder { ++ ++ private static final net.minecraft.world.item.ItemUseAnimation[] VALUES = net.minecraft.world.item.ItemUseAnimation.values(); ++ ++ private float consumeSeconds = net.minecraft.world.item.component.Consumable.DEFAULT_CONSUME_SECONDS; ++ private net.minecraft.world.item.ItemUseAnimation consumeAnimation = net.minecraft.world.item.ItemUseAnimation.EAT; ++ private Holder eatSound = SoundEvents.GENERIC_EAT; ++ private boolean hasConsumeParticles = true; ++ private final List effects = new ObjectArrayList<>(); ++ ++ @Override ++ public Builder consumeSeconds(final @NonNegative float consumeSeconds) { ++ Preconditions.checkArgument(consumeSeconds >= 0, "consumeSeconds must be non-negative, was %s", consumeSeconds); ++ this.consumeSeconds = consumeSeconds; ++ return this; ++ } ++ ++ @Override ++ public Builder animation(final ItemUseAnimation animation) { ++ this.consumeAnimation = VALUES[animation.ordinal()]; ++ return this; ++ } ++ ++ @Override ++ public Builder sound(final Key sound) { ++ final ResourceLocation keySound = PaperAdventure.asVanilla(sound); ++ this.eatSound = BuiltInRegistries.SOUND_EVENT.wrapAsHolder( ++ BuiltInRegistries.SOUND_EVENT.getOptional(keySound).orElse( ++ SoundEvent.createVariableRangeEvent(keySound) ++ ) ++ ); ++ ++ return this; ++ } ++ ++ @Override ++ public Builder hasConsumeParticles(final boolean hasConsumeParticles) { ++ this.hasConsumeParticles = hasConsumeParticles; ++ return this; ++ } ++ ++ @Override ++ public Builder addEffect(final ConsumeEffect effect) { ++ this.effects.add(PaperConsumableEffects.toNms(effect)); ++ return this; ++ } ++ ++ @Override ++ public Builder addEffects(final List effects) { ++ for (final ConsumeEffect effect : effects) { ++ this.effects.add(PaperConsumableEffects.toNms(effect)); ++ } ++ return this; ++ } ++ ++ @Override ++ public Consumable build() { ++ return new PaperConsumable( ++ new net.minecraft.world.item.component.Consumable( ++ this.consumeSeconds, ++ this.consumeAnimation, ++ this.eatSound, ++ this.hasConsumeParticles, ++ new ObjectArrayList<>(this.effects) ++ ) ++ ); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperCustomModelData.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperCustomModelData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8373d882e8b927e74961d5ed2d548b2db6dacdaf +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperCustomModelData.java +@@ -0,0 +1,18 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import org.bukkit.craftbukkit.util.Handleable; ++ ++public record PaperCustomModelData( ++ net.minecraft.world.item.component.CustomModelData impl ++) implements CustomModelData, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.component.CustomModelData getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public int id() { ++ return this.impl.value(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperDamageResistant.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperDamageResistant.java +new file mode 100644 +index 0000000000000000000000000000000000000000..adc986c8b3d65e3fb91a8951048194bbe4052b74 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperDamageResistant.java +@@ -0,0 +1,21 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.registry.PaperRegistries; ++import io.papermc.paper.registry.tag.TagKey; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.bukkit.damage.DamageType; ++ ++public record PaperDamageResistant( ++ net.minecraft.world.item.component.DamageResistant impl ++) implements DamageResistant, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.component.DamageResistant getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public TagKey types() { ++ return PaperRegistries.fromNms(this.impl.types()); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperDeathProtection.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperDeathProtection.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6184acced73d8e99c0fa8b0df03680ad9b84f689 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperDeathProtection.java +@@ -0,0 +1,50 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.datacomponent.item.consumable.ConsumeEffect; ++import io.papermc.paper.datacomponent.item.consumable.PaperConsumableEffects; ++import io.papermc.paper.util.MCUtil; ++import java.util.ArrayList; ++import java.util.List; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.jetbrains.annotations.Unmodifiable; ++ ++public record PaperDeathProtection( ++ net.minecraft.world.item.component.DeathProtection impl ++) implements DeathProtection, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.component.DeathProtection getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public @Unmodifiable List deathEffects() { ++ return MCUtil.transformUnmodifiable(impl.deathEffects(), PaperConsumableEffects::fromNms); ++ } ++ ++ static final class BuilderImpl implements Builder { ++ ++ private final List effects = new ArrayList<>(); ++ ++ @Override ++ public Builder addEffect(final ConsumeEffect effect) { ++ this.effects.add(PaperConsumableEffects.toNms(effect)); ++ return this; ++ } ++ ++ @Override ++ public Builder addEffects(final List effects) { ++ for (final ConsumeEffect effect : effects) { ++ this.effects.add(PaperConsumableEffects.toNms(effect)); ++ } ++ return this; ++ } ++ ++ @Override ++ public DeathProtection build() { ++ return new PaperDeathProtection( ++ new net.minecraft.world.item.component.DeathProtection(this.effects) ++ ); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperDyedItemColor.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperDyedItemColor.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2407d79e2e77e8be6de8e65769efc4d79e3be9db +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperDyedItemColor.java +@@ -0,0 +1,52 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import org.bukkit.Color; ++import org.bukkit.craftbukkit.util.Handleable; ++ ++public record PaperDyedItemColor( ++ net.minecraft.world.item.component.DyedItemColor impl ++) implements DyedItemColor, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.component.DyedItemColor getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public Color color() { ++ return Color.fromRGB(this.impl.rgb() & 0x00FFFFFF); // skip alpha channel ++ } ++ ++ @Override ++ public boolean showInTooltip() { ++ return this.impl.showInTooltip(); ++ } ++ ++ @Override ++ public DyedItemColor showInTooltip(final boolean showInTooltip) { ++ return new PaperDyedItemColor(this.impl.withTooltip(showInTooltip)); ++ } ++ ++ static final class BuilderImpl implements DyedItemColor.Builder { ++ ++ private Color color = Color.WHITE; ++ private boolean showInToolTip = true; ++ ++ @Override ++ public DyedItemColor.Builder color(final Color color) { ++ this.color = color; ++ return this; ++ } ++ ++ @Override ++ public DyedItemColor.Builder showInTooltip(final boolean showInTooltip) { ++ this.showInToolTip = showInTooltip; ++ return this; ++ } ++ ++ @Override ++ public DyedItemColor build() { ++ return new PaperDyedItemColor(new net.minecraft.world.item.component.DyedItemColor(this.color.asRGB(), this.showInToolTip)); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperEnchantable.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperEnchantable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..422e1a4d606481f0dc68843fbbc8126ccfda1cc3 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperEnchantable.java +@@ -0,0 +1,18 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import org.bukkit.craftbukkit.util.Handleable; ++ ++public record PaperEnchantable( ++ net.minecraft.world.item.enchantment.Enchantable impl ++) implements Enchantable, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.enchantment.Enchantable getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public int value() { ++ return this.impl.value(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperEquippable.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperEquippable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..dbf8d060ab20b9cf31f209f26a8ad4d0cf05d6db +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperEquippable.java +@@ -0,0 +1,169 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.adventure.PaperAdventure; ++import io.papermc.paper.registry.RegistryKey; ++import io.papermc.paper.registry.set.PaperRegistrySets; ++import io.papermc.paper.registry.set.RegistryKeySet; ++import io.papermc.paper.util.MCUtil; ++import java.util.Optional; ++import net.kyori.adventure.key.Key; ++import net.minecraft.core.Holder; ++import net.minecraft.core.HolderSet; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.core.registries.Registries; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.sounds.SoundEvent; ++import net.minecraft.sounds.SoundEvents; ++import org.bukkit.craftbukkit.CraftEquipmentSlot; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.bukkit.entity.EntityType; ++import org.bukkit.inventory.EquipmentSlot; ++import org.checkerframework.checker.nullness.qual.Nullable; ++ ++public record PaperEquippable( ++ net.minecraft.world.item.equipment.Equippable impl ++) implements Equippable, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.equipment.Equippable getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public EquipmentSlot slot() { ++ return CraftEquipmentSlot.getSlot(this.impl.slot()); ++ } ++ ++ @Override ++ public Key equipSound() { ++ return PaperAdventure.asAdventure(this.impl.equipSound().value().location()); ++ } ++ ++ @Override ++ public @Nullable Key model() { ++ return this.impl.model() ++ .map(PaperAdventure::asAdventure) ++ .orElse(null); ++ } ++ ++ @Override ++ public @Nullable Key cameraOverlay() { ++ return this.impl.cameraOverlay() ++ .map(PaperAdventure::asAdventure) ++ .orElse(null); ++ } ++ ++ @Override ++ public @Nullable RegistryKeySet allowedEntities() { ++ return this.impl.allowedEntities() ++ .map((set) -> PaperRegistrySets.convertToApi(RegistryKey.ENTITY_TYPE, set)) ++ .orElse(null); ++ } ++ ++ @Override ++ public boolean dispensable() { ++ return this.impl.dispensable(); ++ } ++ ++ @Override ++ public boolean swappable() { ++ return this.impl.swappable(); ++ } ++ ++ @Override ++ public boolean damageOnHurt() { ++ return this.impl.damageOnHurt(); ++ } ++ ++ @Override ++ public Builder toBuilder() { ++ return new BuilderImpl(this.slot()) ++ .equipSound(this.equipSound()) ++ .model(this.model()) ++ .cameraOverlay(this.cameraOverlay()) ++ .allowedEntities(this.allowedEntities()) ++ .dispensable(this.dispensable()) ++ .swappable(this.swappable()) ++ .damageOnHurt(this.damageOnHurt()); ++ } ++ ++ ++ static final class BuilderImpl implements Builder { ++ ++ private final net.minecraft.world.entity.EquipmentSlot equipmentSlot; ++ private Holder equipSound = SoundEvents.ARMOR_EQUIP_GENERIC; ++ private Optional model = Optional.empty(); ++ private Optional cameraOverlay = Optional.empty(); ++ private Optional>> allowedEntities = Optional.empty(); ++ private boolean dispensable = true; ++ private boolean swappable = true; ++ private boolean damageOnHurt = true; ++ ++ BuilderImpl(final EquipmentSlot equipmentSlot) { ++ this.equipmentSlot = CraftEquipmentSlot.getNMS(equipmentSlot); ++ } ++ ++ @Override ++ public Builder equipSound(final Key equipSound) { ++ this.equipSound = MCUtil.keyToSound(equipSound); ++ return this; ++ } ++ ++ @Override ++ public Builder model(final @Nullable Key model) { ++ this.model = Optional.ofNullable(model) ++ .map(PaperAdventure::asVanilla); ++ ++ return this; ++ } ++ ++ @Override ++ public Builder cameraOverlay(@Nullable final Key cameraOverlay) { ++ this.cameraOverlay = Optional.ofNullable(cameraOverlay) ++ .map(PaperAdventure::asVanilla); ++ ++ return this; ++ } ++ ++ @Override ++ public Builder allowedEntities(final @Nullable RegistryKeySet allowedEntities) { ++ this.allowedEntities = Optional.ofNullable(allowedEntities) ++ .map((set) -> PaperRegistrySets.convertToNms(Registries.ENTITY_TYPE, BuiltInRegistries.BUILT_IN_CONVERSIONS.lookup(), set)); ++ return this; ++ } ++ ++ @Override ++ public Builder dispensable(final boolean dispensable) { ++ this.dispensable = dispensable; ++ return this; ++ } ++ ++ @Override ++ public Builder swappable(final boolean swappable) { ++ this.swappable = swappable; ++ return this; ++ } ++ ++ @Override ++ public Builder damageOnHurt(final boolean damageOnHurt) { ++ this.damageOnHurt = damageOnHurt; ++ return this; ++ } ++ ++ @Override ++ public Equippable build() { ++ return new PaperEquippable( ++ new net.minecraft.world.item.equipment.Equippable( ++ this.equipmentSlot, ++ this.equipSound, ++ this.model, ++ this.cameraOverlay, ++ this.allowedEntities, ++ this.dispensable, ++ this.swappable, ++ this.damageOnHurt ++ ) ++ ); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperFireworks.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperFireworks.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d7002c97086b55af851faaf8c65ad05c75381b02 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperFireworks.java +@@ -0,0 +1,73 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import com.google.common.base.Preconditions; ++import io.papermc.paper.util.MCUtil; ++import it.unimi.dsi.fastutil.objects.ObjectArrayList; ++import java.util.List; ++import net.minecraft.world.item.component.FireworkExplosion; ++import org.bukkit.FireworkEffect; ++import org.bukkit.craftbukkit.inventory.CraftMetaFirework; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.jetbrains.annotations.Unmodifiable; ++ ++public record PaperFireworks( ++ net.minecraft.world.item.component.Fireworks impl ++) implements Fireworks, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.component.Fireworks getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public @Unmodifiable List effects() { ++ return MCUtil.transformUnmodifiable(impl.explosions(), CraftMetaFirework::getEffect); ++ } ++ ++ @Override ++ public int flightDuration() { ++ return this.impl.flightDuration(); ++ } ++ ++ static final class BuilderImpl implements Fireworks.Builder { ++ ++ private final List effects = new ObjectArrayList<>(); ++ private int duration = 0; // default set from nms Fireworks component ++ ++ @Override ++ public Fireworks.Builder flightDuration(final int duration) { ++ Preconditions.checkArgument(duration >= 0 && duration <= 0xFF, "duration must be an unsigned byte ([%s, %s]), was %s", 0, 0xFF, duration); ++ this.duration = duration; ++ return this; ++ } ++ ++ @Override ++ public Fireworks.Builder addEffect(final FireworkEffect effect) { ++ Preconditions.checkArgument( ++ this.effects.size() + 1 <= net.minecraft.world.item.component.Fireworks.MAX_EXPLOSIONS, ++ "Cannot have more than %s effects, had %s", ++ net.minecraft.world.item.component.Fireworks.MAX_EXPLOSIONS, ++ this.effects.size() + 1 ++ ); ++ this.effects.add(CraftMetaFirework.getExplosion(effect)); ++ return this; ++ } ++ ++ @Override ++ public Fireworks.Builder addEffects(final List effects) { ++ Preconditions.checkArgument( ++ this.effects.size() + effects.size() <= net.minecraft.world.item.component.Fireworks.MAX_EXPLOSIONS, ++ "Cannot have more than %s effects, had %s", ++ net.minecraft.world.item.component.Fireworks.MAX_EXPLOSIONS, ++ this.effects.size() + effects.size() ++ ); ++ MCUtil.addAndConvert(this.effects, effects, CraftMetaFirework::getExplosion); ++ return this; ++ } ++ ++ @Override ++ public Fireworks build() { ++ return new PaperFireworks(new net.minecraft.world.item.component.Fireworks(this.duration, new ObjectArrayList<>(this.effects))); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperFoodProperties.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperFoodProperties.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2a043bb9001048f66d3a6aa8cb896b35bd2df606 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperFoodProperties.java +@@ -0,0 +1,72 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import com.google.common.base.Preconditions; ++import org.bukkit.craftbukkit.util.Handleable; ++ ++public record PaperFoodProperties( ++ net.minecraft.world.food.FoodProperties impl ++) implements FoodProperties, Handleable { ++ ++ @Override ++ public int nutrition() { ++ return this.impl.nutrition(); ++ } ++ ++ @Override ++ public float saturation() { ++ return this.impl.saturation(); ++ } ++ ++ @Override ++ public boolean canAlwaysEat() { ++ return this.impl.canAlwaysEat(); ++ } ++ ++ @Override ++ public FoodProperties.Builder toBuilder() { ++ return new BuilderImpl() ++ .nutrition(this.nutrition()) ++ .saturation(this.saturation()) ++ .canAlwaysEat(this.canAlwaysEat()); ++ } ++ ++ @Override ++ public net.minecraft.world.food.FoodProperties getHandle() { ++ return this.impl; ++ } ++ ++ static final class BuilderImpl implements FoodProperties.Builder { ++ ++ private boolean canAlwaysEat = false; ++ private float saturation = 0; ++ private int nutrition = 0; ++ ++ @Override ++ public FoodProperties.Builder canAlwaysEat(final boolean canAlwaysEat) { ++ this.canAlwaysEat = canAlwaysEat; ++ return this; ++ } ++ ++ @Override ++ public FoodProperties.Builder saturation(final float saturation) { ++ this.saturation = saturation; ++ return this; ++ } ++ ++ @Override ++ public FoodProperties.Builder nutrition(final int nutrition) { ++ Preconditions.checkArgument(nutrition >= 0, "nutrition must be non-negative, was %s", nutrition); ++ this.nutrition = nutrition; ++ return this; ++ } ++ ++ @Override ++ public FoodProperties build() { ++ return new PaperFoodProperties(new net.minecraft.world.food.FoodProperties( ++ this.nutrition, ++ this.saturation, ++ this.canAlwaysEat ++ )); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAdventurePredicate.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAdventurePredicate.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c7e40bd15b7063f155b2065927e8201f80fb6d0e +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAdventurePredicate.java +@@ -0,0 +1,75 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.block.BlockPredicate; ++import io.papermc.paper.registry.RegistryKey; ++import io.papermc.paper.registry.set.PaperRegistrySets; ++import io.papermc.paper.util.MCUtil; ++import it.unimi.dsi.fastutil.objects.ObjectArrayList; ++import java.util.List; ++import java.util.Optional; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.core.registries.Registries; ++import org.bukkit.craftbukkit.util.Handleable; ++ ++public record PaperItemAdventurePredicate( ++ net.minecraft.world.item.AdventureModePredicate impl ++) implements ItemAdventurePredicate, Handleable { ++ ++ private static List convert(final net.minecraft.world.item.AdventureModePredicate nmsModifiers) { ++ return MCUtil.transformUnmodifiable(nmsModifiers.predicates, nms -> BlockPredicate.predicate() ++ .blocks(nms.blocks().map(blocks -> PaperRegistrySets.convertToApi(RegistryKey.BLOCK, blocks)).orElse(null)).build()); ++ } ++ ++ @Override ++ public net.minecraft.world.item.AdventureModePredicate getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public boolean showInTooltip() { ++ return this.impl.showInTooltip(); ++ } ++ ++ @Override ++ public PaperItemAdventurePredicate showInTooltip(final boolean showInTooltip) { ++ return new PaperItemAdventurePredicate(this.impl.withTooltip(showInTooltip)); ++ } ++ ++ @Override ++ public List predicates() { ++ return convert(this.impl); ++ } ++ ++ static final class BuilderImpl implements ItemAdventurePredicate.Builder { ++ ++ private final List predicates = new ObjectArrayList<>(); ++ private boolean showInTooltip = net.minecraft.world.item.component.ItemAttributeModifiers.EMPTY.showInTooltip(); ++ ++ @Override ++ public ItemAdventurePredicate.Builder addPredicate(final BlockPredicate predicate) { ++ this.predicates.add(new net.minecraft.advancements.critereon.BlockPredicate(Optional.ofNullable(predicate.blocks()).map( ++ blocks -> PaperRegistrySets.convertToNms(Registries.BLOCK, BuiltInRegistries.BUILT_IN_CONVERSIONS.lookup(), blocks) ++ ), Optional.empty(), Optional.empty())); ++ return this; ++ } ++ ++ @Override ++ public Builder addPredicates(final List predicates) { ++ for (final BlockPredicate predicate : predicates) { ++ this.addPredicate(predicate); ++ } ++ return this; ++ } ++ ++ @Override ++ public ItemAdventurePredicate.Builder showInTooltip(final boolean showInTooltip) { ++ this.showInTooltip = showInTooltip; ++ return this; ++ } ++ ++ @Override ++ public ItemAdventurePredicate build() { ++ return new PaperItemAdventurePredicate(new net.minecraft.world.item.AdventureModePredicate(new ObjectArrayList<>(this.predicates), this.showInTooltip)); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperItemArmorTrim.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemArmorTrim.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5d060c907f4b1bc2bae063ca1e3baf35140215b6 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemArmorTrim.java +@@ -0,0 +1,62 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import org.bukkit.craftbukkit.inventory.trim.CraftTrimMaterial; ++import org.bukkit.craftbukkit.inventory.trim.CraftTrimPattern; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.bukkit.inventory.meta.trim.ArmorTrim; ++ ++public record PaperItemArmorTrim( ++ net.minecraft.world.item.equipment.trim.ArmorTrim impl ++) implements ItemArmorTrim, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.equipment.trim.ArmorTrim getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public boolean showInTooltip() { ++ return this.impl.showInTooltip(); ++ } ++ ++ @Override ++ public ItemArmorTrim showInTooltip(final boolean showInTooltip) { ++ return new PaperItemArmorTrim(this.impl.withTooltip(showInTooltip)); ++ } ++ ++ @Override ++ public ArmorTrim armorTrim() { ++ return new ArmorTrim(CraftTrimMaterial.minecraftHolderToBukkit(this.impl.material()), CraftTrimPattern.minecraftHolderToBukkit(this.impl.pattern())); ++ } ++ ++ static final class BuilderImpl implements ItemArmorTrim.Builder { ++ ++ private ArmorTrim armorTrim; ++ private boolean showInTooltip = true; ++ ++ BuilderImpl(final ArmorTrim armorTrim) { ++ this.armorTrim = armorTrim; ++ } ++ ++ @Override ++ public ItemArmorTrim.Builder showInTooltip(final boolean showInTooltip) { ++ this.showInTooltip = showInTooltip; ++ return this; ++ } ++ ++ @Override ++ public ItemArmorTrim.Builder armorTrim(final ArmorTrim armorTrim) { ++ this.armorTrim = armorTrim; ++ return this; ++ } ++ ++ @Override ++ public ItemArmorTrim build() { ++ return new PaperItemArmorTrim(new net.minecraft.world.item.equipment.trim.ArmorTrim( ++ CraftTrimMaterial.bukkitToMinecraftHolder(this.armorTrim.getMaterial()), ++ CraftTrimPattern.bukkitToMinecraftHolder(this.armorTrim.getPattern()), ++ this.showInTooltip ++ )); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAttributeModifiers.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAttributeModifiers.java +new file mode 100644 +index 0000000000000000000000000000000000000000..47ca2b8eb1c1483b6049cf18c7d8a40dd20e7cab +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAttributeModifiers.java +@@ -0,0 +1,97 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import com.google.common.base.Preconditions; ++import io.papermc.paper.util.MCUtil; ++import it.unimi.dsi.fastutil.objects.ObjectArrayList; ++import java.util.List; ++import org.bukkit.attribute.Attribute; ++import org.bukkit.attribute.AttributeModifier; ++import org.bukkit.craftbukkit.CraftEquipmentSlot; ++import org.bukkit.craftbukkit.attribute.CraftAttribute; ++import org.bukkit.craftbukkit.attribute.CraftAttributeInstance; ++import org.bukkit.craftbukkit.util.CraftNamespacedKey; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.bukkit.inventory.EquipmentSlotGroup; ++import org.jetbrains.annotations.Unmodifiable; ++ ++public record PaperItemAttributeModifiers( ++ net.minecraft.world.item.component.ItemAttributeModifiers impl ++) implements ItemAttributeModifiers, Handleable { ++ ++ private static List convert(final net.minecraft.world.item.component.ItemAttributeModifiers nmsModifiers) { ++ return MCUtil.transformUnmodifiable(nmsModifiers.modifiers(), nms -> new PaperEntry( ++ CraftAttribute.minecraftHolderToBukkit(nms.attribute()), ++ CraftAttributeInstance.convert(nms.modifier(), nms.slot()) ++ )); ++ } ++ ++ @Override ++ public net.minecraft.world.item.component.ItemAttributeModifiers getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public boolean showInTooltip() { ++ return this.impl.showInTooltip(); ++ } ++ ++ @Override ++ public ItemAttributeModifiers showInTooltip(final boolean showInTooltip) { ++ return new PaperItemAttributeModifiers(this.impl.withTooltip(showInTooltip)); ++ } ++ ++ @Override ++ public @Unmodifiable List modifiers() { ++ return convert(this.impl); ++ } ++ ++ public record PaperEntry(Attribute attribute, AttributeModifier modifier) implements ItemAttributeModifiers.Entry { ++ } ++ ++ static final class BuilderImpl implements ItemAttributeModifiers.Builder { ++ ++ private final List entries = new ObjectArrayList<>(); ++ private boolean showInTooltip = net.minecraft.world.item.component.ItemAttributeModifiers.EMPTY.showInTooltip(); ++ ++ @Override ++ public Builder addModifier(final Attribute attribute, final AttributeModifier modifier) { ++ return this.addModifier(attribute, modifier, modifier.getSlotGroup()); ++ } ++ ++ @Override ++ public ItemAttributeModifiers.Builder addModifier(final Attribute attribute, final AttributeModifier modifier, final EquipmentSlotGroup equipmentSlotGroup) { ++ Preconditions.checkArgument( ++ this.entries.stream().noneMatch(e -> ++ e.modifier().id().equals(CraftNamespacedKey.toMinecraft(modifier.getKey())) && e.attribute().is(CraftNamespacedKey.toMinecraft(attribute.getKey())) ++ ), ++ "Cannot add 2 modifiers with identical keys on the same attribute (modifier %s for attribute %s)", ++ modifier.getKey(), attribute.getKey() ++ ); ++ ++ this.entries.add(new net.minecraft.world.item.component.ItemAttributeModifiers.Entry( ++ CraftAttribute.bukkitToMinecraftHolder(attribute), ++ CraftAttributeInstance.convert(modifier), ++ CraftEquipmentSlot.getNMSGroup(equipmentSlotGroup) ++ )); ++ return this; ++ } ++ ++ @Override ++ public ItemAttributeModifiers.Builder showInTooltip(final boolean showInTooltip) { ++ this.showInTooltip = showInTooltip; ++ return this; ++ } ++ ++ @Override ++ public ItemAttributeModifiers build() { ++ if (this.entries.isEmpty()) { ++ return new PaperItemAttributeModifiers(net.minecraft.world.item.component.ItemAttributeModifiers.EMPTY.withTooltip(this.showInTooltip)); ++ } ++ ++ return new PaperItemAttributeModifiers(new net.minecraft.world.item.component.ItemAttributeModifiers( ++ new ObjectArrayList<>(this.entries), ++ this.showInTooltip ++ )); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperItemContainerContents.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemContainerContents.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e65603e711ecd08039361d291a0aac761a2f9349 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemContainerContents.java +@@ -0,0 +1,65 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import com.google.common.base.Preconditions; ++import io.papermc.paper.util.MCUtil; ++import it.unimi.dsi.fastutil.objects.ObjectArrayList; ++import java.util.List; ++import org.bukkit.craftbukkit.inventory.CraftItemStack; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.bukkit.inventory.ItemStack; ++ ++public record PaperItemContainerContents( ++ net.minecraft.world.item.component.ItemContainerContents impl ++) implements ItemContainerContents, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.component.ItemContainerContents getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public List contents() { ++ return MCUtil.transformUnmodifiable(this.impl.items, CraftItemStack::asBukkitCopy); ++ } ++ ++ static final class BuilderImpl implements ItemContainerContents.Builder { ++ ++ private final List items = new ObjectArrayList<>(); ++ ++ @Override ++ public ItemContainerContents.Builder add(final ItemStack stack) { ++ Preconditions.checkNotNull(stack); ++ Preconditions.checkArgument( ++ this.items.size() + 1 <= net.minecraft.world.item.component.ItemContainerContents.MAX_SIZE, ++ "Cannot have more than %s items, had %s", ++ net.minecraft.world.item.component.ItemContainerContents.MAX_SIZE, ++ this.items.size() + 1 ++ ); ++ this.items.add(CraftItemStack.asNMSCopy(stack)); ++ return this; ++ } ++ ++ @Override ++ public ItemContainerContents.Builder addAll(final List stacks) { ++ Preconditions.checkArgument( ++ this.items.size() + stacks.size() <= net.minecraft.world.item.component.ItemContainerContents.MAX_SIZE, ++ "Cannot have more than %s items, had %s", ++ net.minecraft.world.item.component.ItemContainerContents.MAX_SIZE, ++ this.items.size() + stacks.size() ++ ); ++ MCUtil.addAndConvert(this.items, stacks, itemStack -> { ++ Preconditions.checkNotNull(itemStack, "Cannot pass null itemstacks!"); ++ return CraftItemStack.asNMSCopy(itemStack); ++ }); ++ return this; ++ } ++ ++ @Override ++ public ItemContainerContents build() { ++ if (this.items.isEmpty()) { ++ return new PaperItemContainerContents(net.minecraft.world.item.component.ItemContainerContents.EMPTY); ++ } ++ return new PaperItemContainerContents(net.minecraft.world.item.component.ItemContainerContents.fromItems(this.items)); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperItemEnchantments.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemEnchantments.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3cfb18f6a4868ff32e2b118c5833b1b9864e967c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemEnchantments.java +@@ -0,0 +1,92 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import com.google.common.base.Preconditions; ++import it.unimi.dsi.fastutil.objects.Object2IntMap; ++import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; ++import java.util.Collections; ++import java.util.HashMap; ++import java.util.Map; ++import net.minecraft.core.Holder; ++import org.bukkit.craftbukkit.enchantments.CraftEnchantment; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.bukkit.enchantments.Enchantment; ++ ++public record PaperItemEnchantments( ++ net.minecraft.world.item.enchantment.ItemEnchantments impl, ++ Map enchantments // API values are stored externally as the concept of a lazy key transformer map does not make much sense ++) implements ItemEnchantments, Handleable { ++ ++ public PaperItemEnchantments(final net.minecraft.world.item.enchantment.ItemEnchantments itemEnchantments) { ++ this(itemEnchantments, convert(itemEnchantments)); ++ } ++ ++ private static Map convert(final net.minecraft.world.item.enchantment.ItemEnchantments itemEnchantments) { ++ if (itemEnchantments.isEmpty()) { ++ return Collections.emptyMap(); ++ } ++ final Map map = new HashMap<>(itemEnchantments.size()); ++ for (final Object2IntMap.Entry> entry : itemEnchantments.entrySet()) { ++ map.put(CraftEnchantment.minecraftHolderToBukkit(entry.getKey()), entry.getIntValue()); ++ } ++ return Collections.unmodifiableMap(map); // TODO look into making a "transforming" map maybe? ++ } ++ ++ @Override ++ public boolean showInTooltip() { ++ return this.impl.showInTooltip; ++ } ++ ++ @Override ++ public ItemEnchantments showInTooltip(final boolean showInTooltip) { ++ return new PaperItemEnchantments(this.impl.withTooltip(showInTooltip), this.enchantments); ++ } ++ ++ @Override ++ public net.minecraft.world.item.enchantment.ItemEnchantments getHandle() { ++ return this.impl; ++ } ++ ++ static final class BuilderImpl implements ItemEnchantments.Builder { ++ ++ private final Map enchantments = new Object2ObjectOpenHashMap<>(); ++ private boolean showInTooltip = true; ++ ++ @Override ++ public ItemEnchantments.Builder add(final Enchantment enchantment, final int level) { ++ Preconditions.checkArgument( ++ level >= 1 && level <= net.minecraft.world.item.enchantment.Enchantment.MAX_LEVEL, ++ "level must be between %s and %s, was %s", ++ 1, net.minecraft.world.item.enchantment.Enchantment.MAX_LEVEL, ++ level ++ ); ++ this.enchantments.put(enchantment, level); ++ return this; ++ } ++ ++ @Override ++ public ItemEnchantments.Builder addAll(final Map enchantments) { ++ enchantments.forEach(this::add); ++ return this; ++ } ++ ++ @Override ++ public ItemEnchantments.Builder showInTooltip(final boolean showInTooltip) { ++ this.showInTooltip = showInTooltip; ++ return this; ++ } ++ ++ @Override ++ public ItemEnchantments build() { ++ final net.minecraft.world.item.enchantment.ItemEnchantments initialEnchantments = net.minecraft.world.item.enchantment.ItemEnchantments.EMPTY.withTooltip(this.showInTooltip); ++ if (this.enchantments.isEmpty()) { ++ return new PaperItemEnchantments(initialEnchantments); ++ } ++ ++ final net.minecraft.world.item.enchantment.ItemEnchantments.Mutable mutable = new net.minecraft.world.item.enchantment.ItemEnchantments.Mutable(initialEnchantments); ++ this.enchantments.forEach((enchantment, level) -> ++ mutable.set(CraftEnchantment.bukkitToMinecraftHolder(enchantment), level) ++ ); ++ return new PaperItemEnchantments(mutable.toImmutable()); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperItemLore.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemLore.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8f58b6869bb79428288a4be05424ace4d77c3845 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemLore.java +@@ -0,0 +1,77 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import com.google.common.base.Preconditions; ++import io.papermc.paper.adventure.PaperAdventure; ++import io.papermc.paper.util.MCUtil; ++import it.unimi.dsi.fastutil.objects.ObjectArrayList; ++import java.util.ArrayList; ++import java.util.List; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.ComponentLike; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.jetbrains.annotations.Unmodifiable; ++ ++public record PaperItemLore( ++ net.minecraft.world.item.component.ItemLore impl ++) implements ItemLore, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.component.ItemLore getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public @Unmodifiable List lines() { ++ return MCUtil.transformUnmodifiable(impl.lines(), PaperAdventure::asAdventure); ++ } ++ ++ @Override ++ public @Unmodifiable List styledLines() { ++ return MCUtil.transformUnmodifiable(impl.styledLines(), PaperAdventure::asAdventure); ++ } ++ ++ static final class BuilderImpl implements ItemLore.Builder { ++ ++ private List lines = new ObjectArrayList<>(); ++ ++ private static void validateLineCount(final int current, final int add) { ++ final int newSize = current + add; ++ Preconditions.checkArgument( ++ newSize <= net.minecraft.world.item.component.ItemLore.MAX_LINES, ++ "Cannot have more than %s lines, had %s", ++ net.minecraft.world.item.component.ItemLore.MAX_LINES, ++ newSize ++ ); ++ } ++ ++ @Override ++ public ItemLore.Builder lines(final List lines) { ++ validateLineCount(0, lines.size()); ++ this.lines = new ArrayList<>(ComponentLike.asComponents(lines)); ++ return this; ++ } ++ ++ @Override ++ public ItemLore.Builder addLine(final ComponentLike line) { ++ validateLineCount(this.lines.size(), 1); ++ this.lines.add(line.asComponent()); ++ return this; ++ } ++ ++ @Override ++ public ItemLore.Builder addLines(final List lines) { ++ validateLineCount(this.lines.size(), lines.size()); ++ this.lines.addAll(ComponentLike.asComponents(lines)); ++ return this; ++ } ++ ++ @Override ++ public ItemLore build() { ++ if (this.lines.isEmpty()) { ++ return new PaperItemLore(net.minecraft.world.item.component.ItemLore.EMPTY); ++ } ++ ++ return new PaperItemLore(new net.minecraft.world.item.component.ItemLore(PaperAdventure.asVanilla(this.lines))); // asVanilla does a list clone ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperItemTool.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemTool.java +new file mode 100644 +index 0000000000000000000000000000000000000000..538a61eaa02c029b4d92f938e0ffde8aa6cf027c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemTool.java +@@ -0,0 +1,100 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import com.google.common.base.Preconditions; ++import io.papermc.paper.registry.RegistryKey; ++import io.papermc.paper.registry.set.PaperRegistrySets; ++import io.papermc.paper.registry.set.RegistryKeySet; ++import io.papermc.paper.util.MCUtil; ++import it.unimi.dsi.fastutil.objects.ObjectArrayList; ++import java.util.Collection; ++import java.util.List; ++import java.util.Optional; ++import net.kyori.adventure.util.TriState; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.core.registries.Registries; ++import org.bukkit.block.BlockType; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.jetbrains.annotations.Unmodifiable; ++ ++public record PaperItemTool( ++ net.minecraft.world.item.component.Tool impl ++) implements Tool, Handleable { ++ ++ private static List convert(final List tool) { ++ return MCUtil.transformUnmodifiable(tool, nms -> new PaperRule( ++ PaperRegistrySets.convertToApi(RegistryKey.BLOCK, nms.blocks()), ++ nms.speed().orElse(null), ++ TriState.byBoolean(nms.correctForDrops().orElse(null)) ++ )); ++ } ++ ++ @Override ++ public net.minecraft.world.item.component.Tool getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public @Unmodifiable List rules() { ++ return convert(this.impl.rules()); ++ } ++ ++ @Override ++ public float defaultMiningSpeed() { ++ return this.impl.defaultMiningSpeed(); ++ } ++ ++ @Override ++ public int damagePerBlock() { ++ return this.impl.damagePerBlock(); ++ } ++ ++ record PaperRule(RegistryKeySet blocks, @Nullable Float speed, TriState correctForDrops) implements Rule { ++ ++ public static PaperRule fromUnsafe(final RegistryKeySet blocks, final @Nullable Float speed, final TriState correctForDrops) { ++ Preconditions.checkArgument(speed == null || speed > 0, "speed must be positive"); ++ return new PaperRule(blocks, speed, correctForDrops); ++ } ++ } ++ ++ static final class BuilderImpl implements Builder { ++ ++ private final List rules = new ObjectArrayList<>(); ++ private int damage = 1; ++ private float miningSpeed = 1.0F; ++ ++ @Override ++ public Builder damagePerBlock(final int damage) { ++ Preconditions.checkArgument(damage >= 0, "damage must be non-negative, was %s", damage); ++ this.damage = damage; ++ return this; ++ } ++ ++ @Override ++ public Builder defaultMiningSpeed(final float miningSpeed) { ++ this.miningSpeed = miningSpeed; ++ return this; ++ } ++ ++ @Override ++ public Builder addRule(final Rule rule) { ++ this.rules.add(new net.minecraft.world.item.component.Tool.Rule( ++ PaperRegistrySets.convertToNms(Registries.BLOCK, BuiltInRegistries.BUILT_IN_CONVERSIONS.lookup(), rule.blocks()), ++ Optional.ofNullable(rule.speed()), ++ Optional.ofNullable(rule.correctForDrops().toBoolean()) ++ )); ++ return this; ++ } ++ ++ @Override ++ public Builder addRules(final Collection rules) { ++ rules.forEach(this::addRule); ++ return this; ++ } ++ ++ @Override ++ public Tool build() { ++ return new PaperItemTool(new net.minecraft.world.item.component.Tool(new ObjectArrayList<>(this.rules), this.miningSpeed, this.damage)); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperJukeboxPlayable.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperJukeboxPlayable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..eb7209d42e7c44ae7c9b31663aa94ed6cc77f592 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperJukeboxPlayable.java +@@ -0,0 +1,58 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import net.minecraft.world.item.EitherHolder; ++import org.bukkit.JukeboxSong; ++import org.bukkit.craftbukkit.CraftJukeboxSong; ++import org.bukkit.craftbukkit.util.Handleable; ++ ++public record PaperJukeboxPlayable( ++ net.minecraft.world.item.JukeboxPlayable impl ++) implements JukeboxPlayable, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.JukeboxPlayable getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public boolean showInTooltip() { ++ return this.impl.showInTooltip(); ++ } ++ ++ @Override ++ public PaperJukeboxPlayable showInTooltip(final boolean showInTooltip) { ++ return new PaperJukeboxPlayable(this.impl.withTooltip(showInTooltip)); ++ } ++ ++ @Override ++ public JukeboxSong jukeboxSong() { ++ return this.impl.song().holder().map(CraftJukeboxSong::minecraftHolderToBukkit).orElseThrow(); ++ } ++ ++ static final class BuilderImpl implements JukeboxPlayable.Builder { ++ ++ private JukeboxSong song; ++ private boolean showInTooltip = true; ++ ++ BuilderImpl(final JukeboxSong song) { ++ this.song = song; ++ } ++ ++ @Override ++ public JukeboxPlayable.Builder showInTooltip(final boolean showInTooltip) { ++ this.showInTooltip = showInTooltip; ++ return this; ++ } ++ ++ @Override ++ public JukeboxPlayable.Builder jukeboxSong(final JukeboxSong song) { ++ this.song = song; ++ return this; ++ } ++ ++ @Override ++ public JukeboxPlayable build() { ++ return new PaperJukeboxPlayable(new net.minecraft.world.item.JukeboxPlayable(new EitherHolder<>(CraftJukeboxSong.bukkitToMinecraftHolder(this.song)), this.showInTooltip)); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperLodestoneTracker.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperLodestoneTracker.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5b97249f6ae90bc1a10c2089e39f064068d7cd2c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperLodestoneTracker.java +@@ -0,0 +1,53 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import java.util.Optional; ++import org.bukkit.Location; ++import org.bukkit.craftbukkit.util.CraftLocation; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.jspecify.annotations.Nullable; ++ ++public record PaperLodestoneTracker( ++ net.minecraft.world.item.component.LodestoneTracker impl ++) implements LodestoneTracker, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.component.LodestoneTracker getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public @Nullable Location location() { ++ return this.impl.target().map(CraftLocation::fromGlobalPos).orElse(null); ++ } ++ ++ @Override ++ public boolean tracked() { ++ return this.impl.tracked(); ++ } ++ ++ static final class BuilderImpl implements LodestoneTracker.Builder { ++ ++ private @Nullable Location location; ++ private boolean tracked = true; ++ ++ @Override ++ public LodestoneTracker.Builder location(final @Nullable Location location) { ++ this.location = location; ++ return this; ++ } ++ ++ @Override ++ public LodestoneTracker.Builder tracked(final boolean tracked) { ++ this.tracked = tracked; ++ return this; ++ } ++ ++ @Override ++ public LodestoneTracker build() { ++ return new PaperLodestoneTracker(new net.minecraft.world.item.component.LodestoneTracker( ++ Optional.ofNullable(this.location).map(CraftLocation::toGlobalPos), ++ this.tracked ++ )); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperMapDecorations.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapDecorations.java +new file mode 100644 +index 0000000000000000000000000000000000000000..322a1285b0c5127abb67ccab478f1b16b44d0be4 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapDecorations.java +@@ -0,0 +1,97 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; ++import java.util.Collections; ++import java.util.Map; ++import java.util.Set; ++import org.bukkit.craftbukkit.map.CraftMapCursor; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.bukkit.map.MapCursor; ++import org.jspecify.annotations.Nullable; ++ ++public record PaperMapDecorations( ++ net.minecraft.world.item.component.MapDecorations impl ++) implements MapDecorations, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.component.MapDecorations getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public @Nullable DecorationEntry decoration(final String id) { ++ final net.minecraft.world.item.component.MapDecorations.Entry decoration = this.impl.decorations().get(id); ++ if (decoration == null) { ++ return null; ++ } ++ ++ return new PaperDecorationEntry(decoration); ++ } ++ ++ @Override ++ public Map decorations() { ++ if (this.impl.decorations().isEmpty()) { ++ return Collections.emptyMap(); ++ } ++ ++ final Set> entries = this.impl.decorations().entrySet(); ++ final Map decorations = new Object2ObjectOpenHashMap<>(entries.size()); ++ for (final Map.Entry entry : entries) { ++ decorations.put(entry.getKey(), new PaperDecorationEntry(entry.getValue())); ++ } ++ ++ return Collections.unmodifiableMap(decorations); ++ } ++ ++ public record PaperDecorationEntry(net.minecraft.world.item.component.MapDecorations.Entry entry) implements DecorationEntry { ++ ++ public static DecorationEntry toApi(final MapCursor.Type type, final double x, final double z, final float rotation) { ++ return new PaperDecorationEntry(new net.minecraft.world.item.component.MapDecorations.Entry(CraftMapCursor.CraftType.bukkitToMinecraftHolder(type), x, z, rotation)); ++ } ++ ++ @Override ++ public MapCursor.Type type() { ++ return CraftMapCursor.CraftType.minecraftHolderToBukkit(this.entry.type()); ++ } ++ ++ @Override ++ public double x() { ++ return this.entry.x(); ++ } ++ ++ @Override ++ public double z() { ++ return this.entry.z(); ++ } ++ ++ @Override ++ public float rotation() { ++ return this.entry.rotation(); ++ } ++ } ++ ++ static final class BuilderImpl implements Builder { ++ ++ private final Map entries = new Object2ObjectOpenHashMap<>(); ++ ++ @Override ++ public MapDecorations.Builder put(final String id, final DecorationEntry entry) { ++ this.entries.put(id, new net.minecraft.world.item.component.MapDecorations.Entry(CraftMapCursor.CraftType.bukkitToMinecraftHolder(entry.type()), entry.x(), entry.z(), entry.rotation())); ++ return this; ++ } ++ ++ @Override ++ public Builder putAll(final Map entries) { ++ entries.forEach(this::put); ++ return this; ++ } ++ ++ @Override ++ public MapDecorations build() { ++ if (this.entries.isEmpty()) { ++ return new PaperMapDecorations(net.minecraft.world.item.component.MapDecorations.EMPTY); ++ } ++ return new PaperMapDecorations(new net.minecraft.world.item.component.MapDecorations(new Object2ObjectOpenHashMap<>(this.entries))); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperMapId.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapId.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a2b4cc372bb154bbc741ad1bf47cba210f292c5c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapId.java +@@ -0,0 +1,19 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import org.bukkit.craftbukkit.util.Handleable; ++ ++public record PaperMapId( ++ net.minecraft.world.level.saveddata.maps.MapId impl ++) implements MapId, Handleable { ++ ++ @Override ++ public net.minecraft.world.level.saveddata.maps.MapId getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public int id() { ++ return this.impl.id(); ++ } ++ ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperMapItemColor.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapItemColor.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9b6fdfc9c1248bac426ce24d7b66610a6eff3b8f +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapItemColor.java +@@ -0,0 +1,35 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import org.bukkit.Color; ++import org.bukkit.craftbukkit.util.Handleable; ++ ++public record PaperMapItemColor( ++ net.minecraft.world.item.component.MapItemColor impl ++) implements MapItemColor, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.component.MapItemColor getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public Color color() { ++ return Color.fromRGB(this.impl.rgb() & 0x00FFFFFF); // skip alpha channel ++ } ++ ++ static final class BuilderImpl implements Builder { ++ ++ private Color color = Color.fromRGB(net.minecraft.world.item.component.MapItemColor.DEFAULT.rgb()); ++ ++ @Override ++ public Builder color(final Color color) { ++ this.color = color; ++ return this; ++ } ++ ++ @Override ++ public MapItemColor build() { ++ return new PaperMapItemColor(new net.minecraft.world.item.component.MapItemColor(this.color.asRGB())); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperOminousBottleAmplifier.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperOminousBottleAmplifier.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a7ed2aa21d0384384a4c5830ead544cb064b15b6 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperOminousBottleAmplifier.java +@@ -0,0 +1,18 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import org.bukkit.craftbukkit.util.Handleable; ++ ++public record PaperOminousBottleAmplifier( ++ net.minecraft.world.item.component.OminousBottleAmplifier impl ++) implements OminousBottleAmplifier, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.component.OminousBottleAmplifier getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public int amplifier() { ++ return this.impl.value(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperPotDecorations.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperPotDecorations.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bde757b51d0ae6a36870c789d416ec0e05c4cadf +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperPotDecorations.java +@@ -0,0 +1,83 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import java.util.Optional; ++import org.bukkit.craftbukkit.inventory.CraftItemType; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.bukkit.inventory.ItemType; ++import org.jspecify.annotations.Nullable; ++ ++public record PaperPotDecorations( ++ net.minecraft.world.level.block.entity.PotDecorations impl ++) implements PotDecorations, Handleable { ++ ++ @Override ++ public @Nullable ItemType back() { ++ return this.impl.back().map(CraftItemType::minecraftToBukkitNew).orElse(null); ++ } ++ ++ @Override ++ public @Nullable ItemType left() { ++ return this.impl.left().map(CraftItemType::minecraftToBukkitNew).orElse(null); ++ } ++ ++ @Override ++ public @Nullable ItemType right() { ++ return this.impl.right().map(CraftItemType::minecraftToBukkitNew).orElse(null); ++ } ++ ++ @Override ++ public @Nullable ItemType front() { ++ return this.impl.front().map(CraftItemType::minecraftToBukkitNew).orElse(null); ++ } ++ ++ @Override ++ public net.minecraft.world.level.block.entity.PotDecorations getHandle() { ++ return this.impl; ++ } ++ ++ static final class BuilderImpl implements PotDecorations.Builder { ++ ++ private @Nullable ItemType back; ++ private @Nullable ItemType left; ++ private @Nullable ItemType right; ++ private @Nullable ItemType front; ++ ++ @Override ++ public PotDecorations.Builder back(final @Nullable ItemType back) { ++ this.back = back; ++ return this; ++ } ++ ++ @Override ++ public PotDecorations.Builder left(final @Nullable ItemType left) { ++ this.left = left; ++ return this; ++ } ++ ++ @Override ++ public PotDecorations.Builder right(final @Nullable ItemType right) { ++ this.right = right; ++ return this; ++ } ++ ++ @Override ++ public PotDecorations.Builder front(final @Nullable ItemType front) { ++ this.front = front; ++ return this; ++ } ++ ++ @Override ++ public PotDecorations build() { ++ if (this.back == null && this.left == null && this.right == null && this.front == null) { ++ return new PaperPotDecorations(net.minecraft.world.level.block.entity.PotDecorations.EMPTY); ++ } ++ ++ return new PaperPotDecorations(new net.minecraft.world.level.block.entity.PotDecorations( ++ Optional.ofNullable(this.back).map(CraftItemType::bukkitToMinecraftNew), ++ Optional.ofNullable(this.left).map(CraftItemType::bukkitToMinecraftNew), ++ Optional.ofNullable(this.right).map(CraftItemType::bukkitToMinecraftNew), ++ Optional.ofNullable(this.front).map(CraftItemType::bukkitToMinecraftNew) ++ )); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperPotionContents.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperPotionContents.java +new file mode 100644 +index 0000000000000000000000000000000000000000..196297175610644a5a4cad8e619303b4517df274 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperPotionContents.java +@@ -0,0 +1,103 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import com.google.common.base.Preconditions; ++import io.papermc.paper.util.MCUtil; ++import it.unimi.dsi.fastutil.objects.ObjectArrayList; ++import java.util.List; ++import java.util.Optional; ++import net.minecraft.world.effect.MobEffectInstance; ++import org.bukkit.Color; ++import org.bukkit.craftbukkit.potion.CraftPotionType; ++import org.bukkit.craftbukkit.potion.CraftPotionUtil; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.bukkit.potion.PotionEffect; ++import org.bukkit.potion.PotionType; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.jetbrains.annotations.Unmodifiable; ++ ++public record PaperPotionContents( ++ net.minecraft.world.item.alchemy.PotionContents impl ++) implements PotionContents, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.alchemy.PotionContents getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public @Unmodifiable List customEffects() { ++ return MCUtil.transformUnmodifiable(impl.customEffects(), CraftPotionUtil::toBukkit); ++ } ++ ++ @Override ++ public @Nullable PotionType potion() { ++ return this.impl.potion() ++ .map(CraftPotionType::minecraftHolderToBukkit) ++ .orElse(null); ++ } ++ ++ @Override ++ public @Nullable Color customColor() { ++ return this.impl.customColor() ++ .map(Color::fromARGB) // alpha channel is supported for tipped arrows, so let's just leave it in ++ .orElse(null); ++ } ++ ++ @Override ++ public @Nullable String customName() { ++ return this.impl.customName().orElse(null); ++ } ++ ++ static final class BuilderImpl implements PotionContents.Builder { ++ ++ private final List customEffects = new ObjectArrayList<>(); ++ private @Nullable PotionType type; ++ private @Nullable Color color; ++ private @Nullable String customName; ++ ++ @Override ++ public PotionContents.Builder potion(final @Nullable PotionType type) { ++ this.type = type; ++ return this; ++ } ++ ++ @Override ++ public PotionContents.Builder customColor(final @Nullable Color color) { ++ this.color = color; ++ return this; ++ } ++ ++ @Override ++ public Builder customName(final @Nullable String name) { ++ Preconditions.checkArgument(name == null || name.length() <= Short.MAX_VALUE, "Custom name is longer than %s characters", Short.MAX_VALUE); ++ this.customName = name; ++ return this; ++ } ++ ++ @Override ++ public PotionContents.Builder addCustomEffect(final PotionEffect effect) { ++ this.customEffects.add(CraftPotionUtil.fromBukkit(effect)); ++ return this; ++ } ++ ++ @Override ++ public PotionContents.Builder addCustomEffects(final List effects) { ++ effects.forEach(this::addCustomEffect); ++ return this; ++ } ++ ++ @Override ++ public PotionContents build() { ++ if (this.type == null && this.color == null && this.customEffects.isEmpty() && this.customName == null) { ++ return new PaperPotionContents(net.minecraft.world.item.alchemy.PotionContents.EMPTY); ++ } ++ ++ return new PaperPotionContents(new net.minecraft.world.item.alchemy.PotionContents( ++ Optional.ofNullable(this.type).map(CraftPotionType::bukkitToMinecraftHolder), ++ Optional.ofNullable(this.color).map(Color::asARGB), ++ new ObjectArrayList<>(this.customEffects), ++ Optional.ofNullable(this.customName) ++ )); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperRepairable.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperRepairable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..96345e051c4aa77820e857a02768b684d52d7096 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperRepairable.java +@@ -0,0 +1,22 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.registry.RegistryKey; ++import io.papermc.paper.registry.set.PaperRegistrySets; ++import io.papermc.paper.registry.set.RegistryKeySet; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.bukkit.inventory.ItemType; ++ ++public record PaperRepairable( ++ net.minecraft.world.item.enchantment.Repairable impl ++) implements Repairable, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.enchantment.Repairable getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public RegistryKeySet types() { ++ return PaperRegistrySets.convertToApi(RegistryKey.ITEM, this.impl.items()); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperResolvableProfile.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperResolvableProfile.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2c59b17f58502402c3234289b38da28672244cbb +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperResolvableProfile.java +@@ -0,0 +1,105 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import com.destroystokyo.paper.profile.CraftPlayerProfile; ++import com.destroystokyo.paper.profile.PlayerProfile; ++import com.destroystokyo.paper.profile.ProfileProperty; ++import com.google.common.base.Preconditions; ++import com.mojang.authlib.properties.Property; ++import com.mojang.authlib.properties.PropertyMap; ++import io.papermc.paper.util.MCUtil; ++import java.util.Collection; ++import java.util.Optional; ++import java.util.UUID; ++import java.util.concurrent.CompletableFuture; ++import net.minecraft.util.StringUtil; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.jetbrains.annotations.Unmodifiable; ++ ++public record PaperResolvableProfile( ++ net.minecraft.world.item.component.ResolvableProfile impl ++) implements ResolvableProfile, Handleable { ++ ++ static PaperResolvableProfile toApi(final PlayerProfile profile) { ++ return new PaperResolvableProfile(new net.minecraft.world.item.component.ResolvableProfile(CraftPlayerProfile.asAuthlibCopy(profile))); ++ } ++ ++ @Override ++ public net.minecraft.world.item.component.ResolvableProfile getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public @Nullable UUID uuid() { ++ return this.impl.id().orElse(null); ++ } ++ ++ @Override ++ public @Nullable String name() { ++ return this.impl.name().orElse(null); ++ } ++ ++ @Override ++ public @Unmodifiable Collection properties() { ++ return MCUtil.transformUnmodifiable(impl.properties().values(), input -> new ProfileProperty(input.name(), input.value(), input.signature())); ++ } ++ ++ @Override ++ public CompletableFuture resolve() { ++ return this.impl.resolve().thenApply(resolvableProfile -> CraftPlayerProfile.asBukkitCopy(resolvableProfile.gameProfile())); ++ } ++ ++ static final class BuilderImpl implements ResolvableProfile.Builder { ++ ++ private final PropertyMap propertyMap = new PropertyMap(); ++ private @Nullable String name; ++ private @Nullable UUID uuid; ++ ++ @Override ++ public ResolvableProfile.Builder name(final @Nullable String name) { ++ if (name != null) { ++ Preconditions.checkArgument(name.length() <= 16, "name cannot be more than 16 characters, was %s", name.length()); ++ Preconditions.checkArgument(StringUtil.isValidPlayerName(name), "name cannot include invalid characters, was %s", name); ++ } ++ this.name = name; ++ return this; ++ } ++ ++ @Override ++ public ResolvableProfile.Builder uuid(final @Nullable UUID uuid) { ++ this.uuid = uuid; ++ return this; ++ } ++ ++ @Override ++ public ResolvableProfile.Builder addProperty(final ProfileProperty property) { ++ // ProfileProperty constructor already has specific validations ++ final Property newProperty = new Property(property.getName(), property.getValue(), property.getSignature()); ++ if (!this.propertyMap.containsEntry(property.getName(), newProperty)) { // underlying map is a multimap that doesn't allow duplicate key-value pair ++ final int newSize = this.propertyMap.size() + 1; ++ Preconditions.checkArgument(newSize <= 16, "Cannot have more than 16 properties, was %s", newSize); ++ } ++ ++ this.propertyMap.put(property.getName(), newProperty); ++ return this; ++ } ++ ++ @Override ++ public ResolvableProfile.Builder addProperties(final Collection properties) { ++ properties.forEach(this::addProperty); ++ return this; ++ } ++ ++ @Override ++ public ResolvableProfile build() { ++ final PropertyMap shallowCopy = new PropertyMap(); ++ shallowCopy.putAll(this.propertyMap); ++ ++ return new PaperResolvableProfile(new net.minecraft.world.item.component.ResolvableProfile( ++ Optional.ofNullable(this.name), ++ Optional.ofNullable(this.uuid), ++ shallowCopy ++ )); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperSeededContainerLoot.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperSeededContainerLoot.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1ee469b3b690a877e066dbca79706678cd915fa8 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperSeededContainerLoot.java +@@ -0,0 +1,59 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.adventure.PaperAdventure; ++import net.kyori.adventure.key.Key; ++import net.minecraft.core.registries.Registries; ++import net.minecraft.resources.ResourceKey; ++import net.minecraft.world.level.storage.loot.LootTable; ++import org.bukkit.craftbukkit.util.CraftNamespacedKey; ++import org.bukkit.craftbukkit.util.Handleable; ++ ++public record PaperSeededContainerLoot( ++ net.minecraft.world.item.component.SeededContainerLoot impl ++) implements SeededContainerLoot, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.component.SeededContainerLoot getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public Key lootTable() { ++ return CraftNamespacedKey.fromMinecraft(this.impl.lootTable().location()); ++ } ++ ++ @Override ++ public long seed() { ++ return this.impl.seed(); ++ } ++ ++ static final class BuilderImpl implements SeededContainerLoot.Builder { ++ ++ private long seed = LootTable.RANDOMIZE_SEED; ++ private Key key; ++ ++ BuilderImpl(final Key key) { ++ this.key = key; ++ } ++ ++ @Override ++ public SeededContainerLoot.Builder lootTable(final Key key) { ++ this.key = key; ++ return this; ++ } ++ ++ @Override ++ public SeededContainerLoot.Builder seed(final long seed) { ++ this.seed = seed; ++ return this; ++ } ++ ++ @Override ++ public SeededContainerLoot build() { ++ return new PaperSeededContainerLoot(new net.minecraft.world.item.component.SeededContainerLoot( ++ ResourceKey.create(Registries.LOOT_TABLE, PaperAdventure.asVanilla(this.key)), ++ this.seed ++ )); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperSuspiciousStewEffects.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperSuspiciousStewEffects.java +new file mode 100644 +index 0000000000000000000000000000000000000000..69801d8f22945b9818299d8e770fe80a28da7018 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperSuspiciousStewEffects.java +@@ -0,0 +1,58 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.potion.SuspiciousEffectEntry; ++import io.papermc.paper.util.MCUtil; ++import it.unimi.dsi.fastutil.objects.ObjectArrayList; ++import java.util.Collection; ++import java.util.List; ++import org.bukkit.craftbukkit.potion.CraftPotionEffectType; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.jetbrains.annotations.Unmodifiable; ++ ++import static io.papermc.paper.potion.SuspiciousEffectEntry.create; ++ ++public record PaperSuspiciousStewEffects( ++ net.minecraft.world.item.component.SuspiciousStewEffects impl ++) implements SuspiciousStewEffects, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.component.SuspiciousStewEffects getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public @Unmodifiable List effects() { ++ return MCUtil.transformUnmodifiable(impl.effects(), entry -> create(CraftPotionEffectType.minecraftHolderToBukkit(entry.effect()), entry.duration())); ++ } ++ ++ static final class BuilderImpl implements Builder { ++ ++ private final List effects = new ObjectArrayList<>(); ++ ++ @Override ++ public Builder add(final SuspiciousEffectEntry entry) { ++ this.effects.add(new net.minecraft.world.item.component.SuspiciousStewEffects.Entry( ++ org.bukkit.craftbukkit.potion.CraftPotionEffectType.bukkitToMinecraftHolder(entry.effect()), ++ entry.duration() ++ )); ++ return this; ++ } ++ ++ @Override ++ public Builder addAll(final Collection entries) { ++ entries.forEach(this::add); ++ return this; ++ } ++ ++ @Override ++ public SuspiciousStewEffects build() { ++ if (this.effects.isEmpty()) { ++ return new PaperSuspiciousStewEffects(net.minecraft.world.item.component.SuspiciousStewEffects.EMPTY); ++ } ++ ++ return new PaperSuspiciousStewEffects( ++ new net.minecraft.world.item.component.SuspiciousStewEffects(new ObjectArrayList<>(this.effects)) ++ ); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperUnbreakable.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperUnbreakable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..edeb3308af4c359d1930fdbc5417727451b6f0eb +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperUnbreakable.java +@@ -0,0 +1,39 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import org.bukkit.craftbukkit.util.Handleable; ++ ++public record PaperUnbreakable( ++ net.minecraft.world.item.component.Unbreakable impl ++) implements Unbreakable, Handleable { ++ ++ @Override ++ public boolean showInTooltip() { ++ return this.impl.showInTooltip(); ++ } ++ ++ @Override ++ public Unbreakable showInTooltip(final boolean showInTooltip) { ++ return new PaperUnbreakable(this.impl.withTooltip(showInTooltip)); ++ } ++ ++ @Override ++ public net.minecraft.world.item.component.Unbreakable getHandle() { ++ return this.impl; ++ } ++ ++ static final class BuilderImpl implements Unbreakable.Builder { ++ ++ private boolean showInTooltip = true; ++ ++ @Override ++ public Unbreakable.Builder showInTooltip(final boolean showInTooltip) { ++ this.showInTooltip = showInTooltip; ++ return this; ++ } ++ ++ @Override ++ public Unbreakable build() { ++ return new PaperUnbreakable(new net.minecraft.world.item.component.Unbreakable(this.showInTooltip)); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperUseCooldown.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperUseCooldown.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1aeab920faaf5653ddb8e77372060fb8d3226641 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperUseCooldown.java +@@ -0,0 +1,56 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import io.papermc.paper.adventure.PaperAdventure; ++import java.util.Optional; ++import net.kyori.adventure.key.Key; ++import net.minecraft.resources.ResourceLocation; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.jspecify.annotations.Nullable; ++ ++public record PaperUseCooldown( ++ net.minecraft.world.item.component.UseCooldown impl ++) implements UseCooldown, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.component.UseCooldown getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public float seconds() { ++ return this.impl.seconds(); ++ } ++ ++ @Override ++ public @Nullable Key cooldownGroup() { ++ return this.impl.cooldownGroup() ++ .map(PaperAdventure::asAdventure) ++ .orElse(null); ++ } ++ ++ ++ static final class BuilderImpl implements Builder { ++ ++ private final float seconds; ++ private Optional cooldownGroup = Optional.empty(); ++ ++ BuilderImpl(final float seconds) { ++ this.seconds = seconds; ++ } ++ ++ @Override ++ public Builder cooldownGroup(@Nullable final Key key) { ++ this.cooldownGroup = Optional.ofNullable(key) ++ .map(PaperAdventure::asVanilla); ++ ++ return this; ++ } ++ ++ @Override ++ public UseCooldown build() { ++ return new PaperUseCooldown( ++ new net.minecraft.world.item.component.UseCooldown(this.seconds, this.cooldownGroup) ++ ); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperUseRemainder.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperUseRemainder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c2c04506940704c2ec9a5e6bb469c4771e2d49c2 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperUseRemainder.java +@@ -0,0 +1,20 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import org.bukkit.craftbukkit.inventory.CraftItemStack; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.bukkit.inventory.ItemStack; ++ ++public record PaperUseRemainder( ++ net.minecraft.world.item.component.UseRemainder impl ++) implements UseRemainder, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.component.UseRemainder getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public ItemStack transformInto() { ++ return CraftItemStack.asBukkitCopy(this.impl.convertInto()); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperWritableBookContent.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperWritableBookContent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..563ac5345efe68cfe223853df13d5f07cc48e850 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperWritableBookContent.java +@@ -0,0 +1,105 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import com.google.common.base.Preconditions; ++import io.papermc.paper.text.Filtered; ++import io.papermc.paper.util.MCUtil; ++import it.unimi.dsi.fastutil.objects.ObjectArrayList; ++import java.util.List; ++import java.util.Optional; ++import net.minecraft.server.network.Filterable; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.jetbrains.annotations.Unmodifiable; ++ ++import static io.papermc.paper.text.Filtered.of; ++ ++public record PaperWritableBookContent( ++ net.minecraft.world.item.component.WritableBookContent impl ++) implements WritableBookContent, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.component.WritableBookContent getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public @Unmodifiable List> pages() { ++ return MCUtil.transformUnmodifiable(impl.pages(), input -> of(input.raw(), input.filtered().orElse(null))); ++ } ++ ++ static final class BuilderImpl implements WritableBookContent.Builder { ++ ++ private final List> pages = new ObjectArrayList<>(); ++ ++ private static void validatePageLength(final String page) { ++ Preconditions.checkArgument( ++ page.length() <= net.minecraft.world.item.component.WritableBookContent.PAGE_EDIT_LENGTH, ++ "Cannot have page length more than %s, had %s", ++ net.minecraft.world.item.component.WritableBookContent.PAGE_EDIT_LENGTH, ++ page.length() ++ ); ++ } ++ ++ private static void validatePageCount(final int current, final int add) { ++ final int newSize = current + add; ++ Preconditions.checkArgument( ++ newSize <= net.minecraft.world.item.component.WritableBookContent.MAX_PAGES, ++ "Cannot have more than %s pages, had %s", ++ net.minecraft.world.item.component.WritableBookContent.MAX_PAGES, ++ newSize ++ ); ++ } ++ ++ @Override ++ public WritableBookContent.Builder addPage(final String page) { ++ validatePageLength(page); ++ validatePageCount(this.pages.size(), 1); ++ this.pages.add(Filterable.passThrough(page)); ++ return this; ++ } ++ ++ @Override ++ public WritableBookContent.Builder addPages(final List pages) { ++ validatePageCount(this.pages.size(), pages.size()); ++ for (final String page : pages) { ++ validatePageLength(page); ++ this.pages.add(Filterable.passThrough(page)); ++ } ++ return this; ++ } ++ ++ @Override ++ public WritableBookContent.Builder addFilteredPage(final Filtered page) { ++ validatePageLength(page.raw()); ++ if (page.filtered() != null) { ++ validatePageLength(page.filtered()); ++ } ++ validatePageCount(this.pages.size(), 1); ++ this.pages.add(new Filterable<>(page.raw(), Optional.ofNullable(page.filtered()))); ++ return this; ++ } ++ ++ @Override ++ public WritableBookContent.Builder addFilteredPages(final List> pages) { ++ validatePageCount(this.pages.size(), pages.size()); ++ for (final Filtered page : pages) { ++ validatePageLength(page.raw()); ++ if (page.filtered() != null) { ++ validatePageLength(page.filtered()); ++ } ++ this.pages.add(new Filterable<>(page.raw(), Optional.ofNullable(page.filtered()))); ++ } ++ return this; ++ } ++ ++ @Override ++ public WritableBookContent build() { ++ if (this.pages.isEmpty()) { ++ return new PaperWritableBookContent(net.minecraft.world.item.component.WritableBookContent.EMPTY); ++ } ++ ++ return new PaperWritableBookContent( ++ new net.minecraft.world.item.component.WritableBookContent(new ObjectArrayList<>(this.pages)) ++ ); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperWrittenBookContent.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperWrittenBookContent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e6ce137806cd789586108fef5a5da508b19b7131 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperWrittenBookContent.java +@@ -0,0 +1,183 @@ ++package io.papermc.paper.datacomponent.item; ++ ++import com.google.common.base.Preconditions; ++import io.papermc.paper.adventure.PaperAdventure; ++import io.papermc.paper.text.Filtered; ++import io.papermc.paper.util.MCUtil; ++import it.unimi.dsi.fastutil.objects.ObjectArrayList; ++import java.util.List; ++import java.util.Optional; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.ComponentLike; ++import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; ++import net.minecraft.server.network.Filterable; ++import net.minecraft.util.GsonHelper; ++import org.bukkit.craftbukkit.util.Handleable; ++import org.jetbrains.annotations.Unmodifiable; ++ ++import static io.papermc.paper.adventure.PaperAdventure.asAdventure; ++import static io.papermc.paper.adventure.PaperAdventure.asVanilla; ++ ++public record PaperWrittenBookContent( ++ net.minecraft.world.item.component.WrittenBookContent impl ++) implements WrittenBookContent, Handleable { ++ ++ @Override ++ public net.minecraft.world.item.component.WrittenBookContent getHandle() { ++ return this.impl; ++ } ++ ++ @Override ++ public Filtered title() { ++ return Filtered.of(this.impl.title().raw(), this.impl.title().filtered().orElse(null)); ++ } ++ ++ @Override ++ public String author() { ++ return this.impl.author(); ++ } ++ ++ @Override ++ public int generation() { ++ return this.impl.generation(); ++ } ++ ++ @Override ++ public @Unmodifiable List> pages() { ++ return MCUtil.transformUnmodifiable( ++ impl.pages(), ++ page -> Filtered.of(asAdventure(page.raw()), page.filtered().map(PaperAdventure::asAdventure).orElse(null)) ++ ); ++ } ++ ++ @Override ++ public boolean resolved() { ++ return this.impl.resolved(); ++ } ++ ++ static final class BuilderImpl implements WrittenBookContent.Builder { ++ ++ private final List> pages = new ObjectArrayList<>(); ++ private Filterable title; ++ private String author; ++ private int generation = 0; ++ private boolean resolved = false; ++ ++ BuilderImpl(final Filtered title, final String author) { ++ validateTitle(title.raw()); ++ if (title.filtered() != null) { ++ validateTitle(title.filtered()); ++ } ++ this.title = new Filterable<>(title.raw(), Optional.ofNullable(title.filtered())); ++ this.author = author; ++ } ++ ++ private static void validateTitle(final String title) { ++ Preconditions.checkArgument( ++ title.length() <= net.minecraft.world.item.component.WrittenBookContent.TITLE_MAX_LENGTH, ++ "Title cannot be longer than %s, was %s", ++ net.minecraft.world.item.component.WrittenBookContent.TITLE_MAX_LENGTH, ++ title.length() ++ ); ++ } ++ ++ private static void validatePageLength(final Component page) { ++ final String flagPage = GsonHelper.toStableString(GsonComponentSerializer.gson().serializeToTree(page)); ++ Preconditions.checkArgument( ++ flagPage.length() <= net.minecraft.world.item.component.WrittenBookContent.PAGE_LENGTH, ++ "Cannot have page length more than %s, had %s", ++ net.minecraft.world.item.component.WrittenBookContent.PAGE_LENGTH, ++ flagPage.length() ++ ); ++ } ++ ++ @Override ++ public WrittenBookContent.Builder title(final String title) { ++ validateTitle(title); ++ this.title = Filterable.passThrough(title); ++ return this; ++ } ++ ++ @Override ++ public WrittenBookContent.Builder filteredTitle(final Filtered title) { ++ validateTitle(title.raw()); ++ if (title.filtered() != null) { ++ validateTitle(title.filtered()); ++ } ++ this.title = new Filterable<>(title.raw(), Optional.ofNullable(title.filtered())); ++ return this; ++ } ++ ++ @Override ++ public WrittenBookContent.Builder author(final String author) { ++ this.author = author; ++ return this; ++ } ++ ++ @Override ++ public WrittenBookContent.Builder generation(final int generation) { ++ Preconditions.checkArgument( ++ generation >= 0 && generation <= net.minecraft.world.item.component.WrittenBookContent.MAX_GENERATION, ++ "generation must be between %s and %s, was %s", ++ 0, net.minecraft.world.item.component.WrittenBookContent.MAX_GENERATION, ++ generation ++ ); ++ this.generation = generation; ++ return this; ++ } ++ ++ @Override ++ public WrittenBookContent.Builder resolved(final boolean resolved) { ++ this.resolved = resolved; ++ return this; ++ } ++ ++ @Override ++ public WrittenBookContent.Builder addPage(final ComponentLike page) { ++ final Component component = page.asComponent(); ++ validatePageLength(component); ++ this.pages.add(Filterable.passThrough(asVanilla(component))); ++ return this; ++ } ++ ++ @Override ++ public WrittenBookContent.Builder addPages(final List pages) { ++ for (final ComponentLike page : pages) { ++ final Component component = page.asComponent(); ++ validatePageLength(component); ++ this.pages.add(Filterable.passThrough(asVanilla(component))); ++ } ++ return this; ++ } ++ ++ @Override ++ public WrittenBookContent.Builder addFilteredPage(final Filtered page) { ++ final Component raw = page.raw().asComponent(); ++ validatePageLength(raw); ++ Component filtered = null; ++ if (page.filtered() != null) { ++ filtered = page.filtered().asComponent(); ++ validatePageLength(filtered); ++ } ++ this.pages.add(new Filterable<>(asVanilla(raw), Optional.ofNullable(filtered).map(PaperAdventure::asVanilla))); ++ return this; ++ } ++ ++ @Override ++ public WrittenBookContent.Builder addFilteredPages(final List> pages) { ++ pages.forEach(this::addFilteredPage); ++ return this; ++ } ++ ++ @Override ++ public WrittenBookContent build() { ++ return new PaperWrittenBookContent(new net.minecraft.world.item.component.WrittenBookContent( ++ this.title, ++ this.author, ++ this.generation, ++ new ObjectArrayList<>(this.pages), ++ this.resolved ++ )); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/ConsumableTypesBridgeImpl.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/ConsumableTypesBridgeImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c96cb39cf21ebe33d09733affc3cb031d94213f2 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/ConsumableTypesBridgeImpl.java +@@ -0,0 +1,66 @@ ++package io.papermc.paper.datacomponent.item.consumable; ++ ++import com.google.common.base.Preconditions; ++import com.google.common.collect.Lists; ++import io.papermc.paper.registry.set.PaperRegistrySets; ++import io.papermc.paper.registry.set.RegistryKeySet; ++import java.util.ArrayList; ++import java.util.List; ++import io.papermc.paper.util.MCUtil; ++import net.kyori.adventure.key.Key; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.core.registries.Registries; ++import org.bukkit.craftbukkit.potion.CraftPotionUtil; ++import org.bukkit.potion.PotionEffect; ++import org.bukkit.potion.PotionEffectType; ++import org.jetbrains.annotations.ApiStatus; ++import org.jspecify.annotations.NullMarked; ++ ++@ApiStatus.Internal ++@NullMarked ++public class ConsumableTypesBridgeImpl implements ConsumableTypesBridge { ++ ++ @Override ++ public ConsumeEffect.ApplyStatusEffects applyStatusEffects(final List effectList, final float probability) { ++ Preconditions.checkArgument(0 <= probability && probability <= 1, "probability must be between 0-1, was %s", probability); ++ return new PaperApplyStatusEffects( ++ new net.minecraft.world.item.consume_effects.ApplyStatusEffectsConsumeEffect( ++ new ArrayList<>(Lists.transform(effectList, CraftPotionUtil::fromBukkit)), ++ probability ++ ) ++ ); ++ } ++ ++ @Override ++ public ConsumeEffect.RemoveStatusEffects removeStatusEffects(final RegistryKeySet potionEffectTypeTagKey) { ++ return new PaperRemoveStatusEffects( ++ new net.minecraft.world.item.consume_effects.RemoveStatusEffectsConsumeEffect( ++ PaperRegistrySets.convertToNms(Registries.MOB_EFFECT, BuiltInRegistries.BUILT_IN_CONVERSIONS.lookup(), potionEffectTypeTagKey) ++ ) ++ ); ++ } ++ ++ @Override ++ public ConsumeEffect.ClearAllStatusEffects clearAllStatusEffects() { ++ return new PaperClearAllStatusEffects( ++ new net.minecraft.world.item.consume_effects.ClearAllStatusEffectsConsumeEffect() ++ ); ++ } ++ ++ @Override ++ public ConsumeEffect.PlaySound playSoundEffect(final Key sound) { ++ return new PaperPlaySound( ++ new net.minecraft.world.item.consume_effects.PlaySoundConsumeEffect( ++ MCUtil.keyToSound(sound) ++ ) ++ ); ++ } ++ ++ @Override ++ public ConsumeEffect.TeleportRandomly teleportRandomlyEffect(final float diameter) { ++ Preconditions.checkArgument(diameter > 0, "diameter must be positive, was %s", diameter); ++ return new PaperTeleportRandomly( ++ new net.minecraft.world.item.consume_effects.TeleportRandomlyConsumeEffect(diameter) ++ ); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperApplyStatusEffects.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperApplyStatusEffects.java +new file mode 100644 +index 0000000000000000000000000000000000000000..465bec185aad849f283aae39bc23e9ba7a052371 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperApplyStatusEffects.java +@@ -0,0 +1,28 @@ ++package io.papermc.paper.datacomponent.item.consumable; ++ ++import java.util.List; ++import net.minecraft.world.item.consume_effects.ApplyStatusEffectsConsumeEffect; ++import org.bukkit.craftbukkit.potion.CraftPotionUtil; ++import org.bukkit.potion.PotionEffect; ++ ++import static io.papermc.paper.datacomponent.ComponentUtils.transform; ++ ++public record PaperApplyStatusEffects( ++ ApplyStatusEffectsConsumeEffect impl ++) implements ConsumeEffect.ApplyStatusEffects, PaperConsumableEffectImpl { ++ ++ @Override ++ public List effects() { ++ return transform(this.impl().effects(), CraftPotionUtil::toBukkit); ++ } ++ ++ @Override ++ public float probability() { ++ return this.impl.probability(); ++ } ++ ++ @Override ++ public ApplyStatusEffectsConsumeEffect getHandle() { ++ return this.impl; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperClearAllStatusEffects.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperClearAllStatusEffects.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2afcbbbeb486783737fd606113b6f938d0a18cb5 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperClearAllStatusEffects.java +@@ -0,0 +1,11 @@ ++package io.papermc.paper.datacomponent.item.consumable; ++ ++public record PaperClearAllStatusEffects( ++ net.minecraft.world.item.consume_effects.ClearAllStatusEffectsConsumeEffect impl ++) implements ConsumeEffect.ClearAllStatusEffects, PaperConsumableEffectImpl { ++ ++ @Override ++ public net.minecraft.world.item.consume_effects.ClearAllStatusEffectsConsumeEffect getHandle() { ++ return this.impl; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffectImpl.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffectImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..05ede1d3f5b0b5ea3a5004cb4a7a153ed7714a55 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffectImpl.java +@@ -0,0 +1,7 @@ ++package io.papermc.paper.datacomponent.item.consumable; ++ ++import net.minecraft.world.item.consume_effects.ConsumeEffect; ++import org.bukkit.craftbukkit.util.Handleable; ++ ++public interface PaperConsumableEffectImpl extends Handleable { ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffects.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffects.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ff07939ef0730a11c712c09c360da8a21a777618 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffects.java +@@ -0,0 +1,32 @@ ++package io.papermc.paper.datacomponent.item.consumable; ++ ++import net.minecraft.world.item.consume_effects.ApplyStatusEffectsConsumeEffect; ++import net.minecraft.world.item.consume_effects.ClearAllStatusEffectsConsumeEffect; ++import net.minecraft.world.item.consume_effects.PlaySoundConsumeEffect; ++import net.minecraft.world.item.consume_effects.RemoveStatusEffectsConsumeEffect; ++import net.minecraft.world.item.consume_effects.TeleportRandomlyConsumeEffect; ++ ++public final class PaperConsumableEffects { ++ ++ private PaperConsumableEffects() { ++ } ++ ++ public static ConsumeEffect fromNms(net.minecraft.world.item.consume_effects.ConsumeEffect consumable) { ++ return switch (consumable) { ++ case ApplyStatusEffectsConsumeEffect effect -> new PaperApplyStatusEffects(effect); ++ case ClearAllStatusEffectsConsumeEffect effect -> new PaperClearAllStatusEffects(effect); ++ case PlaySoundConsumeEffect effect -> new PaperPlaySound(effect); ++ case RemoveStatusEffectsConsumeEffect effect -> new PaperRemoveStatusEffects(effect); ++ case TeleportRandomlyConsumeEffect effect -> new PaperTeleportRandomly(effect); ++ default -> throw new UnsupportedOperationException("Don't know how to convert " + consumable.getClass()); ++ }; ++ } ++ ++ public static net.minecraft.world.item.consume_effects.ConsumeEffect toNms(ConsumeEffect effect) { ++ if (effect instanceof PaperConsumableEffectImpl consumableEffect) { ++ return consumableEffect.getHandle(); ++ } else { ++ throw new UnsupportedOperationException("Must implement handleable!"); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperPlaySound.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperPlaySound.java +new file mode 100644 +index 0000000000000000000000000000000000000000..26a8ee292b45e57462e6e6629b328fbf9d6b47e7 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperPlaySound.java +@@ -0,0 +1,20 @@ ++package io.papermc.paper.datacomponent.item.consumable; ++ ++import io.papermc.paper.adventure.PaperAdventure; ++import net.kyori.adventure.key.Key; ++import net.minecraft.world.item.consume_effects.PlaySoundConsumeEffect; ++ ++public record PaperPlaySound( ++ PlaySoundConsumeEffect impl ++) implements ConsumeEffect.PlaySound, PaperConsumableEffectImpl { ++ ++ @Override ++ public Key sound() { ++ return PaperAdventure.asAdventure(this.impl.sound().value().location()); ++ } ++ ++ @Override ++ public PlaySoundConsumeEffect getHandle() { ++ return this.impl; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperRemoveStatusEffects.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperRemoveStatusEffects.java +new file mode 100644 +index 0000000000000000000000000000000000000000..20e09c6ebab91b1ec103aa149d0f57a2a5502644 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperRemoveStatusEffects.java +@@ -0,0 +1,21 @@ ++package io.papermc.paper.datacomponent.item.consumable; ++ ++import io.papermc.paper.registry.RegistryKey; ++import io.papermc.paper.registry.set.PaperRegistrySets; ++import io.papermc.paper.registry.set.RegistryKeySet; ++import org.bukkit.potion.PotionEffectType; ++ ++public record PaperRemoveStatusEffects( ++ net.minecraft.world.item.consume_effects.RemoveStatusEffectsConsumeEffect impl ++) implements ConsumeEffect.RemoveStatusEffects, PaperConsumableEffectImpl { ++ ++ @Override ++ public RegistryKeySet removeEffects() { ++ return PaperRegistrySets.convertToApi(RegistryKey.MOB_EFFECT, this.impl.effects()); ++ } ++ ++ @Override ++ public net.minecraft.world.item.consume_effects.RemoveStatusEffectsConsumeEffect getHandle() { ++ return this.impl; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperTeleportRandomly.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperTeleportRandomly.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c21889e9984f7c36d9f19771c2e23b6efba5197d +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperTeleportRandomly.java +@@ -0,0 +1,15 @@ ++package io.papermc.paper.datacomponent.item.consumable; ++ ++public record PaperTeleportRandomly( ++ net.minecraft.world.item.consume_effects.TeleportRandomlyConsumeEffect impl ++) implements ConsumeEffect.TeleportRandomly, PaperConsumableEffectImpl { ++ @Override ++ public float diameter() { ++ return this.impl.diameter(); ++ } ++ ++ @Override ++ public net.minecraft.world.item.consume_effects.TeleportRandomlyConsumeEffect getHandle() { ++ return this.impl; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/package-info.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/package-info.java +new file mode 100644 +index 0000000000000000000000000000000000000000..af6720a49a9d336a345e2bc91d6714f6b2c39886 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/package-info.java +@@ -0,0 +1,7 @@ ++/** ++ * Relating to consumable effects for components. ++ */ ++@NullMarked ++package io.papermc.paper.datacomponent.item.consumable; ++ ++import org.jspecify.annotations.NullMarked; +diff --git a/src/main/java/io/papermc/paper/datacomponent/item/package-info.java b/src/main/java/io/papermc/paper/datacomponent/item/package-info.java +new file mode 100644 +index 0000000000000000000000000000000000000000..02a69025662d6a887f5449fd5eaf7d1083973bf3 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/package-info.java +@@ -0,0 +1,4 @@ ++@NullMarked ++package io.papermc.paper.datacomponent.item; ++ ++import org.jspecify.annotations.NullMarked; +diff --git a/src/main/java/io/papermc/paper/datacomponent/package-info.java b/src/main/java/io/papermc/paper/datacomponent/package-info.java +new file mode 100644 +index 0000000000000000000000000000000000000000..62aa1061c35d5358e6dec16a52574b427cc4b732 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/package-info.java +@@ -0,0 +1,4 @@ ++@NullMarked ++package io.papermc.paper.datacomponent; ++ ++import org.jspecify.annotations.NullMarked; +diff --git a/src/main/java/io/papermc/paper/registry/PaperRegistries.java b/src/main/java/io/papermc/paper/registry/PaperRegistries.java +index f8c6da955e4bd0e480c7b581d2a4325738f9dd6f..ee1fce58c6e57dd93a30ee66e7488a92f9da2fe3 100644 +--- a/src/main/java/io/papermc/paper/registry/PaperRegistries.java ++++ b/src/main/java/io/papermc/paper/registry/PaperRegistries.java +@@ -1,6 +1,8 @@ + package io.papermc.paper.registry; + + import io.papermc.paper.adventure.PaperAdventure; ++import io.papermc.paper.datacomponent.DataComponentType; ++import io.papermc.paper.datacomponent.PaperComponentType; + import io.papermc.paper.registry.data.PaperEnchantmentRegistryEntry; + import io.papermc.paper.registry.data.PaperGameEventRegistryEntry; + import io.papermc.paper.registry.entry.RegistryEntry; +@@ -96,6 +98,7 @@ public final class PaperRegistries { + entry(Registries.ATTRIBUTE, RegistryKey.ATTRIBUTE, Attribute.class, CraftAttribute::new), + entry(Registries.FLUID, RegistryKey.FLUID, Fluid.class, CraftFluid::new), + entry(Registries.SOUND_EVENT, RegistryKey.SOUND_EVENT, Sound.class, CraftSound::new), ++ entry(Registries.DATA_COMPONENT_TYPE, RegistryKey.DATA_COMPONENT_TYPE, DataComponentType.class, PaperComponentType::of), + + // data-drivens + entry(Registries.BIOME, RegistryKey.BIOME, Biome.class, CraftBiome::new).delayed(), +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java +index 756c73a401437566258813946fa10c7caa8f2469..bfddaa146c1bcb6a3fdd43773ec06f91a259b200 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java +@@ -206,7 +206,7 @@ public final class CraftItemStack extends ItemStack { + this.adjustTagForItemMeta(oldType); // Paper + } + } +- this.setData(null); ++ this.setData((MaterialData) null); // Paper + } + + @Override +@@ -245,7 +245,7 @@ public final class CraftItemStack extends ItemStack { + + @Override + public int getMaxStackSize() { +- return (this.handle == null) ? Material.AIR.getMaxStackSize() : this.handle.getMaxStackSize(); ++ return (this.handle == null) ? 64 : this.handle.getMaxStackSize(); // Paper - air stacks to 64 + } + + // Paper start +@@ -267,12 +267,14 @@ public final class CraftItemStack extends ItemStack { + public void addUnsafeEnchantment(Enchantment ench, int level) { + Preconditions.checkArgument(ench != null, "Enchantment cannot be null"); + +- // Paper start - Replace whole method +- final ItemMeta itemMeta = this.getItemMeta(); +- if (itemMeta != null) { +- itemMeta.addEnchant(ench, level, true); +- this.setItemMeta(itemMeta); ++ // Paper start ++ if (this.handle == null) { ++ return; + } ++ ++ EnchantmentHelper.updateEnchantments(this.handle, mutable -> { // data component api doesn't really support mutable things once already set yet ++ mutable.set(CraftEnchantment.bukkitToMinecraftHolder(ench), level); ++ }); + // Paper end + } + +@@ -302,17 +304,28 @@ public final class CraftItemStack extends ItemStack { + public int removeEnchantment(Enchantment ench) { + Preconditions.checkArgument(ench != null, "Enchantment cannot be null"); + +- // Paper start - replace entire method +- int level = getEnchantmentLevel(ench); +- if (level > 0) { +- final ItemMeta itemMeta = this.getItemMeta(); +- if (itemMeta == null) return 0; +- itemMeta.removeEnchant(ench); +- this.setItemMeta(itemMeta); ++ // Paper start ++ if (this.handle == null) { ++ return 0; ++ } ++ ++ ItemEnchantments itemEnchantments = this.handle.getOrDefault(DataComponents.ENCHANTMENTS, ItemEnchantments.EMPTY); ++ if (itemEnchantments.isEmpty()) { ++ return 0; + } +- // Paper end + +- return level; ++ Holder removedEnchantment = CraftEnchantment.bukkitToMinecraftHolder(ench); ++ if (itemEnchantments.keySet().contains(removedEnchantment)) { ++ int previousLevel = itemEnchantments.getLevel(removedEnchantment); ++ ++ ItemEnchantments.Mutable mutable = new ItemEnchantments.Mutable(itemEnchantments); // data component api doesn't really support mutable things once already set yet ++ mutable.removeIf(enchantment -> enchantment.equals(removedEnchantment)); ++ this.handle.set(DataComponents.ENCHANTMENTS, mutable.toImmutable()); ++ return previousLevel; ++ } ++ ++ return 0; ++ // Paper end + } + + @Override +@@ -324,7 +337,13 @@ public final class CraftItemStack extends ItemStack { + + @Override + public Map getEnchantments() { +- return this.hasItemMeta() ? this.getItemMeta().getEnchants() : ImmutableMap.of(); // Paper - use Item Meta ++ // Paper start ++ io.papermc.paper.datacomponent.item.ItemEnchantments itemEnchantments = this.getData(io.papermc.paper.datacomponent.DataComponentTypes.ENCHANTMENTS); // empty constant might be useful here ++ if (itemEnchantments == null) { ++ return java.util.Collections.emptyMap(); ++ } ++ return itemEnchantments.enchantments(); ++ // Paper end + } + + static Map getEnchantments(net.minecraft.world.item.ItemStack item) { +@@ -526,4 +545,119 @@ public final class CraftItemStack extends ItemStack { + return this.pdcView; + } + // Paper end - pdc ++ // Paper start - data component API ++ @Override ++ public T getData(final io.papermc.paper.datacomponent.DataComponentType.Valued type) { ++ if (this.isEmpty()) { ++ return null; ++ } ++ return io.papermc.paper.datacomponent.PaperComponentType.convertDataComponentValue(this.handle.getComponents(), (io.papermc.paper.datacomponent.PaperComponentType.ValuedImpl) type); ++ } ++ ++ @Override ++ public boolean hasData(final io.papermc.paper.datacomponent.DataComponentType type) { ++ if (this.isEmpty()) { ++ return false; ++ } ++ return this.handle.has(io.papermc.paper.datacomponent.PaperComponentType.bukkitToMinecraft(type)); ++ } ++ ++ @Override ++ public java.util.Set getDataTypes() { ++ if (this.isEmpty()) { ++ return java.util.Collections.emptySet(); ++ } ++ return io.papermc.paper.datacomponent.PaperComponentType.minecraftToBukkit(this.handle.getComponents().keySet()); ++ } ++ ++ @Override ++ public void setData(final io.papermc.paper.datacomponent.DataComponentType.Valued type, final T value) { ++ Preconditions.checkArgument(value != null, "value cannot be null"); ++ if (this.isEmpty()) { ++ return; ++ } ++ this.setDataInternal((io.papermc.paper.datacomponent.PaperComponentType.ValuedImpl) type, value); ++ } ++ ++ @Override ++ public void setData(final io.papermc.paper.datacomponent.DataComponentType.NonValued type) { ++ if (this.isEmpty()) { ++ return; ++ } ++ this.setDataInternal((io.papermc.paper.datacomponent.PaperComponentType.NonValuedImpl) type, null); ++ } ++ ++ private void setDataInternal(final io.papermc.paper.datacomponent.PaperComponentType type, final A value) { ++ this.handle.set(type.getHandle(), type.getAdapter().toVanilla(value)); ++ } ++ ++ @Override ++ public void unsetData(final io.papermc.paper.datacomponent.DataComponentType type) { ++ if (this.isEmpty()) { ++ return; ++ } ++ this.handle.remove(io.papermc.paper.datacomponent.PaperComponentType.bukkitToMinecraft(type)); ++ } ++ ++ @Override ++ public void resetData(final io.papermc.paper.datacomponent.DataComponentType type) { ++ if (this.isEmpty()) { ++ return; ++ } ++ this.resetData((io.papermc.paper.datacomponent.PaperComponentType) type); ++ } ++ ++ private void resetData(final io.papermc.paper.datacomponent.PaperComponentType type) { ++ final net.minecraft.core.component.DataComponentType nms = io.papermc.paper.datacomponent.PaperComponentType.bukkitToMinecraft(type); ++ final M nmsValue = this.handle.getItem().components().get(nms); ++ // if nmsValue is null, it will clear any set patch ++ // if nmsValue is not null, it will still clear any set patch because it will equal the default value ++ this.handle.set(nms, nmsValue); ++ } ++ ++ @Override ++ public boolean isDataOverridden(final io.papermc.paper.datacomponent.DataComponentType type) { ++ if (this.isEmpty()) { ++ return false; ++ } ++ final net.minecraft.core.component.DataComponentType nms = io.papermc.paper.datacomponent.PaperComponentType.bukkitToMinecraft(type); ++ // maybe a more efficient way is to expose the "patch" map in PatchedDataComponentMap and just check if the type exists as a key ++ return !java.util.Objects.equals(this.handle.get(nms), this.handle.getPrototype().get(nms)); ++ } ++ ++ @Override ++ public boolean matchesWithoutData(final ItemStack item, final java.util.Set exclude, final boolean ignoreCount) { ++ // Extracted from base equals ++ final CraftItemStack craftStack = getCraftStack(item); ++ if (this.handle == craftStack.handle) return true; ++ if (this.handle == null || craftStack.handle == null) return false; ++ if (this.handle.isEmpty() && craftStack.handle.isEmpty()) return true; ++ ++ net.minecraft.world.item.ItemStack left = this.handle; ++ net.minecraft.world.item.ItemStack right = craftStack.handle; ++ if (!ignoreCount && left.getCount() != right.getCount()) { ++ return false; ++ } ++ if (!left.is(right.getItem())) { ++ return false; ++ } ++ ++ // It can be assumed that the prototype is equal since the type is the same. This way all we need to check is the patch ++ ++ // Fast path when excluded types is empty ++ if (exclude.isEmpty()) { ++ return left.getComponentsPatch().equals(right.getComponentsPatch()); ++ } ++ ++ // Collect all the NMS types into a set ++ java.util.Set> skippingTypes = new java.util.HashSet<>(exclude.size()); ++ for (io.papermc.paper.datacomponent.DataComponentType api : exclude) { ++ skippingTypes.add(io.papermc.paper.datacomponent.PaperComponentType.bukkitToMinecraft(api)); ++ } ++ ++ // Check the patch by first stripping excluded types and then compare the trimmed patches ++ return left.getComponentsPatch().forget(skippingTypes::contains).equals(right.getComponentsPatch().forget(skippingTypes::contains)); ++ } ++ ++ // Paper end - data component API + } +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java +index 1b57649d0d3db24ed32c78cf3d5ce1d9fb1353e0..ce1287edd7db00279ec8569d767ab6272c8ae3fb 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java +@@ -270,4 +270,20 @@ public class CraftItemType implements ItemType.Typed, Han + return rarity == null ? null : org.bukkit.inventory.ItemRarity.valueOf(rarity.name()); + } + // Paper end - expand ItemRarity API ++ // Paper start - data component API ++ @Override ++ public T getDefaultData(final io.papermc.paper.datacomponent.DataComponentType.Valued type) { ++ return io.papermc.paper.datacomponent.PaperComponentType.convertDataComponentValue(this.item.components(), ((io.papermc.paper.datacomponent.PaperComponentType.ValuedImpl) type)); ++ } ++ ++ @Override ++ public boolean hasDefaultData(final io.papermc.paper.datacomponent.DataComponentType type) { ++ return this.item.components().has(io.papermc.paper.datacomponent.PaperComponentType.bukkitToMinecraft(type)); ++ } ++ ++ @Override ++ public java.util.Set getDefaultDataTypes() { ++ return io.papermc.paper.datacomponent.PaperComponentType.minecraftToBukkit(this.item.components().keySet()); ++ } ++ // Paper end - data component API + } +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java +index a944803771d514572f94b4e98a6d4435a009c078..82cb8cd1635c279326cec8454f1906ce35021dec 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java +@@ -91,7 +91,7 @@ class CraftMetaFirework extends CraftMetaItem implements FireworkMeta { + this.safelyAddEffects(effects, false); // Paper - limit firework effects + } + +- static FireworkEffect getEffect(FireworkExplosion explosion) { ++ public static FireworkEffect getEffect(FireworkExplosion explosion) { // Paper + FireworkEffect.Builder effect = FireworkEffect.builder() + .flicker(explosion.hasTwinkle()) + .trail(explosion.hasTrail()) +@@ -111,7 +111,7 @@ class CraftMetaFirework extends CraftMetaItem implements FireworkMeta { + return effect.build(); + } + +- static FireworkExplosion getExplosion(FireworkEffect effect) { ++ public static FireworkExplosion getExplosion(FireworkEffect effect) { // Paper + IntList colors = CraftMetaFirework.addColors(effect.getColors()); + IntList fadeColors = CraftMetaFirework.addColors(effect.getFadeColors()); + +diff --git a/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.ItemComponentTypesBridge b/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.ItemComponentTypesBridge +new file mode 100644 +index 0000000000000000000000000000000000000000..0fd276c2fdbba784c1cd95105553996b4ba2460e +--- /dev/null ++++ b/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.ItemComponentTypesBridge +@@ -0,0 +1 @@ ++io.papermc.paper.datacomponent.item.ItemComponentTypesBridgesImpl +diff --git a/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.consumable.ConsumableTypesBridge b/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.consumable.ConsumableTypesBridge +new file mode 100644 +index 0000000000000000000000000000000000000000..852ab097181491735fb9ee5ee4f70e4ceeb32e6d +--- /dev/null ++++ b/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.consumable.ConsumableTypesBridge +@@ -0,0 +1 @@ ++io.papermc.paper.datacomponent.item.consumable.ConsumableTypesBridgeImpl +diff --git a/src/test/java/io/papermc/paper/datacomponent/DataComponentTypesTest.java b/src/test/java/io/papermc/paper/datacomponent/DataComponentTypesTest.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1d707114f53e80bf278dc640c55b515d85f03120 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/datacomponent/DataComponentTypesTest.java +@@ -0,0 +1,58 @@ ++package io.papermc.paper.datacomponent; ++ ++import com.google.common.collect.Collections2; ++import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; ++import net.minecraft.core.registries.Registries; ++import net.minecraft.resources.ResourceLocation; ++import org.bukkit.craftbukkit.util.CraftNamespacedKey; ++import org.bukkit.support.RegistryHelper; ++import org.bukkit.support.environment.AllFeatures; ++import org.junit.jupiter.api.Assertions; ++import org.junit.jupiter.api.Test; ++import java.lang.reflect.Field; ++import java.util.Set; ++import java.util.function.Predicate; ++import java.util.stream.Collectors; ++ ++@AllFeatures ++public class DataComponentTypesTest { ++ ++ private static final Set NOT_IN_API = Set.of( ++ ResourceLocation.parse("custom_data"), ++ ResourceLocation.parse("entity_data"), ++ ResourceLocation.parse("bees"), ++ ResourceLocation.parse("debug_stick_state"), ++ ResourceLocation.parse("block_entity_data"), ++ ResourceLocation.parse("bucket_entity_data"), ++ ResourceLocation.parse("lock"), ++ ResourceLocation.parse("creative_slot_lock") ++ ); ++ ++ @Test ++ public void testAllDataComponentsAreMapped() throws IllegalAccessException { ++ final Set vanillaDataComponentTypes = new ObjectOpenHashSet<>( ++ RegistryHelper.getRegistry() ++ .lookupOrThrow(Registries.DATA_COMPONENT_TYPE) ++ .keySet() ++ ); ++ ++ for (final Field declaredField : DataComponentTypes.class.getDeclaredFields()) { ++ if (!DataComponentType.class.isAssignableFrom(declaredField.getType())) continue; ++ ++ final DataComponentType dataComponentType = (DataComponentType) declaredField.get(null); ++ if (!vanillaDataComponentTypes.remove(CraftNamespacedKey.toMinecraft(dataComponentType.getKey()))) { ++ Assertions.fail("API defined component type " + dataComponentType.key().asMinimalString() + " is unknown to vanilla registry"); ++ } ++ } ++ ++ if (!vanillaDataComponentTypes.containsAll(NOT_IN_API)) { ++ Assertions.fail("API defined data components that were marked as not-yet-implemented: " + NOT_IN_API.stream().filter(Predicate.not(vanillaDataComponentTypes::contains)).map(ResourceLocation::toString).collect(Collectors.joining(", "))); ++ } ++ ++ vanillaDataComponentTypes.removeAll(NOT_IN_API); ++ if (!vanillaDataComponentTypes.isEmpty()) { ++ Assertions.fail("API did not define following vanilla data component types: " + String.join(", ", Collections2.transform(vanillaDataComponentTypes, ResourceLocation::toString))); ++ } ++ } ++ ++} +diff --git a/src/test/java/io/papermc/paper/item/ItemStackDataComponentEqualsTest.java b/src/test/java/io/papermc/paper/item/ItemStackDataComponentEqualsTest.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4ee0491763341232844a99aa528310a3b3dca1d5 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/item/ItemStackDataComponentEqualsTest.java +@@ -0,0 +1,92 @@ ++package io.papermc.paper.item; ++ ++import io.papermc.paper.datacomponent.DataComponentTypes; ++import java.util.Set; ++import net.kyori.adventure.text.Component; ++import org.bukkit.Material; ++import org.bukkit.inventory.ItemStack; ++import org.bukkit.support.environment.AllFeatures; ++import org.junit.jupiter.api.Assertions; ++import org.junit.jupiter.api.Test; ++ ++@AllFeatures ++class ItemStackDataComponentEqualsTest { ++ ++ @Test ++ public void testEqual() { ++ ItemStack item1 = ItemStack.of(Material.STONE, 1); ++ item1.setData(DataComponentTypes.MAX_STACK_SIZE, 32); ++ item1.setData(DataComponentTypes.ITEM_NAME, Component.text("HI")); ++ ++ ItemStack item2 = ItemStack.of(Material.STONE, 1); ++ item2.setData(DataComponentTypes.MAX_STACK_SIZE, 32); ++ item2.setData(DataComponentTypes.ITEM_NAME, Component.text("HI")); ++ ++ Assertions.assertTrue(item1.matchesWithoutData(item2, Set.of())); ++ } ++ ++ @Test ++ public void testEqualIgnoreComponent() { ++ ItemStack item1 = ItemStack.of(Material.STONE, 2); ++ item1.setData(DataComponentTypes.MAX_STACK_SIZE, 1); ++ ++ ItemStack item2 = ItemStack.of(Material.STONE, 1); ++ item2.setData(DataComponentTypes.MAX_STACK_SIZE, 2); ++ ++ Assertions.assertFalse(item1.matchesWithoutData(item2, Set.of(DataComponentTypes.MAX_STACK_SIZE))); ++ } ++ ++ @Test ++ public void testEqualIgnoreComponentAndSize() { ++ ItemStack item1 = ItemStack.of(Material.STONE, 2); ++ item1.setData(DataComponentTypes.MAX_STACK_SIZE, 1); ++ ++ ItemStack item2 = ItemStack.of(Material.STONE, 1); ++ item2.setData(DataComponentTypes.MAX_STACK_SIZE, 2); ++ ++ Assertions.assertTrue(item1.matchesWithoutData(item2, Set.of(DataComponentTypes.MAX_STACK_SIZE), true)); ++ } ++ ++ @Test ++ public void testEqualWithoutComponent() { ++ ItemStack item1 = ItemStack.of(Material.STONE, 1); ++ ++ ItemStack item2 = ItemStack.of(Material.STONE, 1); ++ item2.setData(DataComponentTypes.MAX_STACK_SIZE, 2); ++ ++ Assertions.assertFalse(item1.matchesWithoutData(item2, Set.of(DataComponentTypes.WRITTEN_BOOK_CONTENT))); ++ } ++ ++ @Test ++ public void testEqualRemoveComponent() { ++ ItemStack item1 = ItemStack.of(Material.STONE, 1); ++ item1.unsetData(DataComponentTypes.MAX_STACK_SIZE); ++ ++ ItemStack item2 = ItemStack.of(Material.STONE, 1); ++ item2.unsetData(DataComponentTypes.MAX_STACK_SIZE); ++ ++ Assertions.assertTrue(item1.matchesWithoutData(item2, Set.of())); ++ } ++ ++ @Test ++ public void testEqualIncludeComponentIgnoreSize() { ++ ItemStack item1 = ItemStack.of(Material.STONE, 2); ++ item1.setData(DataComponentTypes.MAX_STACK_SIZE, 1); ++ ++ ItemStack item2 = ItemStack.of(Material.STONE, 1); ++ item2.setData(DataComponentTypes.MAX_STACK_SIZE, 1); ++ ++ Assertions.assertTrue(item1.matchesWithoutData(item2, Set.of(), true)); ++ } ++ ++ @Test ++ public void testAdvancedExample() { ++ ItemStack oakLeaves = ItemStack.of(Material.OAK_LEAVES, 1); ++ oakLeaves.setData(DataComponentTypes.HIDE_TOOLTIP); ++ oakLeaves.setData(DataComponentTypes.MAX_STACK_SIZE, 1); ++ ++ ItemStack otherOakLeavesItem = ItemStack.of(Material.OAK_LEAVES, 2); ++ ++ Assertions.assertTrue(oakLeaves.matchesWithoutData(otherOakLeavesItem, Set.of(DataComponentTypes.HIDE_TOOLTIP, DataComponentTypes.MAX_STACK_SIZE), true)); ++ } ++} +diff --git a/src/test/java/io/papermc/paper/item/ItemStackDataComponentTest.java b/src/test/java/io/papermc/paper/item/ItemStackDataComponentTest.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5825be69af0b949ce28d6bde28bef68935db0d45 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/item/ItemStackDataComponentTest.java +@@ -0,0 +1,371 @@ ++package io.papermc.paper.item; ++ ++import io.papermc.paper.datacomponent.DataComponentType; ++import io.papermc.paper.datacomponent.DataComponentTypes; ++import io.papermc.paper.datacomponent.item.ChargedProjectiles; ++import io.papermc.paper.datacomponent.item.CustomModelData; ++import io.papermc.paper.datacomponent.item.DyedItemColor; ++import io.papermc.paper.datacomponent.item.Fireworks; ++import io.papermc.paper.datacomponent.item.FoodProperties; ++import io.papermc.paper.datacomponent.item.ItemArmorTrim; ++import io.papermc.paper.datacomponent.item.ItemAttributeModifiers; ++import io.papermc.paper.datacomponent.item.ItemEnchantments; ++import io.papermc.paper.datacomponent.item.ItemLore; ++import io.papermc.paper.datacomponent.item.JukeboxPlayable; ++import io.papermc.paper.datacomponent.item.MapId; ++import io.papermc.paper.datacomponent.item.MapItemColor; ++import io.papermc.paper.datacomponent.item.PotDecorations; ++import io.papermc.paper.datacomponent.item.Tool; ++import io.papermc.paper.datacomponent.item.Unbreakable; ++import io.papermc.paper.registry.RegistryAccess; ++import io.papermc.paper.registry.RegistryKey; ++import io.papermc.paper.registry.set.RegistrySet; ++import io.papermc.paper.registry.tag.TagKey; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.util.TriState; ++import org.bukkit.Color; ++import org.bukkit.FireworkEffect; ++import org.bukkit.JukeboxSong; ++import org.bukkit.Material; ++import org.bukkit.NamespacedKey; ++import org.bukkit.Registry; ++import org.bukkit.attribute.Attribute; ++import org.bukkit.attribute.AttributeModifier; ++import org.bukkit.block.BlockState; ++import org.bukkit.block.BlockType; ++import org.bukkit.block.DecoratedPot; ++import org.bukkit.enchantments.Enchantment; ++import org.bukkit.inventory.EquipmentSlotGroup; ++import org.bukkit.inventory.ItemFlag; ++import org.bukkit.inventory.ItemRarity; ++import org.bukkit.inventory.ItemStack; ++import org.bukkit.inventory.ItemType; ++import org.bukkit.inventory.meta.ArmorMeta; ++import org.bukkit.inventory.meta.BlockStateMeta; ++import org.bukkit.inventory.meta.CrossbowMeta; ++import org.bukkit.inventory.meta.Damageable; ++import org.bukkit.inventory.meta.FireworkMeta; ++import org.bukkit.inventory.meta.ItemMeta; ++import org.bukkit.inventory.meta.LeatherArmorMeta; ++import org.bukkit.inventory.meta.MapMeta; ++import org.bukkit.inventory.meta.Repairable; ++import org.bukkit.inventory.meta.components.FoodComponent; ++import org.bukkit.inventory.meta.components.JukeboxPlayableComponent; ++import org.bukkit.inventory.meta.components.ToolComponent; ++import org.bukkit.inventory.meta.trim.ArmorTrim; ++import org.bukkit.inventory.meta.trim.TrimMaterial; ++import org.bukkit.inventory.meta.trim.TrimPattern; ++import org.bukkit.support.environment.AllFeatures; ++import org.junit.jupiter.api.Assertions; ++import org.junit.jupiter.api.Test; ++import java.util.List; ++import java.util.Map; ++import java.util.function.BiConsumer; ++import java.util.function.Function; ++ ++@AllFeatures ++class ItemStackDataComponentTest { ++ ++ @Test ++ void testMaxStackSize() { ++ testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.MAX_STACK_SIZE, 32, ItemMeta.class, ItemMeta::getMaxStackSize, ItemMeta::setMaxStackSize); ++ } ++ ++ @Test ++ void testMaxDamage() { ++ testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.MAX_DAMAGE, 120, Damageable.class, Damageable::getMaxDamage, Damageable::setMaxDamage); ++ } ++ ++ @Test ++ void testDamage() { ++ testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.DAMAGE, 120, Damageable.class, Damageable::getDamage, Damageable::setDamage); ++ } ++ ++ @Test ++ void testUnbreakable() { ++ final ItemStack stack = new ItemStack(Material.STONE); ++ stack.setData(DataComponentTypes.UNBREAKABLE, Unbreakable.unbreakable().showInTooltip(false).build()); ++ ++ Assertions.assertTrue(stack.getItemMeta().isUnbreakable()); ++ Assertions.assertTrue(stack.getItemMeta().getItemFlags().contains(ItemFlag.HIDE_UNBREAKABLE)); ++ stack.unsetData(DataComponentTypes.UNBREAKABLE); ++ Assertions.assertFalse(stack.getItemMeta().isUnbreakable()); ++ } ++ ++ @Test ++ void testHideAdditionalTooltip() { ++ final ItemStack stack = new ItemStack(Material.STONE); ++ stack.setData(DataComponentTypes.HIDE_ADDITIONAL_TOOLTIP); ++ ++ Assertions.assertTrue(stack.getItemMeta().getItemFlags().contains(ItemFlag.HIDE_ADDITIONAL_TOOLTIP)); ++ stack.unsetData(DataComponentTypes.HIDE_ADDITIONAL_TOOLTIP); ++ Assertions.assertFalse(stack.getItemMeta().getItemFlags().contains(ItemFlag.HIDE_ADDITIONAL_TOOLTIP)); ++ } ++ ++ @Test ++ void testHideTooltip() { ++ ItemStack stack = new ItemStack(Material.STONE); ++ stack.setData(DataComponentTypes.HIDE_TOOLTIP); ++ ++ Assertions.assertEquals(stack.getItemMeta().isHideTooltip(), stack.hasData(DataComponentTypes.HIDE_TOOLTIP)); ++ Assertions.assertTrue(stack.getItemMeta().isHideTooltip()); ++ stack.unsetData(DataComponentTypes.HIDE_TOOLTIP); ++ Assertions.assertFalse(stack.getItemMeta().isHideTooltip()); ++ stack = new ItemStack(Material.STONE); ++ ++ stack.unsetData(DataComponentTypes.HIDE_TOOLTIP); ++ Assertions.assertFalse(stack.getItemMeta().isHideTooltip()); ++ Assertions.assertEquals(stack.getItemMeta().isHideTooltip(), stack.hasData(DataComponentTypes.HIDE_TOOLTIP)); ++ } ++ ++ @Test ++ void testRepairCost() { ++ final ItemStack stack = new ItemStack(Material.STONE); ++ testWithMeta(stack, DataComponentTypes.REPAIR_COST, 120, Repairable.class, Repairable::getRepairCost, Repairable::setRepairCost); ++ } ++ ++ @Test ++ void testCustomName() { ++ testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.CUSTOM_NAME, Component.text("HELLO!!!!!!"), ItemMeta.class, ItemMeta::displayName, ItemMeta::displayName); ++ } ++ ++ @Test ++ void testItemName() { ++ testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.ITEM_NAME, Component.text("HELLO!!!!!! ITEM NAME"), ItemMeta.class, ItemMeta::itemName, ItemMeta::itemName); ++ } ++ ++ @Test ++ void testItemLore() { ++ List list = List.of(Component.text("1"), Component.text("2")); ++ testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.LORE, ItemLore.lore().lines(list).build(), ItemLore::lines, ItemMeta.class, ItemMeta::lore, ItemMeta::lore); ++ } ++ ++ @Test ++ void testItemRarity() { ++ testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.RARITY, ItemRarity.RARE, ItemMeta.class, ItemMeta::getRarity, ItemMeta::setRarity); ++ } ++ ++ @Test ++ void testItemEnchantments() { ++ final ItemStack stack = new ItemStack(Material.STONE); ++ Map enchantmentIntegerMap = Map.of(Enchantment.SOUL_SPEED, 1); ++ stack.setData(DataComponentTypes.ENCHANTMENTS, ItemEnchantments.itemEnchantments(enchantmentIntegerMap, false)); ++ ++ Assertions.assertTrue(stack.getItemMeta().hasItemFlag(ItemFlag.HIDE_ENCHANTS)); ++ Assertions.assertEquals(1, stack.getItemMeta().getEnchantLevel(Enchantment.SOUL_SPEED)); ++ Assertions.assertEquals(stack.getItemMeta().getEnchants(), enchantmentIntegerMap); ++ stack.unsetData(DataComponentTypes.ENCHANTMENTS); ++ Assertions.assertTrue(stack.getItemMeta().getEnchants().isEmpty()); ++ } ++ ++ @Test ++ void testItemAttributes() { ++ final ItemStack stack = new ItemStack(Material.STONE); ++ AttributeModifier modifier = new AttributeModifier(NamespacedKey.minecraft("test"), 5, AttributeModifier.Operation.ADD_NUMBER, EquipmentSlotGroup.ANY); ++ stack.setData(DataComponentTypes.ATTRIBUTE_MODIFIERS, ItemAttributeModifiers.itemAttributes().showInTooltip(false).addModifier(Attribute.ATTACK_DAMAGE, modifier).build()); ++ ++ Assertions.assertTrue(stack.getItemMeta().hasItemFlag(ItemFlag.HIDE_ATTRIBUTES)); ++ Assertions.assertEquals(modifier, ((List) stack.getItemMeta().getAttributeModifiers(Attribute.ATTACK_DAMAGE)).getFirst()); ++ stack.unsetData(DataComponentTypes.ATTRIBUTE_MODIFIERS); ++ Assertions.assertNull(stack.getItemMeta().getAttributeModifiers()); ++ } ++ ++ @Test ++ void testCustomModelData() { ++ testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.CUSTOM_MODEL_DATA, CustomModelData.customModelData(1), CustomModelData::id, ItemMeta.class, ItemMeta::getCustomModelData, ItemMeta::setCustomModelData); ++ } ++ ++ @Test ++ void testEnchantmentGlintOverride() { ++ testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, true, ItemMeta.class, ItemMeta::getEnchantmentGlintOverride, ItemMeta::setEnchantmentGlintOverride); ++ } ++ ++ @Test ++ void testFood() { ++ FoodProperties properties = FoodProperties.food() ++ .canAlwaysEat(true) ++ .saturation(1.3F) ++ .nutrition(1) ++ .build(); ++ ++ final ItemStack stack = new ItemStack(Material.CROSSBOW); ++ stack.setData(DataComponentTypes.FOOD, properties); ++ ++ ItemMeta meta = stack.getItemMeta(); ++ FoodComponent component = meta.getFood(); ++ Assertions.assertEquals(properties.canAlwaysEat(), component.canAlwaysEat()); ++ Assertions.assertEquals(properties.saturation(), component.getSaturation()); ++ Assertions.assertEquals(properties.nutrition(), component.getNutrition()); ++ ++ stack.unsetData(DataComponentTypes.FOOD); ++ meta = stack.getItemMeta(); ++ Assertions.assertFalse(meta.hasFood()); ++ } ++ ++ @Test ++ void testTool() { ++ Tool properties = Tool.tool() ++ .damagePerBlock(1) ++ .defaultMiningSpeed(2F) ++ .addRules(List.of( ++ Tool.rule( ++ RegistrySet.keySetFromValues(RegistryKey.BLOCK, List.of(BlockType.STONE, BlockType.GRAVEL)), ++ 2F, ++ TriState.TRUE ++ ), ++ Tool.rule( ++ RegistryAccess.registryAccess().getRegistry(RegistryKey.BLOCK).getTag(TagKey.create(RegistryKey.BLOCK, NamespacedKey.minecraft("bamboo_blocks"))), ++ 2F, ++ TriState.TRUE ++ ) ++ )) ++ .build(); ++ ++ final ItemStack stack = new ItemStack(Material.CROSSBOW); ++ stack.setData(DataComponentTypes.TOOL, properties); ++ ++ ItemMeta meta = stack.getItemMeta(); ++ ToolComponent component = meta.getTool(); ++ Assertions.assertEquals(properties.damagePerBlock(), component.getDamagePerBlock()); ++ Assertions.assertEquals(properties.defaultMiningSpeed(), component.getDefaultMiningSpeed()); ++ ++ int idx = 0; ++ for (ToolComponent.ToolRule effect : component.getRules()) { ++ Assertions.assertEquals(properties.rules().get(idx).speed(), effect.getSpeed()); ++ Assertions.assertEquals(properties.rules().get(idx).correctForDrops().toBoolean(), effect.isCorrectForDrops()); ++ Assertions.assertEquals(properties.rules().get(idx).blocks().resolve(Registry.BLOCK), effect.getBlocks().stream().map(Material::asBlockType).toList()); ++ idx++; ++ } ++ ++ stack.unsetData(DataComponentTypes.TOOL); ++ meta = stack.getItemMeta(); ++ Assertions.assertFalse(meta.hasTool()); ++ } ++ ++ @Test ++ void testJukeboxPlayable() { ++ JukeboxPlayable properties = JukeboxPlayable.jukeboxPlayable(JukeboxSong.MALL).build(); ++ ++ final ItemStack stack = new ItemStack(Material.BEEF); ++ stack.setData(DataComponentTypes.JUKEBOX_PLAYABLE, properties); ++ ++ ItemMeta meta = stack.getItemMeta(); ++ JukeboxPlayableComponent component = meta.getJukeboxPlayable(); ++ Assertions.assertEquals(properties.jukeboxSong(), component.getSong()); ++ ++ stack.unsetData(DataComponentTypes.JUKEBOX_PLAYABLE); ++ meta = stack.getItemMeta(); ++ Assertions.assertFalse(meta.hasJukeboxPlayable()); ++ } ++ ++ @Test ++ void testDyedColor() { ++ final ItemStack stack = new ItemStack(Material.LEATHER_CHESTPLATE); ++ Color color = Color.BLUE; ++ stack.setData(DataComponentTypes.DYED_COLOR, DyedItemColor.dyedItemColor(color, false)); ++ ++ Assertions.assertTrue(stack.getItemMeta().hasItemFlag(ItemFlag.HIDE_DYE)); ++ Assertions.assertEquals(color, ((LeatherArmorMeta) stack.getItemMeta()).getColor()); ++ stack.unsetData(DataComponentTypes.DYED_COLOR); ++ Assertions.assertFalse(((LeatherArmorMeta) stack.getItemMeta()).isDyed()); ++ } ++ ++ @Test ++ void testMapColor() { ++ testWithMeta(new ItemStack(Material.FILLED_MAP), DataComponentTypes.MAP_COLOR, MapItemColor.mapItemColor().color(Color.BLUE).build(), MapItemColor::color, MapMeta.class, MapMeta::getColor, MapMeta::setColor); ++ } ++ ++ @Test ++ void testMapId() { ++ testWithMeta(new ItemStack(Material.FILLED_MAP), DataComponentTypes.MAP_ID, MapId.mapId(1), MapId::id, MapMeta.class, MapMeta::getMapId, MapMeta::setMapId); ++ } ++ ++ @Test ++ void testFireworks() { ++ testWithMeta(new ItemStack(Material.FIREWORK_ROCKET), DataComponentTypes.FIREWORKS, Fireworks.fireworks(List.of(FireworkEffect.builder().build()), 1), Fireworks::effects, FireworkMeta.class, FireworkMeta::getEffects, (fireworkMeta, effects) -> { ++ fireworkMeta.clearEffects(); ++ fireworkMeta.addEffects(effects); ++ }); ++ ++ testWithMeta(new ItemStack(Material.FIREWORK_ROCKET), DataComponentTypes.FIREWORKS, Fireworks.fireworks(List.of(FireworkEffect.builder().build()), 1), Fireworks::flightDuration, FireworkMeta.class, FireworkMeta::getPower, FireworkMeta::setPower); ++ } ++ ++ @Test ++ void testTrim() { ++ final ItemStack stack = new ItemStack(Material.LEATHER_CHESTPLATE); ++ ItemArmorTrim armorTrim = ItemArmorTrim.itemArmorTrim(new ArmorTrim(TrimMaterial.AMETHYST, TrimPattern.BOLT), false); ++ stack.setData(DataComponentTypes.TRIM, armorTrim); ++ ++ Assertions.assertTrue(stack.getItemMeta().hasItemFlag(ItemFlag.HIDE_ARMOR_TRIM)); ++ Assertions.assertEquals(armorTrim.armorTrim(), ((ArmorMeta) stack.getItemMeta()).getTrim()); ++ stack.unsetData(DataComponentTypes.TRIM); ++ Assertions.assertFalse(stack.getItemMeta().hasItemFlag(ItemFlag.HIDE_ARMOR_TRIM)); ++ Assertions.assertFalse(((ArmorMeta) stack.getItemMeta()).hasTrim()); ++ } ++ ++ @Test ++ void testChargedProjectiles() { ++ final ItemStack stack = new ItemStack(Material.CROSSBOW); ++ ItemStack projectile = new ItemStack(Material.FIREWORK_ROCKET); ++ stack.setData(DataComponentTypes.CHARGED_PROJECTILES, ChargedProjectiles.chargedProjectiles().add(projectile).build()); ++ ++ CrossbowMeta meta = (CrossbowMeta) stack.getItemMeta(); ++ Assertions.assertEquals(meta.getChargedProjectiles().getFirst(), projectile); ++ ++ stack.unsetData(DataComponentTypes.CHARGED_PROJECTILES); ++ meta = (CrossbowMeta) stack.getItemMeta(); ++ Assertions.assertTrue(meta.getChargedProjectiles().isEmpty()); ++ } ++ ++ @Test ++ void testPot() { ++ final ItemStack stack = new ItemStack(Material.DECORATED_POT); ++ stack.setData(DataComponentTypes.POT_DECORATIONS, PotDecorations.potDecorations().back(ItemType.DANGER_POTTERY_SHERD).build()); ++ ++ BlockState state = ((BlockStateMeta) stack.getItemMeta()).getBlockState(); ++ DecoratedPot decoratedPot = (DecoratedPot) state; ++ ++ Assertions.assertEquals(decoratedPot.getSherd(DecoratedPot.Side.BACK), Material.DANGER_POTTERY_SHERD); ++ stack.unsetData(DataComponentTypes.POT_DECORATIONS); ++ decoratedPot = (DecoratedPot) ((BlockStateMeta) stack.getItemMeta()).getBlockState(); ++ Assertions.assertTrue(decoratedPot.getSherds().values().stream().allMatch((m) -> m.asItemType() == ItemType.BRICK)); ++ } ++ ++ private static void testWithMeta(final ItemStack stack, final DataComponentType.Valued type, final T value, final Class metaType, final Function metaGetter, final BiConsumer metaSetter) { ++ testWithMeta(stack, type, value, Function.identity(), metaType, metaGetter, metaSetter); ++ } ++ ++ private static void testWithMeta(final ItemStack stack, final DataComponentType.Valued type, final T value, Function mapper, final Class metaType, final Function metaGetter, final BiConsumer metaSetter) { ++ ItemStack original = stack.clone(); ++ stack.setData(type, value); ++ ++ Assertions.assertEquals(value, stack.getData(type)); ++ ++ final ItemMeta meta = stack.getItemMeta(); ++ final M typedMeta = Assertions.assertInstanceOf(metaType, meta); ++ ++ Assertions.assertEquals(metaGetter.apply(typedMeta), mapper.apply(value)); ++ ++ // SETTING ++ metaSetter.accept(typedMeta, mapper.apply(value)); ++ original.setItemMeta(typedMeta); ++ Assertions.assertEquals(value, original.getData(type)); ++ } ++ ++ private static void testWithMeta(final ItemStack stack, final DataComponentType.NonValued type, final boolean value, final Class metaType, final Function metaGetter, final BiConsumer metaSetter) { ++ ItemStack original = stack.clone(); ++ stack.setData(type); ++ ++ Assertions.assertEquals(value, stack.hasData(type)); ++ ++ final ItemMeta meta = stack.getItemMeta(); ++ final M typedMeta = Assertions.assertInstanceOf(metaType, meta); ++ ++ Assertions.assertEquals(metaGetter.apply(typedMeta), value); ++ ++ // SETTING ++ metaSetter.accept(typedMeta, value); ++ original.setItemMeta(typedMeta); ++ Assertions.assertEquals(value, original.hasData(type)); ++ } ++} +diff --git a/src/test/java/io/papermc/paper/item/MetaComparisonTest.java b/src/test/java/io/papermc/paper/item/MetaComparisonTest.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1225ad95a204434ef6af5e6e92593cbbafc31b4a +--- /dev/null ++++ b/src/test/java/io/papermc/paper/item/MetaComparisonTest.java +@@ -0,0 +1,284 @@ ++package io.papermc.paper.item; ++ ++import com.destroystokyo.paper.profile.CraftPlayerProfile; ++import com.destroystokyo.paper.profile.PlayerProfile; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.event.HoverEvent; ++import net.kyori.adventure.text.format.NamedTextColor; ++import org.bukkit.Bukkit; ++import org.bukkit.ChatColor; ++import org.bukkit.Color; ++import org.bukkit.Material; ++import org.bukkit.craftbukkit.inventory.CraftItemFactory; ++import org.bukkit.craftbukkit.inventory.CraftItemStack; ++import org.bukkit.enchantments.Enchantment; ++import org.bukkit.inventory.ItemFactory; ++import org.bukkit.inventory.ItemStack; ++import org.bukkit.inventory.meta.BookMeta; ++import org.bukkit.inventory.meta.ItemMeta; ++import org.bukkit.inventory.meta.PotionMeta; ++import org.bukkit.inventory.meta.SkullMeta; ++import org.bukkit.potion.PotionEffect; ++import org.bukkit.potion.PotionEffectType; ++import org.bukkit.support.environment.AllFeatures; ++import org.junit.jupiter.api.Assertions; ++import org.junit.jupiter.api.Disabled; ++import org.junit.jupiter.api.Test; ++ ++import java.util.UUID; ++import java.util.function.Consumer; ++ ++// TODO: This should technically be used to compare legacy meta vs the newly implemented ++@AllFeatures ++public class MetaComparisonTest { ++ ++ private static final ItemFactory FACTORY = CraftItemFactory.instance(); ++ ++ @Test ++ public void testMetaApplication() { ++ ItemStack itemStack = new ItemStack(Material.STONE); ++ ++ ItemMeta meta = itemStack.getItemMeta(); ++ meta.setCustomModelData(1); ++ ++ ItemMeta converted = FACTORY.asMetaFor(meta, Material.GOLD_INGOT); ++ Assertions.assertEquals(converted.getCustomModelData(), meta.getCustomModelData()); ++ ++ ItemMeta convertedAdvanced = FACTORY.asMetaFor(meta, Material.PLAYER_HEAD); ++ Assertions.assertEquals(convertedAdvanced.getCustomModelData(), meta.getCustomModelData()); ++ } ++ ++ @Test ++ public void testMetaApplicationDowngrading() { ++ ItemStack itemStack = new ItemStack(Material.PLAYER_HEAD); ++ PlayerProfile profile = Bukkit.createProfile("Owen1212055"); ++ ++ SkullMeta meta = (SkullMeta) itemStack.getItemMeta(); ++ meta.setPlayerProfile(profile); ++ ++ SkullMeta converted = (SkullMeta) FACTORY.asMetaFor(meta, Material.PLAYER_HEAD); ++ Assertions.assertEquals(converted.getPlayerProfile(), meta.getPlayerProfile()); ++ ++ SkullMeta downgraded = (SkullMeta) FACTORY.asMetaFor(FACTORY.asMetaFor(meta, Material.STONE), Material.PLAYER_HEAD); ++ Assertions.assertNull(downgraded.getPlayerProfile()); ++ } ++ ++ @Test ++ public void testMetaApplicationDowngradingPotion() { ++ ItemStack itemStack = new ItemStack(Material.POTION); ++ Color color = Color.BLUE; ++ ++ PotionMeta meta = (PotionMeta) itemStack.getItemMeta(); ++ meta.setColor(color); ++ ++ PotionMeta converted = (PotionMeta) FACTORY.asMetaFor(meta, Material.POTION); ++ Assertions.assertEquals(converted.getColor(), color); ++ ++ PotionMeta downgraded = (PotionMeta) FACTORY.asMetaFor(FACTORY.asMetaFor(meta, Material.STONE), Material.POTION); ++ Assertions.assertNull(downgraded.getColor()); ++ } ++ ++ @Test ++ public void testNullMeta() { ++ ItemStack itemStack = new ItemStack(Material.AIR); ++ ++ Assertions.assertFalse(itemStack.hasItemMeta()); ++ Assertions.assertNull(itemStack.getItemMeta()); ++ } ++ ++ @Test ++ public void testPotionMeta() { ++ PotionEffect potionEffect = new PotionEffect(PotionEffectType.SPEED, 10, 10, false); ++ ItemStack nmsItemStack = new ItemStack(Material.POTION, 1); ++ ++ testSetAndGet(nmsItemStack, ++ (meta) -> ((PotionMeta) meta).addCustomEffect(potionEffect, true), ++ (meta) -> Assertions.assertEquals(potionEffect, ((PotionMeta) meta).getCustomEffects().getFirst()) ++ ); ++ } ++ ++ @Test ++ public void testEnchantment() { ++ ItemStack stack = new ItemStack(Material.STICK, 1); ++ ++ testSetAndGet(stack, ++ (meta) -> Assertions.assertTrue(meta.addEnchant(Enchantment.SHARPNESS, 1, true)), ++ (meta) -> Assertions.assertEquals(1, meta.getEnchantLevel(Enchantment.SHARPNESS)) ++ ); ++ } ++ ++ @Test ++ @Disabled ++ public void testPlayerHead() { ++ PlayerProfile profile = new CraftPlayerProfile(UUID.randomUUID(), "Owen1212055"); ++ ItemStack stack = new ItemStack(Material.PLAYER_HEAD, 1); ++ ++ testSetAndGet(stack, ++ (meta) -> ((SkullMeta) meta).setPlayerProfile(profile), ++ (meta) -> { ++ Assertions.assertTrue(((SkullMeta) meta).hasOwner()); ++ Assertions.assertEquals(profile, ((SkullMeta) meta).getPlayerProfile()); ++ } ++ ); ++ ++ testSetAndGet(stack, ++ (meta) -> ((SkullMeta) meta).setOwner("Owen1212055"), ++ (meta) -> { ++ Assertions.assertTrue(((SkullMeta) meta).hasOwner()); ++ Assertions.assertEquals("Owen1212055", ((SkullMeta) meta).getOwner()); ++ } ++ ); ++ } ++ ++ @Test ++ public void testBookMetaAuthor() { ++ ItemStack stack = new ItemStack(Material.WRITTEN_BOOK, 1); ++ ++ // Legacy string ++ testSetAndGet(stack, ++ (meta) -> ((BookMeta) meta).setAuthor("Owen1212055"), ++ (meta) -> Assertions.assertEquals("Owen1212055", ((BookMeta) meta).getAuthor()) ++ ); ++ ++ // Component Colored ++ Component coloredName = Component.text("Owen1212055", NamedTextColor.DARK_GRAY); ++ testSetAndGet(stack, ++ (meta) -> ((BookMeta) meta).author(coloredName), ++ (meta) -> Assertions.assertEquals(coloredName, ((BookMeta) meta).author()) ++ ); ++ ++ // Simple text ++ Component name = Component.text("Owen1212055"); ++ testSetAndGet(stack, ++ (meta) -> ((BookMeta) meta).author(name), ++ (meta) -> Assertions.assertEquals(name, ((BookMeta) meta).author()) ++ ); ++ } ++ ++ @Test ++ public void testBookMetaTitle() { ++ ItemStack stack = new ItemStack(Material.WRITTEN_BOOK, 1); ++ ++ // Legacy string ++ testSetAndGet(stack, ++ (meta) -> ((BookMeta) meta).setTitle("Owen1212055"), ++ (meta) -> Assertions.assertEquals("Owen1212055", ((BookMeta) meta).getTitle()) ++ ); ++ ++ // Component Colored ++ Component coloredName = Component.text("Owen1212055", NamedTextColor.DARK_GRAY); ++ testSetAndGet(stack, ++ (meta) -> ((BookMeta) meta).title(coloredName), ++ (meta) -> Assertions.assertEquals(coloredName, ((BookMeta) meta).title()) ++ ); ++ ++ // Simple text ++ Component name = Component.text("Owen1212055"); ++ testSetAndGet(stack, ++ (meta) -> ((BookMeta) meta).title(name), ++ (meta) -> Assertions.assertEquals(name, ((BookMeta) meta).title()) ++ ); ++ } ++ ++ ++ @Test ++ public void testWriteableBookPages() { ++ ItemStack stack = new ItemStack(Material.WRITABLE_BOOK, 1); ++ ++ // Writeable books are serialized as plain text, but has weird legacy color support. ++ // So, we need to test to make sure that all works here. ++ ++ // Legacy string ++ testSetAndGet(stack, ++ (meta) -> ((BookMeta) meta).addPage("Owen1212055"), ++ (meta) -> Assertions.assertEquals("Owen1212055", ((BookMeta) meta).getPage(1)) ++ ); ++ ++ // Legacy string colored ++ String translatedLegacy = ChatColor.translateAlternateColorCodes('&', "&7Owen1212055"); ++ testSetAndGet(stack, ++ (meta) -> ((BookMeta) meta).addPage(translatedLegacy), ++ (meta) -> Assertions.assertEquals(translatedLegacy, ((BookMeta) meta).getPage(1)) ++ ); ++ ++ // Component Colored ++ Component coloredName = Component.text("Owen1212055", NamedTextColor.DARK_GRAY); ++ testSetAndGet(stack, ++ (meta) -> ((BookMeta) meta).addPages(coloredName), ++ (meta) -> Assertions.assertEquals(coloredName, ((BookMeta) meta).page(1)) ++ ); ++ ++ // Simple text ++ Component name = Component.text("Owen1212055"); ++ testSetAndGet(stack, ++ (meta) -> ((BookMeta) meta).addPages(name), ++ (meta) -> Assertions.assertEquals(name, ((BookMeta) meta).page(1)) ++ ); ++ ++ // Simple text + hover... should NOT be saved ++ // As this is plain text ++ Component nameWithHover = Component.text("Owen1212055") ++ .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("Hover"))); ++ testSetAndGet(stack, ++ (meta) -> ((BookMeta) meta).addPages(nameWithHover), ++ (meta) -> Assertions.assertEquals(name, ((BookMeta) meta).page(1)) ++ ); ++ } ++ ++ @Test ++ public void testWrittenBookPages() { ++ ItemStack stack = new ItemStack(Material.WRITTEN_BOOK, 1); ++ ++ // Writeable books are serialized as plain text, but has weird legacy color support. ++ // So, we need to test to make sure that all works here. ++ ++ // Legacy string ++ testSetAndGet(stack, ++ (meta) -> ((BookMeta) meta).addPage("Owen1212055"), ++ (meta) -> Assertions.assertEquals("Owen1212055", ((BookMeta) meta).getPage(1)) ++ ); ++ ++ // Legacy string colored ++ String translatedLegacy = ChatColor.translateAlternateColorCodes('&', "&7Owen1212055"); ++ testSetAndGet(stack, ++ (meta) -> ((BookMeta) meta).addPage(translatedLegacy), ++ (meta) -> Assertions.assertEquals(translatedLegacy, ((BookMeta) meta).getPage(1)) ++ ); ++ ++ // Component Colored ++ Component coloredName = Component.text("Owen1212055", NamedTextColor.DARK_GRAY); ++ testSetAndGet(stack, ++ (meta) -> ((BookMeta) meta).addPages(coloredName), ++ (meta) -> Assertions.assertEquals(coloredName, ((BookMeta) meta).page(1)) ++ ); ++ ++ // Simple text ++ Component name = Component.text("Owen1212055"); ++ testSetAndGet(stack, ++ (meta) -> ((BookMeta) meta).addPages(name), ++ (meta) -> Assertions.assertEquals(name, ((BookMeta) meta).page(1)) ++ ); ++ ++ // Simple text + hover... should be saved ++ Component nameWithHover = Component.text("Owen1212055") ++ .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("Hover"))); ++ testSetAndGet(stack, ++ (meta) -> ((BookMeta) meta).addPages(nameWithHover), ++ (meta) -> Assertions.assertEquals(nameWithHover, ((BookMeta) meta).page(1)) ++ ); ++ } ++ ++ private void testSetAndGet(org.bukkit.inventory.ItemStack itemStack, ++ Consumer set, ++ Consumer get) { ++ ItemMeta craftMeta = CraftItemStack.getItemMeta(CraftItemStack.asNMSCopy(itemStack)); // TODO: This should be converted to use the old meta when this is added. ++ ItemMeta paperMeta = CraftItemStack.getItemMeta(CraftItemStack.asNMSCopy(itemStack)); ++ // Test craft meta ++ set.accept(craftMeta); ++ get.accept(craftMeta); ++ ++ // Test paper meta ++ set.accept(paperMeta); ++ get.accept(paperMeta); ++ } ++} +diff --git a/src/test/java/org/bukkit/PerMaterialTest.java b/src/test/java/org/bukkit/PerMaterialTest.java +index 629fccec144b5d66addc0e8258cde90e81904e1c..6961730365da9083e8963200ecc5f85dbc654f35 100644 +--- a/src/test/java/org/bukkit/PerMaterialTest.java ++++ b/src/test/java/org/bukkit/PerMaterialTest.java +@@ -101,17 +101,13 @@ public class PerMaterialTest { + + final ItemStack bukkit = new ItemStack(material); + final CraftItemStack craft = CraftItemStack.asCraftCopy(bukkit); +- if (material == Material.AIR) { +- final int MAX_AIR_STACK = 0 /* Why can't I hold all of these AIR? */; +- assertThat(material.getMaxStackSize(), is(MAX_AIR_STACK)); +- assertThat(bukkit.getMaxStackSize(), is(MAX_AIR_STACK)); +- assertThat(craft.getMaxStackSize(), is(MAX_AIR_STACK)); +- } else { ++ ++ // Paper - remove air exception + int max = CraftMagicNumbers.getItem(material).components().getOrDefault(DataComponents.MAX_STACK_SIZE, 64); + assertThat(material.getMaxStackSize(), is(max)); + assertThat(bukkit.getMaxStackSize(), is(max)); + assertThat(craft.getMaxStackSize(), is(max)); +- } ++ // Paper - remove air exception + } + + @ParameterizedTest +diff --git a/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java b/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java +index b717a5ffa567781b0687bbe238b62844214db284..2d60c06b70201e4c993498af3c8e52da94b5a63e 100644 +--- a/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java ++++ b/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java +@@ -100,6 +100,7 @@ public class RegistriesArgumentProvider implements ArgumentsProvider { + register(RegistryKey.MAP_DECORATION_TYPE, MapCursor.Type.class, Registries.MAP_DECORATION_TYPE, CraftMapCursor.CraftType.class, MapDecorationType.class); + register(RegistryKey.BANNER_PATTERN, PatternType.class, Registries.BANNER_PATTERN, CraftPatternType.class, BannerPattern.class); + register(RegistryKey.MENU, MenuType.class, Registries.MENU, CraftMenuType.class, net.minecraft.world.inventory.MenuType.class); ++ register(RegistryKey.DATA_COMPONENT_TYPE, io.papermc.paper.datacomponent.DataComponentType.class, Registries.DATA_COMPONENT_TYPE, io.papermc.paper.datacomponent.PaperComponentType.class, net.minecraft.core.component.DataComponentType.class); + } + + private static void register(RegistryKey registryKey, Class bukkit, ResourceKey registry, Class craft, Class minecraft) { // Paper diff --git a/patches/server/1036-Always-send-Banner-patterns-to-the-client.patch b/patches/server/1037-Always-send-Banner-patterns-to-the-client.patch similarity index 100% rename from patches/server/1036-Always-send-Banner-patterns-to-the-client.patch rename to patches/server/1037-Always-send-Banner-patterns-to-the-client.patch diff --git a/patches/server/1037-Rewrite-dataconverter-system.patch b/patches/server/1038-Rewrite-dataconverter-system.patch similarity index 100% rename from patches/server/1037-Rewrite-dataconverter-system.patch rename to patches/server/1038-Rewrite-dataconverter-system.patch diff --git a/patches/server/1038-Moonrise-optimisation-patches.patch b/patches/server/1039-Moonrise-optimisation-patches.patch similarity index 100% rename from patches/server/1038-Moonrise-optimisation-patches.patch rename to patches/server/1039-Moonrise-optimisation-patches.patch diff --git a/patches/server/1039-API-for-checking-sent-chunks.patch b/patches/server/1040-API-for-checking-sent-chunks.patch similarity index 95% rename from patches/server/1039-API-for-checking-sent-chunks.patch rename to patches/server/1040-API-for-checking-sent-chunks.patch index 822754301b..33cbf04ef5 100644 --- a/patches/server/1039-API-for-checking-sent-chunks.patch +++ b/patches/server/1040-API-for-checking-sent-chunks.patch @@ -5,7 +5,7 @@ Subject: [PATCH] API for checking sent chunks diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index 73ee1ba0b70859cc9c012ab32ad04e8ae2a73e30..92fb2f976ae1c36b152733c28f223337e7f8632e 100644 +index 4f1b3b38d1eec331ab67307eb0e9e62621ce3cd5..d0010dfd22463986bf3be9b3ee015ce92735753e 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -3498,6 +3498,35 @@ public class CraftPlayer extends CraftHumanEntity implements Player { diff --git a/patches/server/1040-Fix-CraftWorld-isChunkGenerated.patch b/patches/server/1041-Fix-CraftWorld-isChunkGenerated.patch similarity index 100% rename from patches/server/1040-Fix-CraftWorld-isChunkGenerated.patch rename to patches/server/1041-Fix-CraftWorld-isChunkGenerated.patch diff --git a/patches/server/1041-Add-startup-flag-to-disable-gamerule-limits.patch b/patches/server/1042-Add-startup-flag-to-disable-gamerule-limits.patch similarity index 100% rename from patches/server/1041-Add-startup-flag-to-disable-gamerule-limits.patch rename to patches/server/1042-Add-startup-flag-to-disable-gamerule-limits.patch diff --git a/patches/server/1042-Improved-Watchdog-Support.patch b/patches/server/1043-Improved-Watchdog-Support.patch similarity index 100% rename from patches/server/1042-Improved-Watchdog-Support.patch rename to patches/server/1043-Improved-Watchdog-Support.patch diff --git a/patches/server/1043-Detail-more-information-in-watchdog-dumps.patch b/patches/server/1044-Detail-more-information-in-watchdog-dumps.patch similarity index 100% rename from patches/server/1043-Detail-more-information-in-watchdog-dumps.patch rename to patches/server/1044-Detail-more-information-in-watchdog-dumps.patch diff --git a/patches/server/1044-Entity-load-save-limit-per-chunk.patch b/patches/server/1045-Entity-load-save-limit-per-chunk.patch similarity index 100% rename from patches/server/1044-Entity-load-save-limit-per-chunk.patch rename to patches/server/1045-Entity-load-save-limit-per-chunk.patch diff --git a/patches/server/1045-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch b/patches/server/1046-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch similarity index 100% rename from patches/server/1045-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch rename to patches/server/1046-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch diff --git a/patches/server/1046-Bundle-spark.patch b/patches/server/1047-Bundle-spark.patch similarity index 99% rename from patches/server/1046-Bundle-spark.patch rename to patches/server/1047-Bundle-spark.patch index 6474f92054..00be00c19e 100644 --- a/patches/server/1046-Bundle-spark.patch +++ b/patches/server/1047-Bundle-spark.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Bundle spark diff --git a/build.gradle.kts b/build.gradle.kts -index 3288d59c9635819aef0bd864c64cb80e339c050f..f5104c3232b4ad1bc486490d17a8877d603c7d8d 100644 +index 9e6c2a4630ce75e4115f76b5e7a1e0b50e8b3197..ee5f662ed0e84be997807a9faf97191ef4fc0449 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -76,6 +76,10 @@ dependencies { diff --git a/patches/server/1047-Improve-performance-of-mass-crafts.patch b/patches/server/1048-Improve-performance-of-mass-crafts.patch similarity index 100% rename from patches/server/1047-Improve-performance-of-mass-crafts.patch rename to patches/server/1048-Improve-performance-of-mass-crafts.patch diff --git a/patches/server/1048-Incremental-chunk-and-player-saving.patch b/patches/server/1049-Incremental-chunk-and-player-saving.patch similarity index 98% rename from patches/server/1048-Incremental-chunk-and-player-saving.patch rename to patches/server/1049-Incremental-chunk-and-player-saving.patch index 30f35bb821..19e7e9ed59 100644 --- a/patches/server/1048-Incremental-chunk-and-player-saving.patch +++ b/patches/server/1049-Incremental-chunk-and-player-saving.patch @@ -86,7 +86,7 @@ index 7b936a01888d71fe305863054471b6b4a3aa95b8..c09c718c1b1c9f27fdf1e4160b2df688 // Paper start - add close param this.save(progressListener, flush, savingDisabled, false); diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index 8ceeebb561046933cba0725e15732fa074226884..8c9148426f23cbbdfaf7ae66657d1a620f8bd853 100644 +index dda53c52fab7c02f39451335fe0bfe36852ac994..167ab6346edb8f29c29a7327f031c0f5df71acf1 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java @@ -221,6 +221,7 @@ import org.bukkit.inventory.MainHand; diff --git a/patches/server/1049-Optimise-general-POI-access.patch b/patches/server/1050-Optimise-general-POI-access.patch similarity index 100% rename from patches/server/1049-Optimise-general-POI-access.patch rename to patches/server/1050-Optimise-general-POI-access.patch diff --git a/patches/server/1050-Fix-entity-tracker-desync-when-new-players-are-added.patch b/patches/server/1051-Fix-entity-tracker-desync-when-new-players-are-added.patch similarity index 100% rename from patches/server/1050-Fix-entity-tracker-desync-when-new-players-are-added.patch rename to patches/server/1051-Fix-entity-tracker-desync-when-new-players-are-added.patch diff --git a/patches/server/1051-Lag-compensation-ticks.patch b/patches/server/1052-Lag-compensation-ticks.patch similarity index 98% rename from patches/server/1051-Lag-compensation-ticks.patch rename to patches/server/1052-Lag-compensation-ticks.patch index 49158b5c6c..8fc7383de7 100644 --- a/patches/server/1051-Lag-compensation-ticks.patch +++ b/patches/server/1052-Lag-compensation-ticks.patch @@ -65,7 +65,7 @@ index 504c996220b278c194c93e001a3b326d549868ec..a96f859a5d0c6ec692d4627a69f3c9ee if (this.hasDelayedDestroy) { diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index 4836b01323abb125289982ef3ceca09d6a9cfc3b..e86314de8d908a0c3e9f17d3e163c11180cf3f59 100644 +index 59c992173fda6153c58722caae061b0e6bee86a1..6a3a8f0466998409a01223bc0c16d92b96e50118 100644 --- a/src/main/java/net/minecraft/world/entity/LivingEntity.java +++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java @@ -4051,6 +4051,10 @@ public abstract class LivingEntity extends Entity implements Attackable { diff --git a/patches/server/1052-Optimise-collision-checking-in-player-move-packet-ha.patch b/patches/server/1053-Optimise-collision-checking-in-player-move-packet-ha.patch similarity index 100% rename from patches/server/1052-Optimise-collision-checking-in-player-move-packet-ha.patch rename to patches/server/1053-Optimise-collision-checking-in-player-move-packet-ha.patch diff --git a/patches/server/1053-Optional-per-player-mob-spawns.patch b/patches/server/1054-Optional-per-player-mob-spawns.patch similarity index 100% rename from patches/server/1053-Optional-per-player-mob-spawns.patch rename to patches/server/1054-Optional-per-player-mob-spawns.patch diff --git a/patches/server/1054-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch b/patches/server/1055-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch similarity index 96% rename from patches/server/1054-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch rename to patches/server/1055-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch index e2a568cb21..a4379980b1 100644 --- a/patches/server/1054-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch +++ b/patches/server/1055-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch @@ -60,7 +60,7 @@ index b1ecc218034944533967375d11297705c3fc01a3..aaaadb7be8abf867624a1ca83371595b spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true); } else { diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index 8cc02ee9b1a710e35eb65a5a095681cc7dc542bb..4e4e5b7e8c387cf13cf5bc5e39d334c3222c9103 100644 +index f28babf8c778ef570d35e1aaa88467160cf3f1b1..b525369fb6f3bb80c1553ae41b1e3bddeda29936 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java @@ -307,6 +307,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple @@ -72,7 +72,7 @@ index 8cc02ee9b1a710e35eb65a5a095681cc7dc542bb..4e4e5b7e8c387cf13cf5bc5e39d334c3 // CraftBukkit start public CraftPlayer.TransferCookieConnection transferCookieConnection; diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java -index 0beb5ab48c5405d9bc2ad8d0c430312f456d38c2..5297798c2be1ba85569c2b92ed221956bf75477a 100644 +index 12ebd7829c7f6814ccd79ae96aa9023afcc64696..c1b76a1ebc1eea7ab70cf61d8175a31794dd122a 100644 --- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java +++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java @@ -299,6 +299,11 @@ public final class NaturalSpawner { diff --git a/patches/server/1055-Avoid-issues-with-certain-tasks-not-processing-durin.patch b/patches/server/1056-Avoid-issues-with-certain-tasks-not-processing-durin.patch similarity index 94% rename from patches/server/1055-Avoid-issues-with-certain-tasks-not-processing-durin.patch rename to patches/server/1056-Avoid-issues-with-certain-tasks-not-processing-durin.patch index 180648d806..4bb32dd1da 100644 --- a/patches/server/1055-Avoid-issues-with-certain-tasks-not-processing-durin.patch +++ b/patches/server/1056-Avoid-issues-with-certain-tasks-not-processing-durin.patch @@ -11,7 +11,7 @@ sleep by default, which avoids the problem and makes it more obvious to check if enabled. We also unload chunks during sleep to prevent memory leaks caused by plugin chunk loads. diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 9dfa6bb83469620a446509656ea1ad140b2e683f..af8dbfcc9df105896c8574d2d9dd647ac30ae04d 100644 +index e636a96ea6220fda671a31d3d9cdea468a558768..64b56abf8900d0424100da460fc68ac964394793 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -1638,6 +1638,16 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop