aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/1032-DataComponent-API.patch
diff options
context:
space:
mode:
Diffstat (limited to 'patches/server/1032-DataComponent-API.patch')
-rw-r--r--patches/server/1032-DataComponent-API.patch4926
1 files changed, 4926 insertions, 0 deletions
diff --git a/patches/server/1032-DataComponent-API.patch b/patches/server/1032-DataComponent-API.patch
new file mode 100644
index 0000000000..2493615d74
--- /dev/null
+++ b/patches/server/1032-DataComponent-API.patch
@@ -0,0 +1,4926 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Owen1212055 <[email protected]>
+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/DataComponentAdapter.java b/src/main/java/io/papermc/paper/datacomponent/DataComponentAdapter.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..957fdf1e32d109b8131359a159ea6817885968d1
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/DataComponentAdapter.java
+@@ -0,0 +1,36 @@
++package io.papermc.paper.datacomponent;
++
++import java.util.function.Function;
++import net.minecraft.core.component.DataComponentType;
++import net.minecraft.core.registries.BuiltInRegistries;
++import net.minecraft.util.NullOps;
++import net.minecraft.util.Unit;
++import org.bukkit.craftbukkit.CraftRegistry;
++
++public record DataComponentAdapter<NMS, API>(
++ DataComponentType<NMS> type,
++ Function<API, NMS> apiToVanilla,
++ Function<NMS, API> vanillaToApi,
++ boolean codecValidation
++) {
++ static final Function<Void, Unit> 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/DataComponentAdapters.java b/src/main/java/io/papermc/paper/datacomponent/DataComponentAdapters.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7675588202b20af182cc44253f4c036d37000a8c
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/DataComponentAdapters.java
+@@ -0,0 +1,170 @@
++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 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.core.registries.Registries;
++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.Handleable;
++import org.bukkit.inventory.ItemRarity;
++
++import static io.papermc.paper.util.MCUtil.transformUnmodifiable;
++
++public final class DataComponentAdapters {
++
++ static final Function<Unit, Void> UNIT_TO_API_CONVERTER = $ -> {
++ throw new UnsupportedOperationException("Cannot convert the Unit type to an API value");
++ };
++
++ static final Map<ResourceKey<DataComponentType<?>>, DataComponentAdapter<?, ?>> 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, PaperAdventure::asAdventure, PaperAdventure::asVanilla);
++ 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 -> transformUnmodifiable(nms, PaperAdventure::asAdventureKey),
++ api -> transformUnmodifiable(api, key -> PaperAdventure.asVanilla(Registries.RECIPE, key))
++ );
++ 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<ResourceKey<DataComponentType<?>>, DataComponentType<?>> componentType : BuiltInRegistries.DATA_COMPONENT_TYPE.entrySet()) {
++ if (!ADAPTERS.containsKey(componentType.getKey())) {
++ registerUntyped((DataComponentType<Unit>) componentType.getValue());
++ }
++ }
++ }
++
++ public static void registerUntyped(final DataComponentType<Unit> type) {
++ registerInternal(type, UNIT_TO_API_CONVERTER, DataComponentAdapter.API_TO_UNIT_CONVERTER, false);
++ }
++
++ private static <COMMON> void registerIdentity(final DataComponentType<COMMON> type) {
++ registerInternal(type, Function.identity(), Function.identity(), true);
++ }
++
++ private static <NMS, API extends Handleable<NMS>> void register(final DataComponentType<NMS> type, final Function<NMS, API> vanillaToApi) {
++ registerInternal(type, vanillaToApi, Handleable::getHandle, false);
++ }
++
++ private static <NMS, API> void register(final DataComponentType<NMS> type, final Function<NMS, API> vanillaToApi, final Function<API, NMS> apiToVanilla) {
++ registerInternal(type, vanillaToApi, apiToVanilla, false);
++ }
++
++ private static <NMS, API> void registerInternal(final DataComponentType<NMS> type, final Function<NMS, API> vanillaToApi, final Function<API, NMS> apiToVanilla, final boolean codecValidation) {
++ final ResourceKey<DataComponentType<?>> key = BuiltInRegistries.DATA_COMPONENT_TYPE.getResourceKey(type).orElseThrow();
++ if (ADAPTERS.containsKey(key)) {
++ throw new IllegalStateException("Duplicate adapter registration for " + key);
++ }
++ ADAPTERS.put(key, new DataComponentAdapter<>(type, apiToVanilla, vanillaToApi, codecValidation && !type.isTransient()));
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/PaperDataComponentType.java b/src/main/java/io/papermc/paper/datacomponent/PaperDataComponentType.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..e2fcf870b2256e3df90372c3208f3ed27469b16e
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/PaperDataComponentType.java
+@@ -0,0 +1,109 @@
++package io.papermc.paper.datacomponent;
++
++import java.util.Collections;
++import java.util.HashSet;
++import java.util.Set;
++import net.minecraft.core.component.DataComponentMap;
++import net.minecraft.core.registries.BuiltInRegistries;
++import net.minecraft.core.registries.Registries;
++import org.bukkit.NamespacedKey;
++import org.bukkit.Registry;
++import org.bukkit.craftbukkit.CraftRegistry;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.jspecify.annotations.Nullable;
++
++public abstract class PaperDataComponentType<T, NMS> implements DataComponentType, Handleable<net.minecraft.core.component.DataComponentType<NMS>> {
++
++ static {
++ DataComponentAdapters.bootstrap();
++ }
++
++ public static <T> net.minecraft.core.component.DataComponentType<T> 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<DataComponentType> minecraftToBukkit(final Set<net.minecraft.core.component.DataComponentType<?>> nmsTypes) {
++ final Set<DataComponentType> types = new HashSet<>(nmsTypes.size());
++ for (final net.minecraft.core.component.DataComponentType<?> nmsType : nmsTypes) {
++ types.add(PaperDataComponentType.minecraftToBukkit(nmsType));
++ }
++ return Collections.unmodifiableSet(types);
++ }
++
++ public static <B, M> @Nullable B convertDataComponentValue(final DataComponentMap map, final PaperDataComponentType.ValuedImpl<B, M> type) {
++ final net.minecraft.core.component.DataComponentType<M> 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<NMS> type;
++ private final DataComponentAdapter<NMS, T> adapter;
++
++ public PaperDataComponentType(final NamespacedKey key, final net.minecraft.core.component.DataComponentType<NMS> type, final DataComponentAdapter<NMS, T> 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 DataComponentAdapter<NMS, T> getAdapter() {
++ return this.adapter;
++ }
++
++ @Override
++ public net.minecraft.core.component.DataComponentType<NMS> getHandle() {
++ return this.type;
++ }
++
++ @SuppressWarnings("unchecked")
++ public static <NMS> DataComponentType of(final NamespacedKey key, final net.minecraft.core.component.DataComponentType<NMS> type) {
++ final DataComponentAdapter<NMS, ?> adapter = (DataComponentAdapter<NMS, ?>) DataComponentAdapters.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<T, NMS> extends PaperDataComponentType<T, NMS> implements NonValued {
++
++ NonValuedImpl(
++ final NamespacedKey key,
++ final net.minecraft.core.component.DataComponentType<NMS> type,
++ final DataComponentAdapter<NMS, T> adapter
++ ) {
++ super(key, type, adapter);
++ }
++ }
++
++ public static final class ValuedImpl<T, NMS> extends PaperDataComponentType<T, NMS> implements Valued<T> {
++
++ ValuedImpl(
++ final NamespacedKey key,
++ final net.minecraft.core.component.DataComponentType<NMS> type,
++ final DataComponentAdapter<NMS, T> 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..15c66b0186ffede98a196f63e0e616b125bac35a
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/ItemComponentTypesBridgesImpl.java
+@@ -0,0 +1,239 @@
++package io.papermc.paper.datacomponent.item;
++
++import com.destroystokyo.paper.profile.PlayerProfile;
++import com.google.common.base.Preconditions;
++import io.papermc.paper.registry.PaperRegistries;
++import io.papermc.paper.registry.set.PaperRegistrySets;
++import io.papermc.paper.registry.set.RegistryKeySet;
++import io.papermc.paper.registry.tag.TagKey;
++import io.papermc.paper.text.Filtered;
++import net.kyori.adventure.key.Key;
++import net.kyori.adventure.util.TriState;
++import net.minecraft.core.registries.BuiltInRegistries;
++import net.minecraft.core.registries.Registries;
++import net.minecraft.world.item.component.OminousBottleAmplifier;
++import org.bukkit.JukeboxSong;
++import org.bukkit.block.BlockType;
++import org.bukkit.craftbukkit.inventory.CraftItemStack;
++import org.bukkit.damage.DamageType;
++import org.bukkit.inventory.EquipmentSlot;
++import org.bukkit.inventory.ItemStack;
++import org.bukkit.inventory.ItemType;
++import org.bukkit.inventory.meta.trim.ArmorTrim;
++import org.bukkit.map.MapCursor;
++import org.jspecify.annotations.Nullable;
++
++public final class ItemComponentTypesBridgesImpl implements ItemComponentTypesBridge {
++
++ @Override
++ public ChargedProjectiles.Builder chargedProjectiles() {
++ return new PaperChargedProjectiles.BuilderImpl();
++ }
++
++ @Override
++ public PotDecorations.Builder potDecorations() {
++ return new PaperPotDecorations.BuilderImpl();
++ }
++
++ @Override
++ public Unbreakable.Builder unbreakable() {
++ return new PaperUnbreakable.BuilderImpl();
++ }
++
++ @Override
++ public ItemLore.Builder lore() {
++ return new PaperItemLore.BuilderImpl();
++ }
++
++ @Override
++ public ItemEnchantments.Builder enchantments() {
++ return new PaperItemEnchantments.BuilderImpl();
++ }
++
++ @Override
++ public ItemAttributeModifiers.Builder modifiers() {
++ return new PaperItemAttributeModifiers.BuilderImpl();
++ }
++
++ @Override
++ public FoodProperties.Builder food() {
++ return new PaperFoodProperties.BuilderImpl();
++ }
++
++ @Override
++ public DyedItemColor.Builder dyedItemColor() {
++ return new PaperDyedItemColor.BuilderImpl();
++ }
++
++ @Override
++ public PotionContents.Builder potionContents() {
++ return new PaperPotionContents.BuilderImpl();
++ }
++
++ @Override
++ public BundleContents.Builder bundleContents() {
++ return new PaperBundleContents.BuilderImpl();
++ }
++
++ @Override
++ public SuspiciousStewEffects.Builder suspiciousStewEffects() {
++ return new PaperSuspiciousStewEffects.BuilderImpl();
++ }
++
++ @Override
++ public MapItemColor.Builder mapItemColor() {
++ return new PaperMapItemColor.BuilderImpl();
++ }
++
++ @Override
++ public MapDecorations.Builder mapDecorations() {
++ return new PaperMapDecorations.BuilderImpl();
++ }
++
++ @Override
++ public MapDecorations.DecorationEntry decorationEntry(final MapCursor.Type type, final double x, final double z, final float rotation) {
++ return PaperMapDecorations.PaperDecorationEntry.toApi(type, x, z, rotation);
++ }
++
++ @Override
++ public SeededContainerLoot.Builder seededContainerLoot(final Key lootTableKey) {
++ return new PaperSeededContainerLoot.BuilderImpl(lootTableKey);
++ }
++
++ @Override
++ public ItemContainerContents.Builder itemContainerContents() {
++ return new PaperItemContainerContents.BuilderImpl();
++ }
++
++ @Override
++ public JukeboxPlayable.Builder jukeboxPlayable(final JukeboxSong song) {
++ return new PaperJukeboxPlayable.BuilderImpl(song);
++ }
++
++ @Override
++ public Tool.Builder tool() {
++ return new PaperItemTool.BuilderImpl();
++ }
++
++ @Override
++ public Tool.Rule rule(final RegistryKeySet<BlockType> 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<String> 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.checkArgument(itemStack != null, "Item cannot be null");
++ 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<DamageType> 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<ItemType> 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.Builder customModelData() {
++ return new PaperCustomModelData.BuilderImpl();
++ }
++
++ @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..ca49c2d2e1edcf6c4f7a5ca6c9ba96920aa385f4
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperBannerPatternLayers.java
+@@ -0,0 +1,62 @@
++package io.papermc.paper.datacomponent.item;
++
++import io.papermc.paper.registry.RegistryAccess;
++import io.papermc.paper.registry.RegistryKey;
++import io.papermc.paper.util.MCUtil;
++import java.util.List;
++import java.util.Objects;
++import java.util.Optional;
++import org.bukkit.DyeColor;
++import org.bukkit.block.banner.Pattern;
++import org.bukkit.block.banner.PatternType;
++import org.bukkit.craftbukkit.CraftRegistry;
++import org.bukkit.craftbukkit.block.banner.CraftPatternType;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.jetbrains.annotations.Unmodifiable;
++
++public record PaperBannerPatternLayers(
++ net.minecraft.world.level.block.entity.BannerPatternLayers impl
++) implements BannerPatternLayers, Handleable<net.minecraft.world.level.block.entity.BannerPatternLayers> {
++
++ private static List<Pattern> convert(final net.minecraft.world.level.block.entity.BannerPatternLayers nmsPatterns) {
++ return MCUtil.transformUnmodifiable(nmsPatterns.layers(), input -> {
++ final Optional<PatternType> 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("Inlined 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<Pattern> 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<Pattern> patterns) {
++ patterns.forEach(this::add);
++ return this;
++ }
++
++ @Override
++ public BannerPatternLayers build() {
++ return new PaperBannerPatternLayers(this.builder.build());
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperBlockItemDataProperties.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperBlockItemDataProperties.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..5757e16c5948a6897bc61005ea7260940a49abfe
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperBlockItemDataProperties.java
+@@ -0,0 +1,50 @@
++package io.papermc.paper.datacomponent.item;
++
++import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
++import java.util.Map;
++import net.minecraft.world.item.component.BlockItemStateProperties;
++import net.minecraft.world.level.block.Block;
++import net.minecraft.world.level.block.state.BlockState;
++import org.bukkit.block.BlockType;
++import org.bukkit.block.data.BlockData;
++import org.bukkit.craftbukkit.block.CraftBlockType;
++import org.bukkit.craftbukkit.block.data.CraftBlockData;
++import org.bukkit.craftbukkit.util.Handleable;
++
++public record PaperBlockItemDataProperties(
++ BlockItemStateProperties impl
++) implements BlockItemDataProperties, Handleable<BlockItemStateProperties> {
++
++ @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<String, String> 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..ba95ce77dbddb90fd2616c9112fd74051dddc3ee
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperBundleContents.java
+@@ -0,0 +1,51 @@
++package io.papermc.paper.datacomponent.item;
++
++import com.google.common.base.Preconditions;
++import io.papermc.paper.util.MCUtil;
++import it.unimi.dsi.fastutil.objects.ObjectArrayList;
++import java.util.List;
++import org.bukkit.craftbukkit.inventory.CraftItemStack;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.bukkit.inventory.ItemStack;
++
++public record PaperBundleContents(
++ net.minecraft.world.item.component.BundleContents impl
++) implements BundleContents, Handleable<net.minecraft.world.item.component.BundleContents> {
++
++ @Override
++ public net.minecraft.world.item.component.BundleContents getHandle() {
++ return this.impl;
++ }
++
++ @Override
++ public List<ItemStack> contents() {
++ return MCUtil.transformUnmodifiable((List<net.minecraft.world.item.ItemStack>) this.impl.items(), CraftItemStack::asBukkitCopy);
++ }
++
++ static final class BuilderImpl implements BundleContents.Builder {
++
++ private final List<net.minecraft.world.item.ItemStack> items = new ObjectArrayList<>();
++
++ @Override
++ public BundleContents.Builder add(final ItemStack stack) {
++ Preconditions.checkArgument(stack != null, "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<ItemStack> 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..2129dd67fd02a13f6e6fbdfb07505dc64307a3f0
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperChargedProjectiles.java
+@@ -0,0 +1,51 @@
++package io.papermc.paper.datacomponent.item;
++
++import com.google.common.base.Preconditions;
++import io.papermc.paper.util.MCUtil;
++import java.util.ArrayList;
++import java.util.List;
++import org.bukkit.craftbukkit.inventory.CraftItemStack;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.bukkit.inventory.ItemStack;
++
++public record PaperChargedProjectiles(
++ net.minecraft.world.item.component.ChargedProjectiles impl
++) implements ChargedProjectiles, Handleable<net.minecraft.world.item.component.ChargedProjectiles> {
++
++ @Override
++ public net.minecraft.world.item.component.ChargedProjectiles getHandle() {
++ return this.impl;
++ }
++
++ @Override
++ public List<ItemStack> projectiles() {
++ return MCUtil.transformUnmodifiable(this.impl.getItems() /*makes copies internally*/, CraftItemStack::asCraftMirror);
++ }
++
++ static final class BuilderImpl implements ChargedProjectiles.Builder {
++
++ private final List<net.minecraft.world.item.ItemStack> items = new ArrayList<>();
++
++ @Override
++ public ChargedProjectiles.Builder add(final ItemStack stack) {
++ Preconditions.checkArgument(stack != null, "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<ItemStack> 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..0bc2bad71d6945ca24f37008effc903a84466004
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperConsumable.java
+@@ -0,0 +1,126 @@
++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.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<net.minecraft.world.item.component.Consumable> {
++
++ 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<ConsumeEffect> consumeEffects() {
++ return MCUtil.transformUnmodifiable(this.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<SoundEvent> eatSound = SoundEvents.GENERIC_EAT;
++ private boolean hasConsumeParticles = true;
++ private final List<net.minecraft.world.item.consume_effects.ConsumeEffect> 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) {
++ this.eatSound = PaperAdventure.resolveSound(sound);
++ 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<ConsumeEffect> 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..33a93c8acf79d02f8a19e66c4f52dfdd4471680e
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperCustomModelData.java
+@@ -0,0 +1,121 @@
++package io.papermc.paper.datacomponent.item;
++
++import com.google.common.base.Preconditions;
++import it.unimi.dsi.fastutil.booleans.BooleanArrayList;
++import it.unimi.dsi.fastutil.booleans.BooleanList;
++import it.unimi.dsi.fastutil.floats.FloatArrayList;
++import it.unimi.dsi.fastutil.floats.FloatList;
++import it.unimi.dsi.fastutil.ints.IntArrayList;
++import it.unimi.dsi.fastutil.ints.IntList;
++import it.unimi.dsi.fastutil.objects.ObjectArrayList;
++import java.util.Collections;
++import java.util.List;
++import io.papermc.paper.util.MCUtil;
++import org.bukkit.Color;
++import org.bukkit.craftbukkit.util.Handleable;
++
++public record PaperCustomModelData(
++ net.minecraft.world.item.component.CustomModelData impl
++) implements CustomModelData, Handleable<net.minecraft.world.item.component.CustomModelData> {
++
++ @Override
++ public net.minecraft.world.item.component.CustomModelData getHandle() {
++ return this.impl;
++ }
++
++ @Override
++ public List<Float> floats() {
++ return Collections.unmodifiableList(this.impl.floats());
++ }
++
++ @Override
++ public List<Boolean> flags() {
++ return Collections.unmodifiableList(this.impl.flags());
++ }
++
++ @Override
++ public List<String> strings() {
++ return Collections.unmodifiableList(this.impl.strings());
++ }
++
++ @Override
++ public List<Color> colors() {
++ return MCUtil.transformUnmodifiable(this.impl.colors(), Color::fromRGB);
++ }
++
++ static final class BuilderImpl implements CustomModelData.Builder {
++
++ private final FloatList floats = new FloatArrayList();
++ private final BooleanList flags = new BooleanArrayList();
++ private final List<String> strings = new ObjectArrayList<>();
++ private final IntList colors = new IntArrayList();
++
++ @Override
++ public Builder addFloat(final float f) {
++ this.floats.add(f);
++ return this;
++ }
++
++ @Override
++ public Builder addFloats(final List<Float> floats) {
++ for (Float f : floats) {
++ Preconditions.checkArgument(f != null, "Float cannot be null");
++ }
++ this.floats.addAll(floats);
++ return this;
++ }
++
++ @Override
++ public Builder addFlag(final boolean flag) {
++ this.flags.add(flag);
++ return this;
++ }
++
++ @Override
++ public Builder addFlags(final List<Boolean> flags) {
++ for (Boolean flag : flags) {
++ Preconditions.checkArgument(flag != null, "Flag cannot be null");
++ }
++ this.flags.addAll(flags);
++ return this;
++ }
++
++ @Override
++ public Builder addString(final String string) {
++ Preconditions.checkArgument(string != null, "String cannot be null");
++ this.strings.add(string);
++ return this;
++ }
++
++ @Override
++ public Builder addStrings(final List<String> strings) {
++ strings.forEach(this::addString);
++ return this;
++ }
++
++ @Override
++ public Builder addColor(final Color color) {
++ Preconditions.checkArgument(color != null, "Color cannot be null");
++ this.colors.add(color.asRGB());
++ return this;
++ }
++
++ @Override
++ public Builder addColors(final List<Color> colors) {
++ colors.forEach(this::addColor);
++ return this;
++ }
++
++ @Override
++ public CustomModelData build() {
++ return new PaperCustomModelData(
++ new net.minecraft.world.item.component.CustomModelData(
++ new FloatArrayList(this.floats),
++ new BooleanArrayList(this.flags),
++ new ObjectArrayList<>(this.strings),
++ new IntArrayList(this.colors)
++ )
++ );
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperDamageResistant.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperDamageResistant.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..adc986c8b3d65e3fb91a8951048194bbe4052b74
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperDamageResistant.java
+@@ -0,0 +1,21 @@
++package io.papermc.paper.datacomponent.item;
++
++import io.papermc.paper.registry.PaperRegistries;
++import io.papermc.paper.registry.tag.TagKey;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.bukkit.damage.DamageType;
++
++public record PaperDamageResistant(
++ net.minecraft.world.item.component.DamageResistant impl
++) implements DamageResistant, Handleable<net.minecraft.world.item.component.DamageResistant> {
++
++ @Override
++ public net.minecraft.world.item.component.DamageResistant getHandle() {
++ return this.impl;
++ }
++
++ @Override
++ public TagKey<DamageType> 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..798e45d3b3e895f8b3abb9db1c9d58348bcd22d3
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperDeathProtection.java
+@@ -0,0 +1,50 @@
++package io.papermc.paper.datacomponent.item;
++
++import io.papermc.paper.datacomponent.item.consumable.ConsumeEffect;
++import io.papermc.paper.datacomponent.item.consumable.PaperConsumableEffects;
++import io.papermc.paper.util.MCUtil;
++import java.util.ArrayList;
++import java.util.List;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.jetbrains.annotations.Unmodifiable;
++
++public record PaperDeathProtection(
++ net.minecraft.world.item.component.DeathProtection impl
++) implements DeathProtection, Handleable<net.minecraft.world.item.component.DeathProtection> {
++
++ @Override
++ public net.minecraft.world.item.component.DeathProtection getHandle() {
++ return this.impl;
++ }
++
++ @Override
++ public @Unmodifiable List<ConsumeEffect> deathEffects() {
++ return MCUtil.transformUnmodifiable(this.impl.deathEffects(), PaperConsumableEffects::fromNms);
++ }
++
++ static final class BuilderImpl implements Builder {
++
++ private final List<net.minecraft.world.item.consume_effects.ConsumeEffect> effects = new ArrayList<>();
++
++ @Override
++ public Builder addEffect(final ConsumeEffect effect) {
++ this.effects.add(PaperConsumableEffects.toNms(effect));
++ return this;
++ }
++
++ @Override
++ public Builder addEffects(final List<ConsumeEffect> effects) {
++ for (final ConsumeEffect effect : effects) {
++ this.effects.add(PaperConsumableEffects.toNms(effect));
++ }
++ return this;
++ }
++
++ @Override
++ public DeathProtection build() {
++ return new PaperDeathProtection(
++ new net.minecraft.world.item.component.DeathProtection(this.effects)
++ );
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperDyedItemColor.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperDyedItemColor.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2407d79e2e77e8be6de8e65769efc4d79e3be9db
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperDyedItemColor.java
+@@ -0,0 +1,52 @@
++package io.papermc.paper.datacomponent.item;
++
++import org.bukkit.Color;
++import org.bukkit.craftbukkit.util.Handleable;
++
++public record PaperDyedItemColor(
++ net.minecraft.world.item.component.DyedItemColor impl
++) implements DyedItemColor, Handleable<net.minecraft.world.item.component.DyedItemColor> {
++
++ @Override
++ public net.minecraft.world.item.component.DyedItemColor getHandle() {
++ return this.impl;
++ }
++
++ @Override
++ public Color color() {
++ return Color.fromRGB(this.impl.rgb() & 0x00FFFFFF); // skip alpha channel
++ }
++
++ @Override
++ public boolean showInTooltip() {
++ return this.impl.showInTooltip();
++ }
++
++ @Override
++ public DyedItemColor showInTooltip(final boolean showInTooltip) {
++ return new PaperDyedItemColor(this.impl.withTooltip(showInTooltip));
++ }
++
++ static final class BuilderImpl implements DyedItemColor.Builder {
++
++ private Color color = Color.WHITE;
++ private boolean showInToolTip = true;
++
++ @Override
++ public DyedItemColor.Builder color(final Color color) {
++ this.color = color;
++ return this;
++ }
++
++ @Override
++ public DyedItemColor.Builder showInTooltip(final boolean showInTooltip) {
++ this.showInToolTip = showInTooltip;
++ return this;
++ }
++
++ @Override
++ public DyedItemColor build() {
++ return new PaperDyedItemColor(new net.minecraft.world.item.component.DyedItemColor(this.color.asRGB(), this.showInToolTip));
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperEnchantable.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperEnchantable.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..422e1a4d606481f0dc68843fbbc8126ccfda1cc3
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperEnchantable.java
+@@ -0,0 +1,18 @@
++package io.papermc.paper.datacomponent.item;
++
++import org.bukkit.craftbukkit.util.Handleable;
++
++public record PaperEnchantable(
++ net.minecraft.world.item.enchantment.Enchantable impl
++) implements Enchantable, Handleable<net.minecraft.world.item.enchantment.Enchantable> {
++
++ @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..6d427d2c62cce557fa824dd347fd5d0c2ae7411e
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperEquippable.java
+@@ -0,0 +1,174 @@
++package io.papermc.paper.datacomponent.item;
++
++import io.papermc.paper.adventure.PaperAdventure;
++import io.papermc.paper.registry.PaperRegistries;
++import io.papermc.paper.registry.RegistryKey;
++import io.papermc.paper.registry.set.PaperRegistrySets;
++import io.papermc.paper.registry.set.RegistryKeySet;
++import java.util.Optional;
++import java.util.function.Function;
++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.ResourceKey;
++import net.minecraft.resources.ResourceLocation;
++import net.minecraft.sounds.SoundEvent;
++import net.minecraft.sounds.SoundEvents;
++import net.minecraft.util.datafix.fixes.EquippableAssetRenameFix;
++import net.minecraft.world.item.equipment.EquipmentAsset;
++import net.minecraft.world.item.equipment.EquipmentAssets;
++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<net.minecraft.world.item.equipment.Equippable> {
++
++ @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 assetId() {
++ return this.impl.assetId()
++ .map(PaperAdventure::asAdventureKey)
++ .orElse(null);
++ }
++
++ @Override
++ public @Nullable Key cameraOverlay() {
++ return this.impl.cameraOverlay()
++ .map(PaperAdventure::asAdventure)
++ .orElse(null);
++ }
++
++ @Override
++ public @Nullable RegistryKeySet<EntityType> 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())
++ .assetId(this.assetId())
++ .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<SoundEvent> equipSound = SoundEvents.ARMOR_EQUIP_GENERIC;
++ private Optional<ResourceKey<EquipmentAsset>> assetId = Optional.empty();
++ private Optional<ResourceLocation> cameraOverlay = Optional.empty();
++ private Optional<HolderSet<net.minecraft.world.entity.EntityType<?>>> 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 sound) {
++ this.equipSound = PaperAdventure.resolveSound(sound);
++ return this;
++ }
++
++ @Override
++ public Builder assetId(final @Nullable Key assetId) {
++ this.assetId = Optional.ofNullable(assetId)
++ .map(key -> PaperAdventure.asVanilla(EquipmentAssets.ROOT_ID, key));
++
++ 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<EntityType> 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.assetId,
++ 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..80189eb5054a044a76f19200eb0e5f316c30de92
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperFireworks.java
+@@ -0,0 +1,73 @@
++package io.papermc.paper.datacomponent.item;
++
++import com.google.common.base.Preconditions;
++import io.papermc.paper.util.MCUtil;
++import it.unimi.dsi.fastutil.objects.ObjectArrayList;
++import java.util.List;
++import net.minecraft.world.item.component.FireworkExplosion;
++import org.bukkit.FireworkEffect;
++import org.bukkit.craftbukkit.inventory.CraftMetaFirework;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.jetbrains.annotations.Unmodifiable;
++
++public record PaperFireworks(
++ net.minecraft.world.item.component.Fireworks impl
++) implements Fireworks, Handleable<net.minecraft.world.item.component.Fireworks> {
++
++ @Override
++ public net.minecraft.world.item.component.Fireworks getHandle() {
++ return this.impl;
++ }
++
++ @Override
++ public @Unmodifiable List<FireworkEffect> effects() {
++ return MCUtil.transformUnmodifiable(this.impl.explosions(), CraftMetaFirework::getEffect);
++ }
++
++ @Override
++ public int flightDuration() {
++ return this.impl.flightDuration();
++ }
++
++ static final class BuilderImpl implements Fireworks.Builder {
++
++ private final List<FireworkExplosion> 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<FireworkEffect> effects) {
++ Preconditions.checkArgument(
++ this.effects.size() + effects.size() <= net.minecraft.world.item.component.Fireworks.MAX_EXPLOSIONS,
++ "Cannot have more than %s effects, had %s",
++ net.minecraft.world.item.component.Fireworks.MAX_EXPLOSIONS,
++ this.effects.size() + effects.size()
++ );
++ MCUtil.addAndConvert(this.effects, effects, CraftMetaFirework::getExplosion);
++ return this;
++ }
++
++ @Override
++ public Fireworks build() {
++ return new PaperFireworks(new net.minecraft.world.item.component.Fireworks(this.duration, new ObjectArrayList<>(this.effects)));
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperFoodProperties.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperFoodProperties.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2a043bb9001048f66d3a6aa8cb896b35bd2df606
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperFoodProperties.java
+@@ -0,0 +1,72 @@
++package io.papermc.paper.datacomponent.item;
++
++import com.google.common.base.Preconditions;
++import org.bukkit.craftbukkit.util.Handleable;
++
++public record PaperFoodProperties(
++ net.minecraft.world.food.FoodProperties impl
++) implements FoodProperties, Handleable<net.minecraft.world.food.FoodProperties> {
++
++ @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..e6315cd0ebd46f874284c32da9cc03eb77f0677f
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAdventurePredicate.java
+@@ -0,0 +1,75 @@
++package io.papermc.paper.datacomponent.item;
++
++import io.papermc.paper.block.BlockPredicate;
++import io.papermc.paper.registry.RegistryKey;
++import io.papermc.paper.registry.set.PaperRegistrySets;
++import io.papermc.paper.util.MCUtil;
++import it.unimi.dsi.fastutil.objects.ObjectArrayList;
++import java.util.List;
++import java.util.Optional;
++import net.minecraft.core.registries.BuiltInRegistries;
++import net.minecraft.core.registries.Registries;
++import org.bukkit.craftbukkit.util.Handleable;
++
++public record PaperItemAdventurePredicate(
++ net.minecraft.world.item.AdventureModePredicate impl
++) implements ItemAdventurePredicate, Handleable<net.minecraft.world.item.AdventureModePredicate> {
++
++ private static List<BlockPredicate> 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<BlockPredicate> predicates() {
++ return convert(this.impl);
++ }
++
++ static final class BuilderImpl implements ItemAdventurePredicate.Builder {
++
++ private final List<net.minecraft.advancements.critereon.BlockPredicate> predicates = new ObjectArrayList<>();
++ private boolean showInTooltip = true;
++
++ @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<BlockPredicate> predicates) {
++ for (final BlockPredicate predicate : predicates) {
++ this.addPredicate(predicate);
++ }
++ return this;
++ }
++
++ @Override
++ public ItemAdventurePredicate.Builder showInTooltip(final boolean showInTooltip) {
++ this.showInTooltip = showInTooltip;
++ return this;
++ }
++
++ @Override
++ public ItemAdventurePredicate build() {
++ return new PaperItemAdventurePredicate(new net.minecraft.world.item.AdventureModePredicate(new ObjectArrayList<>(this.predicates), this.showInTooltip));
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperItemArmorTrim.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemArmorTrim.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..5d060c907f4b1bc2bae063ca1e3baf35140215b6
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemArmorTrim.java
+@@ -0,0 +1,62 @@
++package io.papermc.paper.datacomponent.item;
++
++import org.bukkit.craftbukkit.inventory.trim.CraftTrimMaterial;
++import org.bukkit.craftbukkit.inventory.trim.CraftTrimPattern;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.bukkit.inventory.meta.trim.ArmorTrim;
++
++public record PaperItemArmorTrim(
++ net.minecraft.world.item.equipment.trim.ArmorTrim impl
++) implements ItemArmorTrim, Handleable<net.minecraft.world.item.equipment.trim.ArmorTrim> {
++
++ @Override
++ public net.minecraft.world.item.equipment.trim.ArmorTrim getHandle() {
++ return this.impl;
++ }
++
++ @Override
++ public boolean showInTooltip() {
++ return this.impl.showInTooltip();
++ }
++
++ @Override
++ public ItemArmorTrim showInTooltip(final boolean showInTooltip) {
++ return new PaperItemArmorTrim(this.impl.withTooltip(showInTooltip));
++ }
++
++ @Override
++ public ArmorTrim armorTrim() {
++ return new ArmorTrim(CraftTrimMaterial.minecraftHolderToBukkit(this.impl.material()), CraftTrimPattern.minecraftHolderToBukkit(this.impl.pattern()));
++ }
++
++ static final class BuilderImpl implements ItemArmorTrim.Builder {
++
++ private ArmorTrim armorTrim;
++ private boolean showInTooltip = true;
++
++ BuilderImpl(final ArmorTrim armorTrim) {
++ this.armorTrim = armorTrim;
++ }
++
++ @Override
++ public ItemArmorTrim.Builder showInTooltip(final boolean showInTooltip) {
++ this.showInTooltip = showInTooltip;
++ return this;
++ }
++
++ @Override
++ public ItemArmorTrim.Builder armorTrim(final ArmorTrim armorTrim) {
++ this.armorTrim = armorTrim;
++ return this;
++ }
++
++ @Override
++ public ItemArmorTrim build() {
++ return new PaperItemArmorTrim(new net.minecraft.world.item.equipment.trim.ArmorTrim(
++ CraftTrimMaterial.bukkitToMinecraftHolder(this.armorTrim.getMaterial()),
++ CraftTrimPattern.bukkitToMinecraftHolder(this.armorTrim.getPattern()),
++ this.showInTooltip
++ ));
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAttributeModifiers.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAttributeModifiers.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..47ca2b8eb1c1483b6049cf18c7d8a40dd20e7cab
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAttributeModifiers.java
+@@ -0,0 +1,97 @@
++package io.papermc.paper.datacomponent.item;
++
++import com.google.common.base.Preconditions;
++import io.papermc.paper.util.MCUtil;
++import it.unimi.dsi.fastutil.objects.ObjectArrayList;
++import java.util.List;
++import org.bukkit.attribute.Attribute;
++import org.bukkit.attribute.AttributeModifier;
++import org.bukkit.craftbukkit.CraftEquipmentSlot;
++import org.bukkit.craftbukkit.attribute.CraftAttribute;
++import org.bukkit.craftbukkit.attribute.CraftAttributeInstance;
++import org.bukkit.craftbukkit.util.CraftNamespacedKey;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.bukkit.inventory.EquipmentSlotGroup;
++import org.jetbrains.annotations.Unmodifiable;
++
++public record PaperItemAttributeModifiers(
++ net.minecraft.world.item.component.ItemAttributeModifiers impl
++) implements ItemAttributeModifiers, Handleable<net.minecraft.world.item.component.ItemAttributeModifiers> {
++
++ private static List<Entry> 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<Entry> 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<net.minecraft.world.item.component.ItemAttributeModifiers.Entry> 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..2c4ecc2d5fc925f245c691facde9c96f3b5eef85
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemContainerContents.java
+@@ -0,0 +1,65 @@
++package io.papermc.paper.datacomponent.item;
++
++import com.google.common.base.Preconditions;
++import io.papermc.paper.util.MCUtil;
++import it.unimi.dsi.fastutil.objects.ObjectArrayList;
++import java.util.List;
++import org.bukkit.craftbukkit.inventory.CraftItemStack;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.bukkit.inventory.ItemStack;
++
++public record PaperItemContainerContents(
++ net.minecraft.world.item.component.ItemContainerContents impl
++) implements ItemContainerContents, Handleable<net.minecraft.world.item.component.ItemContainerContents> {
++
++ @Override
++ public net.minecraft.world.item.component.ItemContainerContents getHandle() {
++ return this.impl;
++ }
++
++ @Override
++ public List<ItemStack> contents() {
++ return MCUtil.transformUnmodifiable(this.impl.items, CraftItemStack::asBukkitCopy);
++ }
++
++ static final class BuilderImpl implements ItemContainerContents.Builder {
++
++ private final List<net.minecraft.world.item.ItemStack> items = new ObjectArrayList<>();
++
++ @Override
++ public ItemContainerContents.Builder add(final ItemStack stack) {
++ Preconditions.checkArgument(stack != null, "Item cannot be null");
++ 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<ItemStack> 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, stack -> {
++ Preconditions.checkArgument(stack != null, "Cannot pass null itemstacks!");
++ return CraftItemStack.asNMSCopy(stack);
++ });
++ return this;
++ }
++
++ @Override
++ public ItemContainerContents build() {
++ if (this.items.isEmpty()) {
++ return new PaperItemContainerContents(net.minecraft.world.item.component.ItemContainerContents.EMPTY);
++ }
++ return new PaperItemContainerContents(net.minecraft.world.item.component.ItemContainerContents.fromItems(this.items));
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperItemEnchantments.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemEnchantments.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..3cfb18f6a4868ff32e2b118c5833b1b9864e967c
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemEnchantments.java
+@@ -0,0 +1,92 @@
++package io.papermc.paper.datacomponent.item;
++
++import com.google.common.base.Preconditions;
++import it.unimi.dsi.fastutil.objects.Object2IntMap;
++import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
++import java.util.Collections;
++import java.util.HashMap;
++import java.util.Map;
++import net.minecraft.core.Holder;
++import org.bukkit.craftbukkit.enchantments.CraftEnchantment;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.bukkit.enchantments.Enchantment;
++
++public record PaperItemEnchantments(
++ net.minecraft.world.item.enchantment.ItemEnchantments impl,
++ Map<Enchantment, Integer> enchantments // API values are stored externally as the concept of a lazy key transformer map does not make much sense
++) implements ItemEnchantments, Handleable<net.minecraft.world.item.enchantment.ItemEnchantments> {
++
++ public PaperItemEnchantments(final net.minecraft.world.item.enchantment.ItemEnchantments itemEnchantments) {
++ this(itemEnchantments, convert(itemEnchantments));
++ }
++
++ private static Map<Enchantment, Integer> convert(final net.minecraft.world.item.enchantment.ItemEnchantments itemEnchantments) {
++ if (itemEnchantments.isEmpty()) {
++ return Collections.emptyMap();
++ }
++ final Map<Enchantment, Integer> map = new HashMap<>(itemEnchantments.size());
++ for (final Object2IntMap.Entry<Holder<net.minecraft.world.item.enchantment.Enchantment>> 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<Enchantment, Integer> 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<Enchantment, Integer> 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..3bb0c1aebb03c8dfd6a76ab60c26cbb104586975
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemLore.java
+@@ -0,0 +1,77 @@
++package io.papermc.paper.datacomponent.item;
++
++import com.google.common.base.Preconditions;
++import io.papermc.paper.adventure.PaperAdventure;
++import io.papermc.paper.util.MCUtil;
++import it.unimi.dsi.fastutil.objects.ObjectArrayList;
++import java.util.ArrayList;
++import java.util.List;
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.ComponentLike;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.jetbrains.annotations.Unmodifiable;
++
++public record PaperItemLore(
++ net.minecraft.world.item.component.ItemLore impl
++) implements ItemLore, Handleable<net.minecraft.world.item.component.ItemLore> {
++
++ @Override
++ public net.minecraft.world.item.component.ItemLore getHandle() {
++ return this.impl;
++ }
++
++ @Override
++ public @Unmodifiable List<Component> lines() {
++ return MCUtil.transformUnmodifiable(this.impl.lines(), PaperAdventure::asAdventure);
++ }
++
++ @Override
++ public @Unmodifiable List<Component> styledLines() {
++ return MCUtil.transformUnmodifiable(this.impl.styledLines(), PaperAdventure::asAdventure);
++ }
++
++ static final class BuilderImpl implements ItemLore.Builder {
++
++ private List<Component> 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<? extends ComponentLike> 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<? extends ComponentLike> lines) {
++ validateLineCount(this.lines.size(), lines.size());
++ this.lines.addAll(ComponentLike.asComponents(lines));
++ return this;
++ }
++
++ @Override
++ public ItemLore build() {
++ if (this.lines.isEmpty()) {
++ return new PaperItemLore(net.minecraft.world.item.component.ItemLore.EMPTY);
++ }
++
++ return new PaperItemLore(new net.minecraft.world.item.component.ItemLore(PaperAdventure.asVanilla(this.lines))); // asVanilla does a list clone
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperItemTool.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemTool.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..538a61eaa02c029b4d92f938e0ffde8aa6cf027c
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperItemTool.java
+@@ -0,0 +1,100 @@
++package io.papermc.paper.datacomponent.item;
++
++import com.google.common.base.Preconditions;
++import io.papermc.paper.registry.RegistryKey;
++import io.papermc.paper.registry.set.PaperRegistrySets;
++import io.papermc.paper.registry.set.RegistryKeySet;
++import io.papermc.paper.util.MCUtil;
++import it.unimi.dsi.fastutil.objects.ObjectArrayList;
++import java.util.Collection;
++import java.util.List;
++import java.util.Optional;
++import net.kyori.adventure.util.TriState;
++import net.minecraft.core.registries.BuiltInRegistries;
++import net.minecraft.core.registries.Registries;
++import org.bukkit.block.BlockType;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.jetbrains.annotations.Unmodifiable;
++
++public record PaperItemTool(
++ net.minecraft.world.item.component.Tool impl
++) implements Tool, Handleable<net.minecraft.world.item.component.Tool> {
++
++ private static List<Tool.Rule> convert(final List<net.minecraft.world.item.component.Tool.Rule> 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<Rule> 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<BlockType> blocks, @Nullable Float speed, TriState correctForDrops) implements Rule {
++
++ public static PaperRule fromUnsafe(final RegistryKeySet<BlockType> 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<net.minecraft.world.item.component.Tool.Rule> 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<Rule> 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..c43ccf7ccc6157389fce9f9746d5297f0eab1b6e
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperJukeboxPlayable.java
+@@ -0,0 +1,62 @@
++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.CraftRegistry;
++import org.bukkit.craftbukkit.util.Handleable;
++
++public record PaperJukeboxPlayable(
++ net.minecraft.world.item.JukeboxPlayable impl
++) implements JukeboxPlayable, Handleable<net.minecraft.world.item.JukeboxPlayable> {
++
++ @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()
++ .unwrap(CraftRegistry.getMinecraftRegistry())
++ .map(CraftJukeboxSong::minecraftHolderToBukkit)
++ .orElseThrow();
++ }
++
++ static final class BuilderImpl implements JukeboxPlayable.Builder {
++
++ private JukeboxSong song;
++ private boolean showInTooltip = true;
++
++ BuilderImpl(final JukeboxSong song) {
++ this.song = song;
++ }
++
++ @Override
++ public JukeboxPlayable.Builder showInTooltip(final boolean showInTooltip) {
++ this.showInTooltip = showInTooltip;
++ return this;
++ }
++
++ @Override
++ public JukeboxPlayable.Builder jukeboxSong(final JukeboxSong song) {
++ this.song = song;
++ return this;
++ }
++
++ @Override
++ public JukeboxPlayable build() {
++ return new PaperJukeboxPlayable(new net.minecraft.world.item.JukeboxPlayable(new EitherHolder<>(CraftJukeboxSong.bukkitToMinecraftHolder(this.song)), this.showInTooltip));
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperLodestoneTracker.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperLodestoneTracker.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..5b97249f6ae90bc1a10c2089e39f064068d7cd2c
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperLodestoneTracker.java
+@@ -0,0 +1,53 @@
++package io.papermc.paper.datacomponent.item;
++
++import java.util.Optional;
++import org.bukkit.Location;
++import org.bukkit.craftbukkit.util.CraftLocation;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.jspecify.annotations.Nullable;
++
++public record PaperLodestoneTracker(
++ net.minecraft.world.item.component.LodestoneTracker impl
++) implements LodestoneTracker, Handleable<net.minecraft.world.item.component.LodestoneTracker> {
++
++ @Override
++ public net.minecraft.world.item.component.LodestoneTracker getHandle() {
++ return this.impl;
++ }
++
++ @Override
++ public @Nullable Location location() {
++ return this.impl.target().map(CraftLocation::fromGlobalPos).orElse(null);
++ }
++
++ @Override
++ public boolean tracked() {
++ return this.impl.tracked();
++ }
++
++ static final class BuilderImpl implements LodestoneTracker.Builder {
++
++ private @Nullable Location location;
++ private boolean tracked = true;
++
++ @Override
++ public LodestoneTracker.Builder location(final @Nullable Location location) {
++ this.location = location;
++ return this;
++ }
++
++ @Override
++ public LodestoneTracker.Builder tracked(final boolean tracked) {
++ this.tracked = tracked;
++ return this;
++ }
++
++ @Override
++ public LodestoneTracker build() {
++ return new PaperLodestoneTracker(new net.minecraft.world.item.component.LodestoneTracker(
++ Optional.ofNullable(this.location).map(CraftLocation::toGlobalPos),
++ this.tracked
++ ));
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperMapDecorations.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapDecorations.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..322a1285b0c5127abb67ccab478f1b16b44d0be4
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapDecorations.java
+@@ -0,0 +1,97 @@
++package io.papermc.paper.datacomponent.item;
++
++import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
++import java.util.Collections;
++import java.util.Map;
++import java.util.Set;
++import org.bukkit.craftbukkit.map.CraftMapCursor;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.bukkit.map.MapCursor;
++import org.jspecify.annotations.Nullable;
++
++public record PaperMapDecorations(
++ net.minecraft.world.item.component.MapDecorations impl
++) implements MapDecorations, Handleable<net.minecraft.world.item.component.MapDecorations> {
++
++ @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<String, DecorationEntry> decorations() {
++ if (this.impl.decorations().isEmpty()) {
++ return Collections.emptyMap();
++ }
++
++ final Set<Map.Entry<String, net.minecraft.world.item.component.MapDecorations.Entry>> entries = this.impl.decorations().entrySet();
++ final Map<String, DecorationEntry> decorations = new Object2ObjectOpenHashMap<>(entries.size());
++ for (final Map.Entry<String, net.minecraft.world.item.component.MapDecorations.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<String, net.minecraft.world.item.component.MapDecorations.Entry> 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<String, DecorationEntry> entries) {
++ entries.forEach(this::put);
++ return this;
++ }
++
++ @Override
++ public MapDecorations build() {
++ if (this.entries.isEmpty()) {
++ return new PaperMapDecorations(net.minecraft.world.item.component.MapDecorations.EMPTY);
++ }
++ return new PaperMapDecorations(new net.minecraft.world.item.component.MapDecorations(new Object2ObjectOpenHashMap<>(this.entries)));
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperMapId.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapId.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a2b4cc372bb154bbc741ad1bf47cba210f292c5c
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapId.java
+@@ -0,0 +1,19 @@
++package io.papermc.paper.datacomponent.item;
++
++import org.bukkit.craftbukkit.util.Handleable;
++
++public record PaperMapId(
++ net.minecraft.world.level.saveddata.maps.MapId impl
++) implements MapId, Handleable<net.minecraft.world.level.saveddata.maps.MapId> {
++
++ @Override
++ public net.minecraft.world.level.saveddata.maps.MapId getHandle() {
++ return this.impl;
++ }
++
++ @Override
++ public int id() {
++ return this.impl.id();
++ }
++
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperMapItemColor.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapItemColor.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9b6fdfc9c1248bac426ce24d7b66610a6eff3b8f
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperMapItemColor.java
+@@ -0,0 +1,35 @@
++package io.papermc.paper.datacomponent.item;
++
++import org.bukkit.Color;
++import org.bukkit.craftbukkit.util.Handleable;
++
++public record PaperMapItemColor(
++ net.minecraft.world.item.component.MapItemColor impl
++) implements MapItemColor, Handleable<net.minecraft.world.item.component.MapItemColor> {
++
++ @Override
++ public net.minecraft.world.item.component.MapItemColor getHandle() {
++ return this.impl;
++ }
++
++ @Override
++ public Color color() {
++ return Color.fromRGB(this.impl.rgb() & 0x00FFFFFF); // skip alpha channel
++ }
++
++ static final class BuilderImpl implements Builder {
++
++ private Color color = Color.fromRGB(net.minecraft.world.item.component.MapItemColor.DEFAULT.rgb());
++
++ @Override
++ public Builder color(final Color color) {
++ this.color = color;
++ return this;
++ }
++
++ @Override
++ public MapItemColor build() {
++ return new PaperMapItemColor(new net.minecraft.world.item.component.MapItemColor(this.color.asRGB()));
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperOminousBottleAmplifier.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperOminousBottleAmplifier.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a7ed2aa21d0384384a4c5830ead544cb064b15b6
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperOminousBottleAmplifier.java
+@@ -0,0 +1,18 @@
++package io.papermc.paper.datacomponent.item;
++
++import org.bukkit.craftbukkit.util.Handleable;
++
++public record PaperOminousBottleAmplifier(
++ net.minecraft.world.item.component.OminousBottleAmplifier impl
++) implements OminousBottleAmplifier, Handleable<net.minecraft.world.item.component.OminousBottleAmplifier> {
++
++ @Override
++ public net.minecraft.world.item.component.OminousBottleAmplifier getHandle() {
++ return this.impl;
++ }
++
++ @Override
++ public int amplifier() {
++ return this.impl.value();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperPotDecorations.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperPotDecorations.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..bde757b51d0ae6a36870c789d416ec0e05c4cadf
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperPotDecorations.java
+@@ -0,0 +1,83 @@
++package io.papermc.paper.datacomponent.item;
++
++import java.util.Optional;
++import org.bukkit.craftbukkit.inventory.CraftItemType;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.bukkit.inventory.ItemType;
++import org.jspecify.annotations.Nullable;
++
++public record PaperPotDecorations(
++ net.minecraft.world.level.block.entity.PotDecorations impl
++) implements PotDecorations, Handleable<net.minecraft.world.level.block.entity.PotDecorations> {
++
++ @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..4712f8bbaa9f00ede895651472d7975ffa30c88d
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperPotionContents.java
+@@ -0,0 +1,103 @@
++package io.papermc.paper.datacomponent.item;
++
++import com.google.common.base.Preconditions;
++import io.papermc.paper.util.MCUtil;
++import it.unimi.dsi.fastutil.objects.ObjectArrayList;
++import java.util.List;
++import java.util.Optional;
++import net.minecraft.world.effect.MobEffectInstance;
++import org.bukkit.Color;
++import org.bukkit.craftbukkit.potion.CraftPotionType;
++import org.bukkit.craftbukkit.potion.CraftPotionUtil;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.bukkit.potion.PotionEffect;
++import org.bukkit.potion.PotionType;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.jetbrains.annotations.Unmodifiable;
++
++public record PaperPotionContents(
++ net.minecraft.world.item.alchemy.PotionContents impl
++) implements PotionContents, Handleable<net.minecraft.world.item.alchemy.PotionContents> {
++
++ @Override
++ public net.minecraft.world.item.alchemy.PotionContents getHandle() {
++ return this.impl;
++ }
++
++ @Override
++ public @Unmodifiable List<PotionEffect> customEffects() {
++ return MCUtil.transformUnmodifiable(this.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<MobEffectInstance> 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<PotionEffect> effects) {
++ effects.forEach(this::addCustomEffect);
++ return this;
++ }
++
++ @Override
++ public PotionContents build() {
++ if (this.type == null && this.color == null && this.customEffects.isEmpty() && this.customName == null) {
++ return new PaperPotionContents(net.minecraft.world.item.alchemy.PotionContents.EMPTY);
++ }
++
++ return new PaperPotionContents(new net.minecraft.world.item.alchemy.PotionContents(
++ Optional.ofNullable(this.type).map(CraftPotionType::bukkitToMinecraftHolder),
++ Optional.ofNullable(this.color).map(Color::asARGB),
++ new ObjectArrayList<>(this.customEffects),
++ Optional.ofNullable(this.customName)
++ ));
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperRepairable.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperRepairable.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..96345e051c4aa77820e857a02768b684d52d7096
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperRepairable.java
+@@ -0,0 +1,22 @@
++package io.papermc.paper.datacomponent.item;
++
++import io.papermc.paper.registry.RegistryKey;
++import io.papermc.paper.registry.set.PaperRegistrySets;
++import io.papermc.paper.registry.set.RegistryKeySet;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.bukkit.inventory.ItemType;
++
++public record PaperRepairable(
++ net.minecraft.world.item.enchantment.Repairable impl
++) implements Repairable, Handleable<net.minecraft.world.item.enchantment.Repairable> {
++
++ @Override
++ public net.minecraft.world.item.enchantment.Repairable getHandle() {
++ return this.impl;
++ }
++
++ @Override
++ public RegistryKeySet<ItemType> 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..7583a7efb4bfdb0157ee27a1b7cfb661eeccb02a
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperResolvableProfile.java
+@@ -0,0 +1,105 @@
++package io.papermc.paper.datacomponent.item;
++
++import com.destroystokyo.paper.profile.CraftPlayerProfile;
++import com.destroystokyo.paper.profile.PlayerProfile;
++import com.destroystokyo.paper.profile.ProfileProperty;
++import com.google.common.base.Preconditions;
++import com.mojang.authlib.properties.Property;
++import com.mojang.authlib.properties.PropertyMap;
++import io.papermc.paper.util.MCUtil;
++import java.util.Collection;
++import java.util.Optional;
++import java.util.UUID;
++import java.util.concurrent.CompletableFuture;
++import net.minecraft.util.StringUtil;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.jetbrains.annotations.Unmodifiable;
++
++public record PaperResolvableProfile(
++ net.minecraft.world.item.component.ResolvableProfile impl
++) implements ResolvableProfile, Handleable<net.minecraft.world.item.component.ResolvableProfile> {
++
++ 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<ProfileProperty> properties() {
++ return MCUtil.transformUnmodifiable(this.impl.properties().values(), input -> new ProfileProperty(input.name(), input.value(), input.signature()));
++ }
++
++ @Override
++ public CompletableFuture<PlayerProfile> 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<ProfileProperty> properties) {
++ properties.forEach(this::addProperty);
++ return this;
++ }
++
++ @Override
++ public ResolvableProfile build() {
++ final PropertyMap shallowCopy = new PropertyMap();
++ shallowCopy.putAll(this.propertyMap);
++
++ return new PaperResolvableProfile(new net.minecraft.world.item.component.ResolvableProfile(
++ Optional.ofNullable(this.name),
++ Optional.ofNullable(this.uuid),
++ shallowCopy
++ ));
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperSeededContainerLoot.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperSeededContainerLoot.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..1ee469b3b690a877e066dbca79706678cd915fa8
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperSeededContainerLoot.java
+@@ -0,0 +1,59 @@
++package io.papermc.paper.datacomponent.item;
++
++import io.papermc.paper.adventure.PaperAdventure;
++import net.kyori.adventure.key.Key;
++import net.minecraft.core.registries.Registries;
++import net.minecraft.resources.ResourceKey;
++import net.minecraft.world.level.storage.loot.LootTable;
++import org.bukkit.craftbukkit.util.CraftNamespacedKey;
++import org.bukkit.craftbukkit.util.Handleable;
++
++public record PaperSeededContainerLoot(
++ net.minecraft.world.item.component.SeededContainerLoot impl
++) implements SeededContainerLoot, Handleable<net.minecraft.world.item.component.SeededContainerLoot> {
++
++ @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..41df23c7e7e713e88eef757fda347381e151b0fc
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperSuspiciousStewEffects.java
+@@ -0,0 +1,58 @@
++package io.papermc.paper.datacomponent.item;
++
++import io.papermc.paper.potion.SuspiciousEffectEntry;
++import io.papermc.paper.util.MCUtil;
++import it.unimi.dsi.fastutil.objects.ObjectArrayList;
++import java.util.Collection;
++import java.util.List;
++import org.bukkit.craftbukkit.potion.CraftPotionEffectType;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.jetbrains.annotations.Unmodifiable;
++
++import static io.papermc.paper.potion.SuspiciousEffectEntry.create;
++
++public record PaperSuspiciousStewEffects(
++ net.minecraft.world.item.component.SuspiciousStewEffects impl
++) implements SuspiciousStewEffects, Handleable<net.minecraft.world.item.component.SuspiciousStewEffects> {
++
++ @Override
++ public net.minecraft.world.item.component.SuspiciousStewEffects getHandle() {
++ return this.impl;
++ }
++
++ @Override
++ public @Unmodifiable List<SuspiciousEffectEntry> effects() {
++ return MCUtil.transformUnmodifiable(this.impl.effects(), entry -> create(CraftPotionEffectType.minecraftHolderToBukkit(entry.effect()), entry.duration()));
++ }
++
++ static final class BuilderImpl implements Builder {
++
++ private final List<net.minecraft.world.item.component.SuspiciousStewEffects.Entry> 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<SuspiciousEffectEntry> entries) {
++ entries.forEach(this::add);
++ return this;
++ }
++
++ @Override
++ public SuspiciousStewEffects build() {
++ if (this.effects.isEmpty()) {
++ return new PaperSuspiciousStewEffects(net.minecraft.world.item.component.SuspiciousStewEffects.EMPTY);
++ }
++
++ return new PaperSuspiciousStewEffects(
++ new net.minecraft.world.item.component.SuspiciousStewEffects(new ObjectArrayList<>(this.effects))
++ );
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperUnbreakable.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperUnbreakable.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..edeb3308af4c359d1930fdbc5417727451b6f0eb
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperUnbreakable.java
+@@ -0,0 +1,39 @@
++package io.papermc.paper.datacomponent.item;
++
++import org.bukkit.craftbukkit.util.Handleable;
++
++public record PaperUnbreakable(
++ net.minecraft.world.item.component.Unbreakable impl
++) implements Unbreakable, Handleable<net.minecraft.world.item.component.Unbreakable> {
++
++ @Override
++ public boolean showInTooltip() {
++ return this.impl.showInTooltip();
++ }
++
++ @Override
++ public Unbreakable showInTooltip(final boolean showInTooltip) {
++ return new PaperUnbreakable(this.impl.withTooltip(showInTooltip));
++ }
++
++ @Override
++ public net.minecraft.world.item.component.Unbreakable getHandle() {
++ return this.impl;
++ }
++
++ static final class BuilderImpl implements Unbreakable.Builder {
++
++ private boolean showInTooltip = true;
++
++ @Override
++ public Unbreakable.Builder showInTooltip(final boolean showInTooltip) {
++ this.showInTooltip = showInTooltip;
++ return this;
++ }
++
++ @Override
++ public Unbreakable build() {
++ return new PaperUnbreakable(new net.minecraft.world.item.component.Unbreakable(this.showInTooltip));
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperUseCooldown.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperUseCooldown.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..1aeab920faaf5653ddb8e77372060fb8d3226641
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperUseCooldown.java
+@@ -0,0 +1,56 @@
++package io.papermc.paper.datacomponent.item;
++
++import io.papermc.paper.adventure.PaperAdventure;
++import java.util.Optional;
++import net.kyori.adventure.key.Key;
++import net.minecraft.resources.ResourceLocation;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.jspecify.annotations.Nullable;
++
++public record PaperUseCooldown(
++ net.minecraft.world.item.component.UseCooldown impl
++) implements UseCooldown, Handleable<net.minecraft.world.item.component.UseCooldown> {
++
++ @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<ResourceLocation> cooldownGroup = Optional.empty();
++
++ BuilderImpl(final float seconds) {
++ this.seconds = seconds;
++ }
++
++ @Override
++ public Builder cooldownGroup(@Nullable final Key key) {
++ this.cooldownGroup = Optional.ofNullable(key)
++ .map(PaperAdventure::asVanilla);
++
++ return this;
++ }
++
++ @Override
++ public UseCooldown build() {
++ return new PaperUseCooldown(
++ new net.minecraft.world.item.component.UseCooldown(this.seconds, this.cooldownGroup)
++ );
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/PaperUseRemainder.java b/src/main/java/io/papermc/paper/datacomponent/item/PaperUseRemainder.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c2c04506940704c2ec9a5e6bb469c4771e2d49c2
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperUseRemainder.java
+@@ -0,0 +1,20 @@
++package io.papermc.paper.datacomponent.item;
++
++import org.bukkit.craftbukkit.inventory.CraftItemStack;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.bukkit.inventory.ItemStack;
++
++public record PaperUseRemainder(
++ net.minecraft.world.item.component.UseRemainder impl
++) implements UseRemainder, Handleable<net.minecraft.world.item.component.UseRemainder> {
++
++ @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..559343a33bada475cc5bbcd431cd88b537c8cef7
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperWritableBookContent.java
+@@ -0,0 +1,103 @@
++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;
++
++public record PaperWritableBookContent(
++ net.minecraft.world.item.component.WritableBookContent impl
++) implements WritableBookContent, Handleable<net.minecraft.world.item.component.WritableBookContent> {
++
++ @Override
++ public net.minecraft.world.item.component.WritableBookContent getHandle() {
++ return this.impl;
++ }
++
++ @Override
++ public @Unmodifiable List<Filtered<String>> pages() {
++ return MCUtil.transformUnmodifiable(this.impl.pages(), input -> Filtered.of(input.raw(), input.filtered().orElse(null)));
++ }
++
++ static final class BuilderImpl implements WritableBookContent.Builder {
++
++ private final List<Filterable<String>> 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<String> 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<String> 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<Filtered<String>> pages) {
++ validatePageCount(this.pages.size(), pages.size());
++ for (final Filtered<String> 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..037a6695bdb8ee6e3c119fa79000c4ea28e1bef8
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/PaperWrittenBookContent.java
+@@ -0,0 +1,183 @@
++package io.papermc.paper.datacomponent.item;
++
++import com.google.common.base.Preconditions;
++import io.papermc.paper.adventure.PaperAdventure;
++import io.papermc.paper.text.Filtered;
++import io.papermc.paper.util.MCUtil;
++import it.unimi.dsi.fastutil.objects.ObjectArrayList;
++import java.util.List;
++import java.util.Optional;
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.ComponentLike;
++import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
++import net.minecraft.server.network.Filterable;
++import net.minecraft.util.GsonHelper;
++import org.bukkit.craftbukkit.util.Handleable;
++import org.jetbrains.annotations.Unmodifiable;
++
++import static io.papermc.paper.adventure.PaperAdventure.asAdventure;
++import static io.papermc.paper.adventure.PaperAdventure.asVanilla;
++
++public record PaperWrittenBookContent(
++ net.minecraft.world.item.component.WrittenBookContent impl
++) implements WrittenBookContent, Handleable<net.minecraft.world.item.component.WrittenBookContent> {
++
++ @Override
++ public net.minecraft.world.item.component.WrittenBookContent getHandle() {
++ return this.impl;
++ }
++
++ @Override
++ public Filtered<String> 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<Filtered<Component>> pages() {
++ return MCUtil.transformUnmodifiable(
++ this.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<Filterable<net.minecraft.network.chat.Component>> pages = new ObjectArrayList<>();
++ private Filterable<String> title;
++ private String author;
++ private int generation = 0;
++ private boolean resolved = false;
++
++ BuilderImpl(final Filtered<String> 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<String> 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<? extends ComponentLike> 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<? extends ComponentLike> 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<Filtered<? extends ComponentLike>> 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..eab1883d691e0d0034b7959c8130a6240c3f529c
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/ConsumableTypesBridgeImpl.java
+@@ -0,0 +1,64 @@
++package io.papermc.paper.datacomponent.item.consumable;
++
++import com.google.common.base.Preconditions;
++import com.google.common.collect.Lists;
++import io.papermc.paper.adventure.PaperAdventure;
++import io.papermc.paper.registry.set.PaperRegistrySets;
++import io.papermc.paper.registry.set.RegistryKeySet;
++import java.util.ArrayList;
++import java.util.List;
++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;
++
++@NullMarked
++public class ConsumableTypesBridgeImpl implements ConsumableTypesBridge {
++
++ @Override
++ public ConsumeEffect.ApplyStatusEffects applyStatusEffects(final List<PotionEffect> 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<PotionEffectType> effectTypes) {
++ return new PaperRemoveStatusEffects(
++ new net.minecraft.world.item.consume_effects.RemoveStatusEffectsConsumeEffect(
++ PaperRegistrySets.convertToNms(Registries.MOB_EFFECT, BuiltInRegistries.BUILT_IN_CONVERSIONS.lookup(), effectTypes)
++ )
++ );
++ }
++
++ @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(PaperAdventure.resolveSound(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..0d2a4ba560f5a34139517ac2e17667c94a3c56f6
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperApplyStatusEffects.java
+@@ -0,0 +1,28 @@
++package io.papermc.paper.datacomponent.item.consumable;
++
++import java.util.List;
++import net.minecraft.world.item.consume_effects.ApplyStatusEffectsConsumeEffect;
++import org.bukkit.craftbukkit.potion.CraftPotionUtil;
++import org.bukkit.potion.PotionEffect;
++
++import static io.papermc.paper.util.MCUtil.transformUnmodifiable;
++
++public record PaperApplyStatusEffects(
++ ApplyStatusEffectsConsumeEffect impl
++) implements ConsumeEffect.ApplyStatusEffects, PaperConsumableEffectImpl<ApplyStatusEffectsConsumeEffect> {
++
++ @Override
++ public List<PotionEffect> effects() {
++ return transformUnmodifiable(this.impl().effects(), CraftPotionUtil::toBukkit);
++ }
++
++ @Override
++ public float probability() {
++ return this.impl.probability();
++ }
++
++ @Override
++ public ApplyStatusEffectsConsumeEffect getHandle() {
++ return this.impl;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperClearAllStatusEffects.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperClearAllStatusEffects.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2afcbbbeb486783737fd606113b6f938d0a18cb5
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperClearAllStatusEffects.java
+@@ -0,0 +1,11 @@
++package io.papermc.paper.datacomponent.item.consumable;
++
++public record PaperClearAllStatusEffects(
++ net.minecraft.world.item.consume_effects.ClearAllStatusEffectsConsumeEffect impl
++) implements ConsumeEffect.ClearAllStatusEffects, PaperConsumableEffectImpl<net.minecraft.world.item.consume_effects.ClearAllStatusEffectsConsumeEffect> {
++
++ @Override
++ public net.minecraft.world.item.consume_effects.ClearAllStatusEffectsConsumeEffect getHandle() {
++ return this.impl;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffectImpl.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffectImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..05ede1d3f5b0b5ea3a5004cb4a7a153ed7714a55
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffectImpl.java
+@@ -0,0 +1,7 @@
++package io.papermc.paper.datacomponent.item.consumable;
++
++import net.minecraft.world.item.consume_effects.ConsumeEffect;
++import org.bukkit.craftbukkit.util.Handleable;
++
++public interface PaperConsumableEffectImpl<T extends ConsumeEffect> extends Handleable<T> {
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffects.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffects.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ff07939ef0730a11c712c09c360da8a21a777618
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffects.java
+@@ -0,0 +1,32 @@
++package io.papermc.paper.datacomponent.item.consumable;
++
++import net.minecraft.world.item.consume_effects.ApplyStatusEffectsConsumeEffect;
++import net.minecraft.world.item.consume_effects.ClearAllStatusEffectsConsumeEffect;
++import net.minecraft.world.item.consume_effects.PlaySoundConsumeEffect;
++import net.minecraft.world.item.consume_effects.RemoveStatusEffectsConsumeEffect;
++import net.minecraft.world.item.consume_effects.TeleportRandomlyConsumeEffect;
++
++public final class PaperConsumableEffects {
++
++ private PaperConsumableEffects() {
++ }
++
++ public static ConsumeEffect fromNms(net.minecraft.world.item.consume_effects.ConsumeEffect consumable) {
++ return switch (consumable) {
++ case ApplyStatusEffectsConsumeEffect effect -> new PaperApplyStatusEffects(effect);
++ case ClearAllStatusEffectsConsumeEffect effect -> new PaperClearAllStatusEffects(effect);
++ case PlaySoundConsumeEffect effect -> new PaperPlaySound(effect);
++ case RemoveStatusEffectsConsumeEffect effect -> new PaperRemoveStatusEffects(effect);
++ case TeleportRandomlyConsumeEffect effect -> new PaperTeleportRandomly(effect);
++ default -> throw new UnsupportedOperationException("Don't know how to convert " + consumable.getClass());
++ };
++ }
++
++ public static net.minecraft.world.item.consume_effects.ConsumeEffect toNms(ConsumeEffect effect) {
++ if (effect instanceof PaperConsumableEffectImpl<?> consumableEffect) {
++ return consumableEffect.getHandle();
++ } else {
++ throw new UnsupportedOperationException("Must implement handleable!");
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperPlaySound.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperPlaySound.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..26a8ee292b45e57462e6e6629b328fbf9d6b47e7
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperPlaySound.java
+@@ -0,0 +1,20 @@
++package io.papermc.paper.datacomponent.item.consumable;
++
++import io.papermc.paper.adventure.PaperAdventure;
++import net.kyori.adventure.key.Key;
++import net.minecraft.world.item.consume_effects.PlaySoundConsumeEffect;
++
++public record PaperPlaySound(
++ PlaySoundConsumeEffect impl
++) implements ConsumeEffect.PlaySound, PaperConsumableEffectImpl<PlaySoundConsumeEffect> {
++
++ @Override
++ public Key sound() {
++ return PaperAdventure.asAdventure(this.impl.sound().value().location());
++ }
++
++ @Override
++ public PlaySoundConsumeEffect getHandle() {
++ return this.impl;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperRemoveStatusEffects.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperRemoveStatusEffects.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..20e09c6ebab91b1ec103aa149d0f57a2a5502644
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperRemoveStatusEffects.java
+@@ -0,0 +1,21 @@
++package io.papermc.paper.datacomponent.item.consumable;
++
++import io.papermc.paper.registry.RegistryKey;
++import io.papermc.paper.registry.set.PaperRegistrySets;
++import io.papermc.paper.registry.set.RegistryKeySet;
++import org.bukkit.potion.PotionEffectType;
++
++public record PaperRemoveStatusEffects(
++ net.minecraft.world.item.consume_effects.RemoveStatusEffectsConsumeEffect impl
++) implements ConsumeEffect.RemoveStatusEffects, PaperConsumableEffectImpl<net.minecraft.world.item.consume_effects.RemoveStatusEffectsConsumeEffect> {
++
++ @Override
++ public RegistryKeySet<PotionEffectType> removeEffects() {
++ return PaperRegistrySets.convertToApi(RegistryKey.MOB_EFFECT, this.impl.effects());
++ }
++
++ @Override
++ public net.minecraft.world.item.consume_effects.RemoveStatusEffectsConsumeEffect getHandle() {
++ return this.impl;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperTeleportRandomly.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperTeleportRandomly.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c21889e9984f7c36d9f19771c2e23b6efba5197d
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperTeleportRandomly.java
+@@ -0,0 +1,15 @@
++package io.papermc.paper.datacomponent.item.consumable;
++
++public record PaperTeleportRandomly(
++ net.minecraft.world.item.consume_effects.TeleportRandomlyConsumeEffect impl
++) implements ConsumeEffect.TeleportRandomly, PaperConsumableEffectImpl<net.minecraft.world.item.consume_effects.TeleportRandomlyConsumeEffect> {
++ @Override
++ public float diameter() {
++ return this.impl.diameter();
++ }
++
++ @Override
++ public net.minecraft.world.item.consume_effects.TeleportRandomlyConsumeEffect getHandle() {
++ return this.impl;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/consumable/package-info.java b/src/main/java/io/papermc/paper/datacomponent/item/consumable/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..af6720a49a9d336a345e2bc91d6714f6b2c39886
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/consumable/package-info.java
+@@ -0,0 +1,7 @@
++/**
++ * Relating to consumable effects for components.
++ */
++@NullMarked
++package io.papermc.paper.datacomponent.item.consumable;
++
++import org.jspecify.annotations.NullMarked;
+diff --git a/src/main/java/io/papermc/paper/datacomponent/item/package-info.java b/src/main/java/io/papermc/paper/datacomponent/item/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..02a69025662d6a887f5449fd5eaf7d1083973bf3
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/item/package-info.java
+@@ -0,0 +1,4 @@
++@NullMarked
++package io.papermc.paper.datacomponent.item;
++
++import org.jspecify.annotations.NullMarked;
+diff --git a/src/main/java/io/papermc/paper/datacomponent/package-info.java b/src/main/java/io/papermc/paper/datacomponent/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..62aa1061c35d5358e6dec16a52574b427cc4b732
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/datacomponent/package-info.java
+@@ -0,0 +1,4 @@
++@NullMarked
++package io.papermc.paper.datacomponent;
++
++import org.jspecify.annotations.NullMarked;
+diff --git a/src/main/java/io/papermc/paper/registry/PaperRegistries.java b/src/main/java/io/papermc/paper/registry/PaperRegistries.java
+index fd024576e70e0c121c1477a0b7777af18159b7c4..132afec6bceb6c866de0aeb83db9613d4e74e2e7 100644
+--- a/src/main/java/io/papermc/paper/registry/PaperRegistries.java
++++ b/src/main/java/io/papermc/paper/registry/PaperRegistries.java
+@@ -2,6 +2,8 @@ package io.papermc.paper.registry;
+
+ import com.google.common.base.Preconditions;
+ import io.papermc.paper.adventure.PaperAdventure;
++import io.papermc.paper.datacomponent.DataComponentType;
++import io.papermc.paper.datacomponent.PaperDataComponentType;
+ import io.papermc.paper.registry.data.PaperEnchantmentRegistryEntry;
+ import io.papermc.paper.registry.data.PaperGameEventRegistryEntry;
+ import io.papermc.paper.registry.data.PaperPaintingVariantRegistryEntry;
+@@ -95,6 +97,7 @@ public final class PaperRegistries {
+ entry(Registries.ATTRIBUTE, RegistryKey.ATTRIBUTE, Attribute.class, CraftAttribute::new),
+ entry(Registries.FLUID, RegistryKey.FLUID, Fluid.class, CraftFluid::new),
+ entry(Registries.SOUND_EVENT, RegistryKey.SOUND_EVENT, Sound.class, CraftSound::new),
++ entry(Registries.DATA_COMPONENT_TYPE, RegistryKey.DATA_COMPONENT_TYPE, DataComponentType.class, PaperDataComponentType::of),
+
+ // data-drivens
+ entry(Registries.BIOME, RegistryKey.BIOME, Biome.class, CraftBiome::new).delayed(),
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
+index 756c73a401437566258813946fa10c7caa8f2469..78975412da0f0c2b802bfce6d30d56b26d8023e2 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
+@@ -20,13 +20,11 @@ import net.minecraft.world.item.enchantment.ItemEnchantments;
+ import org.bukkit.Material;
+ import org.bukkit.configuration.serialization.DelegateDeserialization;
+ import org.bukkit.craftbukkit.enchantments.CraftEnchantment;
+-import org.bukkit.craftbukkit.util.CraftLegacy;
+ import org.bukkit.craftbukkit.util.CraftMagicNumbers;
+ import org.bukkit.enchantments.Enchantment;
+ import org.bukkit.inventory.ItemStack;
+ import org.bukkit.inventory.meta.ItemMeta;
+ import org.bukkit.material.MaterialData;
+-import org.jetbrains.annotations.ApiStatus;
+
+ @DelegateDeserialization(ItemStack.class)
+ public final class CraftItemStack extends ItemStack {
+@@ -206,7 +204,7 @@ public final class CraftItemStack extends ItemStack {
+ this.adjustTagForItemMeta(oldType); // Paper
+ }
+ }
+- this.setData(null);
++ this.setData((MaterialData) null); // Paper
+ }
+
+ @Override
+@@ -245,7 +243,7 @@ public final class CraftItemStack extends ItemStack {
+
+ @Override
+ public int getMaxStackSize() {
+- return (this.handle == null) ? Material.AIR.getMaxStackSize() : this.handle.getMaxStackSize();
++ return (this.handle == null) ? Item.DEFAULT_MAX_STACK_SIZE : this.handle.getMaxStackSize(); // Paper - air stacks to 64
+ }
+
+ // Paper start
+@@ -267,12 +265,14 @@ public final class CraftItemStack extends ItemStack {
+ public void addUnsafeEnchantment(Enchantment ench, int level) {
+ Preconditions.checkArgument(ench != null, "Enchantment cannot be null");
+
+- // Paper start - Replace whole method
+- final ItemMeta itemMeta = this.getItemMeta();
+- if (itemMeta != null) {
+- itemMeta.addEnchant(ench, level, true);
+- this.setItemMeta(itemMeta);
++ // Paper start
++ if (this.handle == null) {
++ return;
+ }
++
++ EnchantmentHelper.updateEnchantments(this.handle, mutable -> { // data component api doesn't really support mutable things once already set yet
++ mutable.set(CraftEnchantment.bukkitToMinecraftHolder(ench), level);
++ });
+ // Paper end
+ }
+
+@@ -302,17 +302,28 @@ public final class CraftItemStack extends ItemStack {
+ public int removeEnchantment(Enchantment ench) {
+ Preconditions.checkArgument(ench != null, "Enchantment cannot be null");
+
+- // Paper start - replace entire method
+- int level = getEnchantmentLevel(ench);
+- if (level > 0) {
+- final ItemMeta itemMeta = this.getItemMeta();
+- if (itemMeta == null) return 0;
+- itemMeta.removeEnchant(ench);
+- this.setItemMeta(itemMeta);
++ // Paper start
++ if (this.handle == null) {
++ return 0;
++ }
++
++ ItemEnchantments itemEnchantments = this.handle.getOrDefault(DataComponents.ENCHANTMENTS, ItemEnchantments.EMPTY);
++ if (itemEnchantments.isEmpty()) {
++ return 0;
+ }
+- // Paper end
+
+- return level;
++ Holder<net.minecraft.world.item.enchantment.Enchantment> removedEnchantment = CraftEnchantment.bukkitToMinecraftHolder(ench);
++ if (itemEnchantments.keySet().contains(removedEnchantment)) {
++ int previousLevel = itemEnchantments.getLevel(removedEnchantment);
++
++ ItemEnchantments.Mutable mutable = new ItemEnchantments.Mutable(itemEnchantments); // data component api doesn't really support mutable things once already set yet
++ mutable.removeIf(enchantment -> enchantment.equals(removedEnchantment));
++ this.handle.set(DataComponents.ENCHANTMENTS, mutable.toImmutable());
++ return previousLevel;
++ }
++
++ return 0;
++ // Paper end
+ }
+
+ @Override
+@@ -324,7 +335,13 @@ public final class CraftItemStack extends ItemStack {
+
+ @Override
+ public Map<Enchantment, Integer> getEnchantments() {
+- return this.hasItemMeta() ? this.getItemMeta().getEnchants() : ImmutableMap.<Enchantment, Integer>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<Enchantment, Integer> getEnchantments(net.minecraft.world.item.ItemStack item) {
+@@ -526,4 +543,119 @@ public final class CraftItemStack extends ItemStack {
+ return this.pdcView;
+ }
+ // Paper end - pdc
++ // Paper start - data component API
++ @Override
++ public <T> T getData(final io.papermc.paper.datacomponent.DataComponentType.Valued<T> type) {
++ if (this.isEmpty()) {
++ return null;
++ }
++ return io.papermc.paper.datacomponent.PaperDataComponentType.convertDataComponentValue(this.handle.getComponents(), (io.papermc.paper.datacomponent.PaperDataComponentType.ValuedImpl<T, ?>) 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.PaperDataComponentType.bukkitToMinecraft(type));
++ }
++
++ @Override
++ public java.util.Set<io.papermc.paper.datacomponent.DataComponentType> getDataTypes() {
++ if (this.isEmpty()) {
++ return java.util.Collections.emptySet();
++ }
++ return io.papermc.paper.datacomponent.PaperDataComponentType.minecraftToBukkit(this.handle.getComponents().keySet());
++ }
++
++ @Override
++ public <T> void setData(final io.papermc.paper.datacomponent.DataComponentType.Valued<T> type, final T value) {
++ Preconditions.checkArgument(value != null, "value cannot be null");
++ if (this.isEmpty()) {
++ return;
++ }
++ this.setDataInternal((io.papermc.paper.datacomponent.PaperDataComponentType.ValuedImpl<T, ?>) type, value);
++ }
++
++ @Override
++ public void setData(final io.papermc.paper.datacomponent.DataComponentType.NonValued type) {
++ if (this.isEmpty()) {
++ return;
++ }
++ this.setDataInternal((io.papermc.paper.datacomponent.PaperDataComponentType.NonValuedImpl<?, ?>) type, null);
++ }
++
++ private <A, V> void setDataInternal(final io.papermc.paper.datacomponent.PaperDataComponentType<A, V> 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.PaperDataComponentType.bukkitToMinecraft(type));
++ }
++
++ @Override
++ public void resetData(final io.papermc.paper.datacomponent.DataComponentType type) {
++ if (this.isEmpty()) {
++ return;
++ }
++ this.resetData((io.papermc.paper.datacomponent.PaperDataComponentType<?, ?>) type);
++ }
++
++ private <M> void resetData(final io.papermc.paper.datacomponent.PaperDataComponentType<?, M> type) {
++ final net.minecraft.core.component.DataComponentType<M> nms = io.papermc.paper.datacomponent.PaperDataComponentType.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.PaperDataComponentType.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<io.papermc.paper.datacomponent.DataComponentType> 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<net.minecraft.core.component.DataComponentType<?>> skippingTypes = new java.util.HashSet<>(exclude.size());
++ for (io.papermc.paper.datacomponent.DataComponentType api : exclude) {
++ skippingTypes.add(io.papermc.paper.datacomponent.PaperDataComponentType.bukkitToMinecraft(api));
++ }
++
++ // Check the patch by first stripping excluded types and then compare the trimmed patches
++ return left.getComponentsPatch().forget(skippingTypes::contains).equals(right.getComponentsPatch().forget(skippingTypes::contains));
++ }
++
++ // Paper end - data component API
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java
+index 1b57649d0d3db24ed32c78cf3d5ce1d9fb1353e0..b0da057ce5124cb60b6249e13d7a5771601af937 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java
+@@ -150,7 +150,7 @@ public class CraftItemType<M extends ItemMeta> implements ItemType.Typed<M>, Han
+ public int getMaxStackSize() {
+ // Based of the material enum air is only 0, in PerMaterialTest it is also set as special case
+ // the item info itself would return 64
+- if (this == AIR) {
++ if (false && this == AIR) { // Paper - air stacks to 64
+ return 0;
+ }
+ return this.item.components().getOrDefault(DataComponents.MAX_STACK_SIZE, 64);
+@@ -270,4 +270,20 @@ public class CraftItemType<M extends ItemMeta> implements ItemType.Typed<M>, 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> T getDefaultData(final io.papermc.paper.datacomponent.DataComponentType.Valued<T> type) {
++ return io.papermc.paper.datacomponent.PaperDataComponentType.convertDataComponentValue(this.item.components(), ((io.papermc.paper.datacomponent.PaperDataComponentType.ValuedImpl<T, ?>) type));
++ }
++
++ @Override
++ public boolean hasDefaultData(final io.papermc.paper.datacomponent.DataComponentType type) {
++ return this.item.components().has(io.papermc.paper.datacomponent.PaperDataComponentType.bukkitToMinecraft(type));
++ }
++
++ @Override
++ public java.util.Set<io.papermc.paper.datacomponent.DataComponentType> getDefaultDataTypes() {
++ return io.papermc.paper.datacomponent.PaperDataComponentType.minecraftToBukkit(this.item.components().keySet());
++ }
++ // Paper end - data component API
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java
+index a944803771d514572f94b4e98a6d4435a009c078..82cb8cd1635c279326cec8454f1906ce35021dec 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java
+@@ -91,7 +91,7 @@ class CraftMetaFirework extends CraftMetaItem implements FireworkMeta {
+ this.safelyAddEffects(effects, false); // Paper - limit firework effects
+ }
+
+- static FireworkEffect getEffect(FireworkExplosion explosion) {
++ public static FireworkEffect getEffect(FireworkExplosion explosion) { // Paper
+ FireworkEffect.Builder effect = FireworkEffect.builder()
+ .flicker(explosion.hasTwinkle())
+ .trail(explosion.hasTrail())
+@@ -111,7 +111,7 @@ class CraftMetaFirework extends CraftMetaItem implements FireworkMeta {
+ return effect.build();
+ }
+
+- static FireworkExplosion getExplosion(FireworkEffect effect) {
++ public static FireworkExplosion getExplosion(FireworkEffect effect) { // Paper
+ IntList colors = CraftMetaFirework.addColors(effect.getColors());
+ IntList fadeColors = CraftMetaFirework.addColors(effect.getFadeColors());
+
+diff --git a/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.ItemComponentTypesBridge b/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.ItemComponentTypesBridge
+new file mode 100644
+index 0000000000000000000000000000000000000000..0fd276c2fdbba784c1cd95105553996b4ba2460e
+--- /dev/null
++++ b/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.ItemComponentTypesBridge
+@@ -0,0 +1 @@
++io.papermc.paper.datacomponent.item.ItemComponentTypesBridgesImpl
+diff --git a/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.consumable.ConsumableTypesBridge b/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.consumable.ConsumableTypesBridge
+new file mode 100644
+index 0000000000000000000000000000000000000000..852ab097181491735fb9ee5ee4f70e4ceeb32e6d
+--- /dev/null
++++ b/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.consumable.ConsumableTypesBridge
+@@ -0,0 +1 @@
++io.papermc.paper.datacomponent.item.consumable.ConsumableTypesBridgeImpl
+diff --git a/src/test/java/io/papermc/paper/datacomponent/DataComponentTypesTest.java b/src/test/java/io/papermc/paper/datacomponent/DataComponentTypesTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..1d707114f53e80bf278dc640c55b515d85f03120
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/datacomponent/DataComponentTypesTest.java
+@@ -0,0 +1,58 @@
++package io.papermc.paper.datacomponent;
++
++import com.google.common.collect.Collections2;
++import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
++import net.minecraft.core.registries.Registries;
++import net.minecraft.resources.ResourceLocation;
++import org.bukkit.craftbukkit.util.CraftNamespacedKey;
++import org.bukkit.support.RegistryHelper;
++import org.bukkit.support.environment.AllFeatures;
++import org.junit.jupiter.api.Assertions;
++import org.junit.jupiter.api.Test;
++import java.lang.reflect.Field;
++import java.util.Set;
++import java.util.function.Predicate;
++import java.util.stream.Collectors;
++
++@AllFeatures
++public class DataComponentTypesTest {
++
++ private static final Set<ResourceLocation> 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<ResourceLocation> vanillaDataComponentTypes = new ObjectOpenHashSet<>(
++ RegistryHelper.getRegistry()
++ .lookupOrThrow(Registries.DATA_COMPONENT_TYPE)
++ .keySet()
++ );
++
++ for (final Field declaredField : DataComponentTypes.class.getDeclaredFields()) {
++ if (!DataComponentType.class.isAssignableFrom(declaredField.getType())) continue;
++
++ final DataComponentType dataComponentType = (DataComponentType) declaredField.get(null);
++ if (!vanillaDataComponentTypes.remove(CraftNamespacedKey.toMinecraft(dataComponentType.getKey()))) {
++ Assertions.fail("API defined component type " + dataComponentType.key().asMinimalString() + " is unknown to vanilla registry");
++ }
++ }
++
++ if (!vanillaDataComponentTypes.containsAll(NOT_IN_API)) {
++ Assertions.fail("API defined data components that were marked as not-yet-implemented: " + NOT_IN_API.stream().filter(Predicate.not(vanillaDataComponentTypes::contains)).map(ResourceLocation::toString).collect(Collectors.joining(", ")));
++ }
++
++ vanillaDataComponentTypes.removeAll(NOT_IN_API);
++ if (!vanillaDataComponentTypes.isEmpty()) {
++ Assertions.fail("API did not define following vanilla data component types: " + String.join(", ", Collections2.transform(vanillaDataComponentTypes, ResourceLocation::toString)));
++ }
++ }
++
++}
+diff --git a/src/test/java/io/papermc/paper/item/ItemStackDataComponentEqualsTest.java b/src/test/java/io/papermc/paper/item/ItemStackDataComponentEqualsTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..4ee0491763341232844a99aa528310a3b3dca1d5
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/item/ItemStackDataComponentEqualsTest.java
+@@ -0,0 +1,92 @@
++package io.papermc.paper.item;
++
++import io.papermc.paper.datacomponent.DataComponentTypes;
++import java.util.Set;
++import net.kyori.adventure.text.Component;
++import org.bukkit.Material;
++import org.bukkit.inventory.ItemStack;
++import org.bukkit.support.environment.AllFeatures;
++import org.junit.jupiter.api.Assertions;
++import org.junit.jupiter.api.Test;
++
++@AllFeatures
++class ItemStackDataComponentEqualsTest {
++
++ @Test
++ public void testEqual() {
++ ItemStack item1 = ItemStack.of(Material.STONE, 1);
++ item1.setData(DataComponentTypes.MAX_STACK_SIZE, 32);
++ item1.setData(DataComponentTypes.ITEM_NAME, Component.text("HI"));
++
++ ItemStack item2 = ItemStack.of(Material.STONE, 1);
++ item2.setData(DataComponentTypes.MAX_STACK_SIZE, 32);
++ item2.setData(DataComponentTypes.ITEM_NAME, Component.text("HI"));
++
++ Assertions.assertTrue(item1.matchesWithoutData(item2, Set.of()));
++ }
++
++ @Test
++ public void testEqualIgnoreComponent() {
++ ItemStack item1 = ItemStack.of(Material.STONE, 2);
++ item1.setData(DataComponentTypes.MAX_STACK_SIZE, 1);
++
++ ItemStack item2 = ItemStack.of(Material.STONE, 1);
++ item2.setData(DataComponentTypes.MAX_STACK_SIZE, 2);
++
++ Assertions.assertFalse(item1.matchesWithoutData(item2, Set.of(DataComponentTypes.MAX_STACK_SIZE)));
++ }
++
++ @Test
++ public void testEqualIgnoreComponentAndSize() {
++ ItemStack item1 = ItemStack.of(Material.STONE, 2);
++ item1.setData(DataComponentTypes.MAX_STACK_SIZE, 1);
++
++ ItemStack item2 = ItemStack.of(Material.STONE, 1);
++ item2.setData(DataComponentTypes.MAX_STACK_SIZE, 2);
++
++ Assertions.assertTrue(item1.matchesWithoutData(item2, Set.of(DataComponentTypes.MAX_STACK_SIZE), true));
++ }
++
++ @Test
++ public void testEqualWithoutComponent() {
++ ItemStack item1 = ItemStack.of(Material.STONE, 1);
++
++ ItemStack item2 = ItemStack.of(Material.STONE, 1);
++ item2.setData(DataComponentTypes.MAX_STACK_SIZE, 2);
++
++ Assertions.assertFalse(item1.matchesWithoutData(item2, Set.of(DataComponentTypes.WRITTEN_BOOK_CONTENT)));
++ }
++
++ @Test
++ public void testEqualRemoveComponent() {
++ ItemStack item1 = ItemStack.of(Material.STONE, 1);
++ item1.unsetData(DataComponentTypes.MAX_STACK_SIZE);
++
++ ItemStack item2 = ItemStack.of(Material.STONE, 1);
++ item2.unsetData(DataComponentTypes.MAX_STACK_SIZE);
++
++ Assertions.assertTrue(item1.matchesWithoutData(item2, Set.of()));
++ }
++
++ @Test
++ public void testEqualIncludeComponentIgnoreSize() {
++ ItemStack item1 = ItemStack.of(Material.STONE, 2);
++ item1.setData(DataComponentTypes.MAX_STACK_SIZE, 1);
++
++ ItemStack item2 = ItemStack.of(Material.STONE, 1);
++ item2.setData(DataComponentTypes.MAX_STACK_SIZE, 1);
++
++ Assertions.assertTrue(item1.matchesWithoutData(item2, Set.of(), true));
++ }
++
++ @Test
++ public void testAdvancedExample() {
++ ItemStack oakLeaves = ItemStack.of(Material.OAK_LEAVES, 1);
++ oakLeaves.setData(DataComponentTypes.HIDE_TOOLTIP);
++ oakLeaves.setData(DataComponentTypes.MAX_STACK_SIZE, 1);
++
++ ItemStack otherOakLeavesItem = ItemStack.of(Material.OAK_LEAVES, 2);
++
++ Assertions.assertTrue(oakLeaves.matchesWithoutData(otherOakLeavesItem, Set.of(DataComponentTypes.HIDE_TOOLTIP, DataComponentTypes.MAX_STACK_SIZE), true));
++ }
++}
+diff --git a/src/test/java/io/papermc/paper/item/ItemStackDataComponentTest.java b/src/test/java/io/papermc/paper/item/ItemStackDataComponentTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..00133403f878d238fba84743b2d76a16d7a5e32f
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/item/ItemStackDataComponentTest.java
+@@ -0,0 +1,416 @@
++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 java.util.List;
++import java.util.Map;
++import java.util.function.BiConsumer;
++import java.util.function.Function;
++import net.kyori.adventure.key.Key;
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.util.TriState;
++import net.minecraft.core.component.DataComponents;
++import net.minecraft.core.registries.Registries;
++import net.minecraft.world.item.EitherHolder;
++import net.minecraft.world.item.Items;
++import net.minecraft.world.item.JukeboxSongs;
++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.craftbukkit.inventory.CraftItemStack;
++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.KnowledgeBookMeta;
++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.RegistryHelper;
++import org.bukkit.support.environment.AllFeatures;
++import org.junit.jupiter.api.Assertions;
++import org.junit.jupiter.api.Test;
++
++@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<Component> 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<Enchantment, Integer> 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<AttributeModifier>) stack.getItemMeta().getAttributeModifiers(Attribute.ATTACK_DAMAGE)).getFirst());
++ stack.unsetData(DataComponentTypes.ATTRIBUTE_MODIFIERS);
++ Assertions.assertNull(stack.getItemMeta().getAttributeModifiers());
++ }
++
++ @Test
++ void testLegacyCustomModelData() {
++ testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.CUSTOM_MODEL_DATA, CustomModelData.customModelData().addFloat(1).build(), customModelData -> customModelData.floats().get(0).intValue(), 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));
++ }
++
++ @Test
++ void testRecipes() {
++ final ItemStack stack = new ItemStack(Material.KNOWLEDGE_BOOK);
++ stack.setData(DataComponentTypes.RECIPES, List.of(Key.key("paper:fun_recipe")));
++
++ final ItemMeta itemMeta = stack.getItemMeta();
++ Assertions.assertInstanceOf(KnowledgeBookMeta.class, itemMeta);
++
++ final List<NamespacedKey> recipes = ((KnowledgeBookMeta) itemMeta).getRecipes();
++ Assertions.assertEquals(recipes, List.of(new NamespacedKey("paper", "fun_recipe")));
++ }
++
++ @Test
++ void testJukeboxWithEitherKey() {
++ final ItemStack apiStack = CraftItemStack.asBukkitCopy(new net.minecraft.world.item.ItemStack(Items.MUSIC_DISC_5));
++ final JukeboxPlayable data = apiStack.getData(DataComponentTypes.JUKEBOX_PLAYABLE);
++
++ Assertions.assertNotNull(data);
++ Assertions.assertEquals(JukeboxSong.FIVE, data.jukeboxSong());
++ }
++
++ @Test
++ void testJukeboxWithEitherHolder() {
++ final net.minecraft.world.item.ItemStack internalStack = new net.minecraft.world.item.ItemStack(Items.STONE);
++ internalStack.set(DataComponents.JUKEBOX_PLAYABLE, new net.minecraft.world.item.JukeboxPlayable(
++ new EitherHolder<>(RegistryHelper.getRegistry().lookupOrThrow(Registries.JUKEBOX_SONG).getOrThrow(JukeboxSongs.FIVE)),
++ true
++ ));
++
++ final ItemStack apiStack = CraftItemStack.asBukkitCopy(internalStack);
++ final JukeboxPlayable data = apiStack.getData(DataComponentTypes.JUKEBOX_PLAYABLE);
++
++ Assertions.assertNotNull(data);
++ Assertions.assertEquals(JukeboxSong.FIVE, data.jukeboxSong());
++ }
++
++ private static <T, M extends ItemMeta> void testWithMeta(final ItemStack stack, final DataComponentType.Valued<T> type, final T value, final Class<M> metaType, final Function<M, T> metaGetter, final BiConsumer<M, T> metaSetter) {
++ testWithMeta(stack, type, value, Function.identity(), metaType, metaGetter, metaSetter);
++ }
++
++ private static <T, M extends ItemMeta, R> void testWithMeta(final ItemStack stack, final DataComponentType.Valued<T> type, final T value, Function<T, R> mapper, final Class<M> metaType, final Function<M, R> metaGetter, final BiConsumer<M, R> 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 <M extends ItemMeta> void testWithMeta(final ItemStack stack, final DataComponentType.NonValued type, final boolean value, final Class<M> metaType, final Function<M, Boolean> metaGetter, final BiConsumer<M, Boolean> 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..7cda79980729770695451adcd03b1886f60d86e3
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/item/MetaComparisonTest.java
+@@ -0,0 +1,281 @@
++package io.papermc.paper.item;
++
++import com.destroystokyo.paper.profile.CraftPlayerProfile;
++import com.destroystokyo.paper.profile.PlayerProfile;
++import java.util.UUID;
++import java.util.function.Consumer;
++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;
++
++// 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(ItemStack itemStack, Consumer<ItemMeta> set, Consumer<ItemMeta> get) {
++ ItemMeta craftMeta = CraftItemStack.getItemMeta(CraftItemStack.asNMSCopy(itemStack)); // TODO: This should be converted to use the old meta when this is added.
++ ItemMeta paperMeta = CraftItemStack.getItemMeta(CraftItemStack.asNMSCopy(itemStack));
++ // Test craft meta
++ set.accept(craftMeta);
++ get.accept(craftMeta);
++
++ // Test paper meta
++ set.accept(paperMeta);
++ get.accept(paperMeta);
++ }
++}
+diff --git a/src/test/java/org/bukkit/PerMaterialTest.java b/src/test/java/org/bukkit/PerMaterialTest.java
+index 629fccec144b5d66addc0e8258cde90e81904e1c..6961730365da9083e8963200ecc5f85dbc654f35 100644
+--- a/src/test/java/org/bukkit/PerMaterialTest.java
++++ b/src/test/java/org/bukkit/PerMaterialTest.java
+@@ -101,17 +101,13 @@ public class PerMaterialTest {
+
+ final ItemStack bukkit = new ItemStack(material);
+ final CraftItemStack craft = CraftItemStack.asCraftCopy(bukkit);
+- if (material == Material.AIR) {
+- final int MAX_AIR_STACK = 0 /* Why can't I hold all of these AIR? */;
+- assertThat(material.getMaxStackSize(), is(MAX_AIR_STACK));
+- assertThat(bukkit.getMaxStackSize(), is(MAX_AIR_STACK));
+- assertThat(craft.getMaxStackSize(), is(MAX_AIR_STACK));
+- } else {
++
++ // Paper - remove air exception
+ int max = CraftMagicNumbers.getItem(material).components().getOrDefault(DataComponents.MAX_STACK_SIZE, 64);
+ assertThat(material.getMaxStackSize(), is(max));
+ assertThat(bukkit.getMaxStackSize(), is(max));
+ assertThat(craft.getMaxStackSize(), is(max));
+- }
++ // Paper - remove air exception
+ }
+
+ @ParameterizedTest
+diff --git a/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java b/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java
+index b717a5ffa567781b0687bbe238b62844214db284..dc5fadb3d98b443df54b554168d60fe0b0226664 100644
+--- a/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java
++++ b/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java
+@@ -100,6 +100,7 @@ public class RegistriesArgumentProvider implements ArgumentsProvider {
+ register(RegistryKey.MAP_DECORATION_TYPE, MapCursor.Type.class, Registries.MAP_DECORATION_TYPE, CraftMapCursor.CraftType.class, MapDecorationType.class);
+ register(RegistryKey.BANNER_PATTERN, PatternType.class, Registries.BANNER_PATTERN, CraftPatternType.class, BannerPattern.class);
+ register(RegistryKey.MENU, MenuType.class, Registries.MENU, CraftMenuType.class, net.minecraft.world.inventory.MenuType.class);
++ register(RegistryKey.DATA_COMPONENT_TYPE, io.papermc.paper.datacomponent.DataComponentType.class, Registries.DATA_COMPONENT_TYPE, io.papermc.paper.datacomponent.PaperDataComponentType.class, net.minecraft.core.component.DataComponentType.class);
+ }
+
+ private static void register(RegistryKey registryKey, Class bukkit, ResourceKey registry, Class craft, Class minecraft) { // Paper