From 76403d796bbb4e5a30717a1a4209e34e88136152 Mon Sep 17 00:00:00 2001 From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:09:44 -0500 Subject: [PATCH] Item DataComponent API (#10845) Co-authored-by: Jake Potrebic Co-authored-by: Lulu13022002 <41980282+Lulu13022002@users.noreply.github.com> Co-authored-by: Bjarne Koll --- patches/api/DataComponent-API.patch | 4186 +++++++++++++++++++++ patches/server/Adventure.patch | 4 + patches/server/DataComponent-API.patch | 4804 ++++++++++++++++++++++++ patches/server/MC-Utils.patch | 52 + 4 files changed, 9046 insertions(+) create mode 100644 patches/api/DataComponent-API.patch create mode 100644 patches/server/DataComponent-API.patch diff --git a/patches/api/DataComponent-API.patch b/patches/api/DataComponent-API.patch new file mode 100644 index 0000000000..c9f99ebcab --- /dev/null +++ b/patches/api/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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/BlockPredicate.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/BuildableDataComponent.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/DataComponentBuilder.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/DataComponentType.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/DataComponentTypes.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/BannerPatternLayers.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/BlockItemDataProperties.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/BundleContents.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ChargedProjectiles.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/Consumable.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/CustomModelData.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/DamageResistant.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/DeathProtection.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/DyedItemColor.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/Enchantable.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/Equippable.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/Fireworks.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/FoodProperties.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ItemAdventurePredicate.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ItemArmorTrim.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ItemAttributeModifiers.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ItemComponentTypesBridge.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ItemContainerContents.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ItemEnchantments.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ItemLore.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/JukeboxPlayable.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/LodestoneTracker.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/MapDecorations.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/MapId.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/MapItemColor.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/OminousBottleAmplifier.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PotDecorations.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PotionContents.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/Repairable.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ResolvableProfile.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/SeededContainerLoot.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ShownInTooltip.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/SuspiciousStewEffects.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/Tool.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/Unbreakable.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/UseCooldown.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/UseRemainder.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/WritableBookContent.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/WrittenBookContent.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/ConsumableTypesBridge.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/ConsumeEffect.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/ItemUseAnimation.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/item/MapPostProcessing.java +@@ -0,0 +0,0 @@ ++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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/io/papermc/paper/registry/RegistryKey.java ++++ b/src/main/java/io/papermc/paper/registry/RegistryKey.java +@@ -0,0 +0,0 @@ + 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; +@@ -0,0 +0,0 @@ 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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/text/Filtered.java +@@ -0,0 +0,0 @@ ++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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/org/bukkit/Material.java ++++ b/src/main/java/org/bukkit/Material.java +@@ -0,0 +0,0 @@ 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), +@@ -0,0 +0,0 @@ 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(); + } +@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/org/bukkit/Registry.java ++++ b/src/main/java/org/bukkit/Registry.java +@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/org/bukkit/block/BlockType.java ++++ b/src/main/java/org/bukkit/block/BlockType.java +@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/org/bukkit/inventory/ItemStack.java ++++ b/src/main/java/org/bukkit/inventory/ItemStack.java +@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/org/bukkit/inventory/ItemType.java ++++ b/src/main/java/org/bukkit/inventory/ItemType.java +@@ -0,0 +0,0 @@ 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/server/Adventure.patch b/patches/server/Adventure.patch index 48750ba305..d7ce821f74 100644 --- a/patches/server/Adventure.patch +++ b/patches/server/Adventure.patch @@ -1314,6 +1314,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + + // 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()); + } diff --git a/patches/server/DataComponent-API.patch b/patches/server/DataComponent-API.patch new file mode 100644 index 0000000000..8771e3df5b --- /dev/null +++ b/patches/server/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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/ComponentAdapter.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/ComponentAdapters.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/ComponentUtils.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/PaperComponentType.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/ItemComponentTypesBridgesImpl.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperBannerPatternLayers.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperBlockItemDataProperties.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperBundleContents.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperChargedProjectiles.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperConsumable.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperCustomModelData.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperDamageResistant.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperDeathProtection.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperDyedItemColor.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperEnchantable.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperEquippable.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperFireworks.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperFoodProperties.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAdventurePredicate.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemArmorTrim.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAttributeModifiers.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemContainerContents.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemEnchantments.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemLore.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemTool.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperJukeboxPlayable.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperLodestoneTracker.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapDecorations.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapId.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapItemColor.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperOminousBottleAmplifier.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperPotDecorations.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperPotionContents.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperRepairable.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperResolvableProfile.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperSeededContainerLoot.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperSuspiciousStewEffects.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperUnbreakable.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperUseCooldown.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperUseRemainder.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperWritableBookContent.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperWrittenBookContent.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/ConsumableTypesBridgeImpl.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperApplyStatusEffects.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperClearAllStatusEffects.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffectImpl.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffects.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperPlaySound.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperRemoveStatusEffects.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperTeleportRandomly.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/package-info.java +@@ -0,0 +0,0 @@ ++/** ++ * 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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/item/package-info.java +@@ -0,0 +0,0 @@ ++@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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/datacomponent/package-info.java +@@ -0,0 +0,0 @@ ++@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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/io/papermc/paper/registry/PaperRegistries.java ++++ b/src/main/java/io/papermc/paper/registry/PaperRegistries.java +@@ -0,0 +0,0 @@ + 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; +@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java +@@ -0,0 +0,0 @@ public final class CraftItemStack extends ItemStack { + this.adjustTagForItemMeta(oldType); // Paper + } + } +- this.setData(null); ++ this.setData((MaterialData) null); // Paper + } + + @Override +@@ -0,0 +0,0 @@ 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 +@@ -0,0 +0,0 @@ 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 + } + +@@ -0,0 +0,0 @@ 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 +@@ -0,0 +0,0 @@ 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) { +@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java +@@ -0,0 +0,0 @@ public class CraftItemType 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java +@@ -0,0 +0,0 @@ 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()) +@@ -0,0 +0,0 @@ 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..0000000000000000000000000000000000000000 +--- /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..0000000000000000000000000000000000000000 +--- /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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/datacomponent/DataComponentTypesTest.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/item/ItemStackDataComponentEqualsTest.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/item/ItemStackDataComponentTest.java +@@ -0,0 +0,0 @@ ++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..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/item/MetaComparisonTest.java +@@ -0,0 +0,0 @@ ++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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/test/java/org/bukkit/PerMaterialTest.java ++++ b/src/test/java/org/bukkit/PerMaterialTest.java +@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java ++++ b/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java +@@ -0,0 +0,0 @@ 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/MC-Utils.patch b/patches/server/MC-Utils.patch index 868047c55a..51f5231d59 100644 --- a/patches/server/MC-Utils.patch +++ b/patches/server/MC-Utils.patch @@ -4697,10 +4697,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 @@ -0,0 +0,0 @@ +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..00000000000000000000000000000000 +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..00000000000000000000000000000000 + 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 0000000000000000000000000000000000000000..00000000000000000000000000000000 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftLocation.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftLocation.java +@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java