diff options
Diffstat (limited to 'patches/server/1032-DataComponent-API.patch')
-rw-r--r-- | patches/server/1032-DataComponent-API.patch | 4926 |
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 |