diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/DataComponentAdapter.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/DataComponentAdapter.java new file mode 100644 index 0000000000..957fdf1e32 --- /dev/null +++ b/paper-server/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( + DataComponentType type, + Function apiToVanilla, + Function vanillaToApi, + boolean codecValidation +) { + static final Function API_TO_UNIT_CONVERTER = $ -> Unit.INSTANCE; + + public boolean isValued() { + return this.apiToVanilla != API_TO_UNIT_CONVERTER; + } + + public NMS toVanilla(final API value) { + final NMS nms = this.apiToVanilla.apply(value); + if (this.codecValidation) { + this.type.codecOrThrow().encodeStart(CraftRegistry.getMinecraftRegistry().createSerializationContext(NullOps.INSTANCE), nms).ifError(error -> { + throw new IllegalArgumentException("Failed to encode data component %s (%s)".formatted(BuiltInRegistries.DATA_COMPONENT_TYPE.getKey(this.type), error.message())); + }); + } + + return nms; + } + + public API fromVanilla(final NMS value) { + return this.vanillaToApi.apply(value); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/DataComponentAdapters.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/DataComponentAdapters.java new file mode 100644 index 0000000000..7675588202 --- /dev/null +++ b/paper-server/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_TO_API_CONVERTER = $ -> { + throw new UnsupportedOperationException("Cannot convert the Unit type to an API value"); + }; + + static final Map>, 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>, DataComponentType> componentType : BuiltInRegistries.DATA_COMPONENT_TYPE.entrySet()) { + if (!ADAPTERS.containsKey(componentType.getKey())) { + registerUntyped((DataComponentType) componentType.getValue()); + } + } + } + + public static void registerUntyped(final DataComponentType type) { + registerInternal(type, UNIT_TO_API_CONVERTER, DataComponentAdapter.API_TO_UNIT_CONVERTER, false); + } + + private static void registerIdentity(final DataComponentType type) { + registerInternal(type, Function.identity(), Function.identity(), true); + } + + private static > void register(final DataComponentType type, final Function vanillaToApi) { + registerInternal(type, vanillaToApi, Handleable::getHandle, false); + } + + private static void register(final DataComponentType type, final Function vanillaToApi, final Function apiToVanilla) { + registerInternal(type, vanillaToApi, apiToVanilla, false); + } + + private static void registerInternal(final DataComponentType type, final Function vanillaToApi, final Function apiToVanilla, final boolean codecValidation) { + final ResourceKey> key = BuiltInRegistries.DATA_COMPONENT_TYPE.getResourceKey(type).orElseThrow(); + if (ADAPTERS.containsKey(key)) { + throw new IllegalStateException("Duplicate adapter registration for " + key); + } + ADAPTERS.put(key, new DataComponentAdapter<>(type, apiToVanilla, vanillaToApi, codecValidation && !type.isTransient())); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/PaperDataComponentType.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/PaperDataComponentType.java new file mode 100644 index 0000000000..e2fcf870b2 --- /dev/null +++ b/paper-server/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 implements DataComponentType, Handleable> { + + static { + DataComponentAdapters.bootstrap(); + } + + public static net.minecraft.core.component.DataComponentType bukkitToMinecraft(final DataComponentType type) { + return CraftRegistry.bukkitToMinecraft(type); + } + + public static DataComponentType minecraftToBukkit(final net.minecraft.core.component.DataComponentType type) { + return CraftRegistry.minecraftToBukkit(type, Registries.DATA_COMPONENT_TYPE, Registry.DATA_COMPONENT_TYPE); + } + + public static Set minecraftToBukkit(final Set> nmsTypes) { + final Set types = new HashSet<>(nmsTypes.size()); + for (final net.minecraft.core.component.DataComponentType nmsType : nmsTypes) { + types.add(PaperDataComponentType.minecraftToBukkit(nmsType)); + } + return Collections.unmodifiableSet(types); + } + + public static @Nullable B convertDataComponentValue(final DataComponentMap map, final PaperDataComponentType.ValuedImpl type) { + final net.minecraft.core.component.DataComponentType nms = bukkitToMinecraft(type); + final M nmsValue = map.get(nms); + if (nmsValue == null) { + return null; + } + return type.getAdapter().fromVanilla(nmsValue); + } + + private final NamespacedKey key; + private final net.minecraft.core.component.DataComponentType type; + private final DataComponentAdapter adapter; + + public PaperDataComponentType(final NamespacedKey key, final net.minecraft.core.component.DataComponentType type, final DataComponentAdapter 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 getAdapter() { + return this.adapter; + } + + @Override + public net.minecraft.core.component.DataComponentType getHandle() { + return this.type; + } + + @SuppressWarnings("unchecked") + public static DataComponentType of(final NamespacedKey key, final net.minecraft.core.component.DataComponentType type) { + final DataComponentAdapter adapter = (DataComponentAdapter) 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 extends PaperDataComponentType implements NonValued { + + NonValuedImpl( + final NamespacedKey key, + final net.minecraft.core.component.DataComponentType type, + final DataComponentAdapter adapter + ) { + super(key, type, adapter); + } + } + + public static final class ValuedImpl extends PaperDataComponentType implements Valued { + + ValuedImpl( + final NamespacedKey key, + final net.minecraft.core.component.DataComponentType type, + final DataComponentAdapter adapter + ) { + super(key, type, adapter); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/ItemComponentTypesBridgesImpl.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/ItemComponentTypesBridgesImpl.java new file mode 100644 index 0000000000..15c66b0186 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/ItemComponentTypesBridgesImpl.java @@ -0,0 +1,239 @@ +package io.papermc.paper.datacomponent.item; + +import com.destroystokyo.paper.profile.PlayerProfile; +import com.google.common.base.Preconditions; +import io.papermc.paper.registry.PaperRegistries; +import io.papermc.paper.registry.set.PaperRegistrySets; +import io.papermc.paper.registry.set.RegistryKeySet; +import io.papermc.paper.registry.tag.TagKey; +import io.papermc.paper.text.Filtered; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.util.TriState; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.item.component.OminousBottleAmplifier; +import org.bukkit.JukeboxSong; +import org.bukkit.block.BlockType; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.damage.DamageType; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; +import org.bukkit.inventory.meta.trim.ArmorTrim; +import org.bukkit.map.MapCursor; +import org.jspecify.annotations.Nullable; + +public final class ItemComponentTypesBridgesImpl implements ItemComponentTypesBridge { + + @Override + public ChargedProjectiles.Builder chargedProjectiles() { + return new PaperChargedProjectiles.BuilderImpl(); + } + + @Override + public PotDecorations.Builder potDecorations() { + return new PaperPotDecorations.BuilderImpl(); + } + + @Override + public Unbreakable.Builder unbreakable() { + return new PaperUnbreakable.BuilderImpl(); + } + + @Override + public ItemLore.Builder lore() { + return new PaperItemLore.BuilderImpl(); + } + + @Override + public ItemEnchantments.Builder enchantments() { + return new PaperItemEnchantments.BuilderImpl(); + } + + @Override + public ItemAttributeModifiers.Builder modifiers() { + return new PaperItemAttributeModifiers.BuilderImpl(); + } + + @Override + public FoodProperties.Builder food() { + return new PaperFoodProperties.BuilderImpl(); + } + + @Override + public DyedItemColor.Builder dyedItemColor() { + return new PaperDyedItemColor.BuilderImpl(); + } + + @Override + public PotionContents.Builder potionContents() { + return new PaperPotionContents.BuilderImpl(); + } + + @Override + public BundleContents.Builder bundleContents() { + return new PaperBundleContents.BuilderImpl(); + } + + @Override + public SuspiciousStewEffects.Builder suspiciousStewEffects() { + return new PaperSuspiciousStewEffects.BuilderImpl(); + } + + @Override + public MapItemColor.Builder mapItemColor() { + return new PaperMapItemColor.BuilderImpl(); + } + + @Override + public MapDecorations.Builder mapDecorations() { + return new PaperMapDecorations.BuilderImpl(); + } + + @Override + public MapDecorations.DecorationEntry decorationEntry(final MapCursor.Type type, final double x, final double z, final float rotation) { + return PaperMapDecorations.PaperDecorationEntry.toApi(type, x, z, rotation); + } + + @Override + public SeededContainerLoot.Builder seededContainerLoot(final Key lootTableKey) { + return new PaperSeededContainerLoot.BuilderImpl(lootTableKey); + } + + @Override + public ItemContainerContents.Builder itemContainerContents() { + return new PaperItemContainerContents.BuilderImpl(); + } + + @Override + public JukeboxPlayable.Builder jukeboxPlayable(final JukeboxSong song) { + return new PaperJukeboxPlayable.BuilderImpl(song); + } + + @Override + public Tool.Builder tool() { + return new PaperItemTool.BuilderImpl(); + } + + @Override + public Tool.Rule rule(final RegistryKeySet blocks, final @Nullable Float speed, final TriState correctForDrops) { + return PaperItemTool.PaperRule.fromUnsafe(blocks, speed, correctForDrops); + } + + @Override + public ItemAdventurePredicate.Builder itemAdventurePredicate() { + return new PaperItemAdventurePredicate.BuilderImpl(); + } + + @Override + public WrittenBookContent.Builder writtenBookContent(final Filtered title, final String author) { + return new PaperWrittenBookContent.BuilderImpl(title, author); + } + + @Override + public WritableBookContent.Builder writeableBookContent() { + return new PaperWritableBookContent.BuilderImpl(); + } + + @Override + public ItemArmorTrim.Builder itemArmorTrim(final ArmorTrim armorTrim) { + return new PaperItemArmorTrim.BuilderImpl(armorTrim); + } + + @Override + public LodestoneTracker.Builder lodestoneTracker() { + return new PaperLodestoneTracker.BuilderImpl(); + } + + @Override + public Fireworks.Builder fireworks() { + return new PaperFireworks.BuilderImpl(); + } + + @Override + public ResolvableProfile.Builder resolvableProfile() { + return new PaperResolvableProfile.BuilderImpl(); + } + + @Override + public ResolvableProfile resolvableProfile(final PlayerProfile profile) { + return PaperResolvableProfile.toApi(profile); + } + + @Override + public BannerPatternLayers.Builder bannerPatternLayers() { + return new PaperBannerPatternLayers.BuilderImpl(); + } + + @Override + public BlockItemDataProperties.Builder blockItemStateProperties() { + return new PaperBlockItemDataProperties.BuilderImpl(); + } + + @Override + public MapId mapId(final int id) { + return new PaperMapId(new net.minecraft.world.level.saveddata.maps.MapId(id)); + } + + @Override + public UseRemainder useRemainder(final ItemStack itemStack) { + Preconditions.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 types) { + return new PaperDamageResistant(new net.minecraft.world.item.component.DamageResistant(PaperRegistries.toNms(types))); + } + + @Override + public Enchantable enchantable(final int level) { + return new PaperEnchantable(new net.minecraft.world.item.enchantment.Enchantable(level)); + } + + @Override + public Repairable repairable(final RegistryKeySet types) { + return new PaperRepairable(new net.minecraft.world.item.enchantment.Repairable( + PaperRegistrySets.convertToNms(Registries.ITEM, BuiltInRegistries.BUILT_IN_CONVERSIONS.lookup(), types) + )); + } + + @Override + public Equippable.Builder equippable(EquipmentSlot slot) { + return new PaperEquippable.BuilderImpl(slot); + } + + @Override + public DeathProtection.Builder deathProtection() { + return new PaperDeathProtection.BuilderImpl(); + } + + @Override + public CustomModelData.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/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperBannerPatternLayers.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperBannerPatternLayers.java new file mode 100644 index 0000000000..ca49c2d2e1 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperBannerPatternLayers.java @@ -0,0 +1,62 @@ +package io.papermc.paper.datacomponent.item; + +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; +import io.papermc.paper.util.MCUtil; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import org.bukkit.DyeColor; +import org.bukkit.block.banner.Pattern; +import org.bukkit.block.banner.PatternType; +import org.bukkit.craftbukkit.CraftRegistry; +import org.bukkit.craftbukkit.block.banner.CraftPatternType; +import org.bukkit.craftbukkit.util.Handleable; +import org.jetbrains.annotations.Unmodifiable; + +public record PaperBannerPatternLayers( + net.minecraft.world.level.block.entity.BannerPatternLayers impl +) implements BannerPatternLayers, Handleable { + + private static List convert(final net.minecraft.world.level.block.entity.BannerPatternLayers nmsPatterns) { + return MCUtil.transformUnmodifiable(nmsPatterns.layers(), input -> { + final Optional type = CraftRegistry.unwrapAndConvertHolder(RegistryAccess.registryAccess().getRegistry(RegistryKey.BANNER_PATTERN), input.pattern()); + return new Pattern(Objects.requireNonNull(DyeColor.getByWoolData((byte) input.color().getId())), type.orElseThrow(() -> new IllegalStateException("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 patterns() { + return convert(impl); + } + + static final class BuilderImpl implements BannerPatternLayers.Builder { + + private final net.minecraft.world.level.block.entity.BannerPatternLayers.Builder builder = new net.minecraft.world.level.block.entity.BannerPatternLayers.Builder(); + + @Override + public BannerPatternLayers.Builder add(final Pattern pattern) { + this.builder.add( + CraftPatternType.bukkitToMinecraftHolder(pattern.getPattern()), + net.minecraft.world.item.DyeColor.byId(pattern.getColor().getWoolData()) + ); + return this; + } + + @Override + public BannerPatternLayers.Builder addAll(final List patterns) { + patterns.forEach(this::add); + return this; + } + + @Override + public BannerPatternLayers build() { + return new PaperBannerPatternLayers(this.builder.build()); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperBlockItemDataProperties.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperBlockItemDataProperties.java new file mode 100644 index 0000000000..5757e16c59 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperBlockItemDataProperties.java @@ -0,0 +1,50 @@ +package io.papermc.paper.datacomponent.item; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import java.util.Map; +import net.minecraft.world.item.component.BlockItemStateProperties; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import org.bukkit.block.BlockType; +import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.block.CraftBlockType; +import org.bukkit.craftbukkit.block.data.CraftBlockData; +import org.bukkit.craftbukkit.util.Handleable; + +public record PaperBlockItemDataProperties( + BlockItemStateProperties impl +) implements BlockItemDataProperties, Handleable { + + @Override + public BlockData createBlockData(final BlockType blockType) { + final Block block = CraftBlockType.bukkitToMinecraftNew(blockType); + final BlockState defaultState = block.defaultBlockState(); + return this.impl.apply(defaultState).createCraftBlockData(); + } + + @Override + public BlockData applyTo(final BlockData blockData) { + final BlockState state = ((CraftBlockData) blockData).getState(); + return this.impl.apply(state).createCraftBlockData(); + } + + @Override + public BlockItemStateProperties getHandle() { + return this.impl; + } + + static final class BuilderImpl implements BlockItemDataProperties.Builder { + + private final Map properties = new Object2ObjectOpenHashMap<>(); + + // TODO when BlockProperty API is merged + + @Override + public BlockItemDataProperties build() { + if (this.properties.isEmpty()) { + return new PaperBlockItemDataProperties(BlockItemStateProperties.EMPTY); + } + return new PaperBlockItemDataProperties(new BlockItemStateProperties(new Object2ObjectOpenHashMap<>(this.properties))); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperBundleContents.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperBundleContents.java new file mode 100644 index 0000000000..ba95ce77db --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperBundleContents.java @@ -0,0 +1,51 @@ +package io.papermc.paper.datacomponent.item; + +import com.google.common.base.Preconditions; +import io.papermc.paper.util.MCUtil; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import java.util.List; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.craftbukkit.util.Handleable; +import org.bukkit.inventory.ItemStack; + +public record PaperBundleContents( + net.minecraft.world.item.component.BundleContents impl +) implements BundleContents, Handleable { + + @Override + public net.minecraft.world.item.component.BundleContents getHandle() { + return this.impl; + } + + @Override + public List contents() { + return MCUtil.transformUnmodifiable((List) this.impl.items(), CraftItemStack::asBukkitCopy); + } + + static final class BuilderImpl implements BundleContents.Builder { + + private final List items = new ObjectArrayList<>(); + + @Override + public BundleContents.Builder add(final ItemStack stack) { + Preconditions.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 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/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperChargedProjectiles.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperChargedProjectiles.java new file mode 100644 index 0000000000..2129dd67fd --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperChargedProjectiles.java @@ -0,0 +1,51 @@ +package io.papermc.paper.datacomponent.item; + +import com.google.common.base.Preconditions; +import io.papermc.paper.util.MCUtil; +import java.util.ArrayList; +import java.util.List; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.craftbukkit.util.Handleable; +import org.bukkit.inventory.ItemStack; + +public record PaperChargedProjectiles( + net.minecraft.world.item.component.ChargedProjectiles impl +) implements ChargedProjectiles, Handleable { + + @Override + public net.minecraft.world.item.component.ChargedProjectiles getHandle() { + return this.impl; + } + + @Override + public List projectiles() { + return MCUtil.transformUnmodifiable(this.impl.getItems() /*makes copies internally*/, CraftItemStack::asCraftMirror); + } + + static final class BuilderImpl implements ChargedProjectiles.Builder { + + private final List items = new ArrayList<>(); + + @Override + public ChargedProjectiles.Builder add(final ItemStack stack) { + Preconditions.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 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/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperConsumable.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperConsumable.java new file mode 100644 index 0000000000..0bc2bad71d --- /dev/null +++ b/paper-server/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 { + + private static final ItemUseAnimation[] VALUES = ItemUseAnimation.values(); + + @Override + public net.minecraft.world.item.component.Consumable getHandle() { + return this.impl; + } + + @Override + public @NonNegative float consumeSeconds() { + return this.impl.consumeSeconds(); + } + + @Override + public ItemUseAnimation animation() { + return VALUES[this.impl.animation().ordinal()]; + } + + @Override + public Key sound() { + return PaperAdventure.asAdventure(this.impl.sound().value().location()); + } + + @Override + public boolean hasConsumeParticles() { + return this.impl.hasConsumeParticles(); + } + + @Override + public @Unmodifiable List consumeEffects() { + return MCUtil.transformUnmodifiable(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 eatSound = SoundEvents.GENERIC_EAT; + private boolean hasConsumeParticles = true; + private final List effects = new ObjectArrayList<>(); + + @Override + public Builder consumeSeconds(final @NonNegative float consumeSeconds) { + Preconditions.checkArgument(consumeSeconds >= 0, "consumeSeconds must be non-negative, was %s", consumeSeconds); + this.consumeSeconds = consumeSeconds; + return this; + } + + @Override + public Builder animation(final ItemUseAnimation animation) { + this.consumeAnimation = VALUES[animation.ordinal()]; + return this; + } + + @Override + public Builder sound(final Key sound) { + 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 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/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperCustomModelData.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperCustomModelData.java new file mode 100644 index 0000000000..33a93c8acf --- /dev/null +++ b/paper-server/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 { + + @Override + public net.minecraft.world.item.component.CustomModelData getHandle() { + return this.impl; + } + + @Override + public List floats() { + return Collections.unmodifiableList(this.impl.floats()); + } + + @Override + public List flags() { + return Collections.unmodifiableList(this.impl.flags()); + } + + @Override + public List strings() { + return Collections.unmodifiableList(this.impl.strings()); + } + + @Override + public List 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 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 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 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 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 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/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperDamageResistant.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperDamageResistant.java new file mode 100644 index 0000000000..adc986c8b3 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperDamageResistant.java @@ -0,0 +1,21 @@ +package io.papermc.paper.datacomponent.item; + +import io.papermc.paper.registry.PaperRegistries; +import io.papermc.paper.registry.tag.TagKey; +import org.bukkit.craftbukkit.util.Handleable; +import org.bukkit.damage.DamageType; + +public record PaperDamageResistant( + net.minecraft.world.item.component.DamageResistant impl +) implements DamageResistant, Handleable { + + @Override + public net.minecraft.world.item.component.DamageResistant getHandle() { + return this.impl; + } + + @Override + public TagKey types() { + return PaperRegistries.fromNms(this.impl.types()); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperDeathProtection.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperDeathProtection.java new file mode 100644 index 0000000000..798e45d3b3 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperDeathProtection.java @@ -0,0 +1,50 @@ +package io.papermc.paper.datacomponent.item; + +import io.papermc.paper.datacomponent.item.consumable.ConsumeEffect; +import io.papermc.paper.datacomponent.item.consumable.PaperConsumableEffects; +import io.papermc.paper.util.MCUtil; +import java.util.ArrayList; +import java.util.List; +import org.bukkit.craftbukkit.util.Handleable; +import org.jetbrains.annotations.Unmodifiable; + +public record PaperDeathProtection( + net.minecraft.world.item.component.DeathProtection impl +) implements DeathProtection, Handleable { + + @Override + public net.minecraft.world.item.component.DeathProtection getHandle() { + return this.impl; + } + + @Override + public @Unmodifiable List deathEffects() { + return MCUtil.transformUnmodifiable(this.impl.deathEffects(), PaperConsumableEffects::fromNms); + } + + static final class BuilderImpl implements Builder { + + private final List effects = new ArrayList<>(); + + @Override + public Builder addEffect(final ConsumeEffect effect) { + this.effects.add(PaperConsumableEffects.toNms(effect)); + return this; + } + + @Override + public Builder addEffects(final List effects) { + for (final ConsumeEffect effect : effects) { + this.effects.add(PaperConsumableEffects.toNms(effect)); + } + return this; + } + + @Override + public DeathProtection build() { + return new PaperDeathProtection( + new net.minecraft.world.item.component.DeathProtection(this.effects) + ); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperDyedItemColor.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperDyedItemColor.java new file mode 100644 index 0000000000..2407d79e2e --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperDyedItemColor.java @@ -0,0 +1,52 @@ +package io.papermc.paper.datacomponent.item; + +import org.bukkit.Color; +import org.bukkit.craftbukkit.util.Handleable; + +public record PaperDyedItemColor( + net.minecraft.world.item.component.DyedItemColor impl +) implements DyedItemColor, Handleable { + + @Override + public net.minecraft.world.item.component.DyedItemColor getHandle() { + return this.impl; + } + + @Override + public Color color() { + return Color.fromRGB(this.impl.rgb() & 0x00FFFFFF); // skip alpha channel + } + + @Override + public boolean showInTooltip() { + return this.impl.showInTooltip(); + } + + @Override + public DyedItemColor showInTooltip(final boolean showInTooltip) { + return new PaperDyedItemColor(this.impl.withTooltip(showInTooltip)); + } + + static final class BuilderImpl implements DyedItemColor.Builder { + + private Color color = Color.WHITE; + private boolean showInToolTip = true; + + @Override + public DyedItemColor.Builder color(final Color color) { + this.color = color; + return this; + } + + @Override + public DyedItemColor.Builder showInTooltip(final boolean showInTooltip) { + this.showInToolTip = showInTooltip; + return this; + } + + @Override + public DyedItemColor build() { + return new PaperDyedItemColor(new net.minecraft.world.item.component.DyedItemColor(this.color.asRGB(), this.showInToolTip)); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperEnchantable.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperEnchantable.java new file mode 100644 index 0000000000..422e1a4d60 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperEnchantable.java @@ -0,0 +1,18 @@ +package io.papermc.paper.datacomponent.item; + +import org.bukkit.craftbukkit.util.Handleable; + +public record PaperEnchantable( + net.minecraft.world.item.enchantment.Enchantable impl +) implements Enchantable, Handleable { + + @Override + public net.minecraft.world.item.enchantment.Enchantable getHandle() { + return this.impl; + } + + @Override + public int value() { + return this.impl.value(); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperEquippable.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperEquippable.java new file mode 100644 index 0000000000..6d427d2c62 --- /dev/null +++ b/paper-server/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 { + + @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 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 equipSound = SoundEvents.ARMOR_EQUIP_GENERIC; + private Optional> assetId = Optional.empty(); + private Optional cameraOverlay = Optional.empty(); + private Optional>> allowedEntities = Optional.empty(); + private boolean dispensable = true; + private boolean swappable = true; + private boolean damageOnHurt = true; + + BuilderImpl(final EquipmentSlot equipmentSlot) { + this.equipmentSlot = CraftEquipmentSlot.getNMS(equipmentSlot); + } + + @Override + public Builder equipSound(final Key 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 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/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperFireworks.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperFireworks.java new file mode 100644 index 0000000000..80189eb505 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperFireworks.java @@ -0,0 +1,73 @@ +package io.papermc.paper.datacomponent.item; + +import com.google.common.base.Preconditions; +import io.papermc.paper.util.MCUtil; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import java.util.List; +import net.minecraft.world.item.component.FireworkExplosion; +import org.bukkit.FireworkEffect; +import org.bukkit.craftbukkit.inventory.CraftMetaFirework; +import org.bukkit.craftbukkit.util.Handleable; +import org.jetbrains.annotations.Unmodifiable; + +public record PaperFireworks( + net.minecraft.world.item.component.Fireworks impl +) implements Fireworks, Handleable { + + @Override + public net.minecraft.world.item.component.Fireworks getHandle() { + return this.impl; + } + + @Override + public @Unmodifiable List effects() { + return MCUtil.transformUnmodifiable(this.impl.explosions(), CraftMetaFirework::getEffect); + } + + @Override + public int flightDuration() { + return this.impl.flightDuration(); + } + + static final class BuilderImpl implements Fireworks.Builder { + + private final List effects = new ObjectArrayList<>(); + private int duration = 0; // default set from nms Fireworks component + + @Override + public Fireworks.Builder flightDuration(final int duration) { + Preconditions.checkArgument(duration >= 0 && duration <= 0xFF, "duration must be an unsigned byte ([%s, %s]), was %s", 0, 0xFF, duration); + this.duration = duration; + return this; + } + + @Override + public Fireworks.Builder addEffect(final FireworkEffect effect) { + Preconditions.checkArgument( + this.effects.size() + 1 <= net.minecraft.world.item.component.Fireworks.MAX_EXPLOSIONS, + "Cannot have more than %s effects, had %s", + net.minecraft.world.item.component.Fireworks.MAX_EXPLOSIONS, + this.effects.size() + 1 + ); + this.effects.add(CraftMetaFirework.getExplosion(effect)); + return this; + } + + @Override + public Fireworks.Builder addEffects(final List effects) { + Preconditions.checkArgument( + this.effects.size() + effects.size() <= net.minecraft.world.item.component.Fireworks.MAX_EXPLOSIONS, + "Cannot have more than %s effects, had %s", + net.minecraft.world.item.component.Fireworks.MAX_EXPLOSIONS, + this.effects.size() + effects.size() + ); + MCUtil.addAndConvert(this.effects, effects, CraftMetaFirework::getExplosion); + return this; + } + + @Override + public Fireworks build() { + return new PaperFireworks(new net.minecraft.world.item.component.Fireworks(this.duration, new ObjectArrayList<>(this.effects))); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperFoodProperties.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperFoodProperties.java new file mode 100644 index 0000000000..2a043bb900 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperFoodProperties.java @@ -0,0 +1,72 @@ +package io.papermc.paper.datacomponent.item; + +import com.google.common.base.Preconditions; +import org.bukkit.craftbukkit.util.Handleable; + +public record PaperFoodProperties( + net.minecraft.world.food.FoodProperties impl +) implements FoodProperties, Handleable { + + @Override + public int nutrition() { + return this.impl.nutrition(); + } + + @Override + public float saturation() { + return this.impl.saturation(); + } + + @Override + public boolean canAlwaysEat() { + return this.impl.canAlwaysEat(); + } + + @Override + public FoodProperties.Builder toBuilder() { + return new BuilderImpl() + .nutrition(this.nutrition()) + .saturation(this.saturation()) + .canAlwaysEat(this.canAlwaysEat()); + } + + @Override + public net.minecraft.world.food.FoodProperties getHandle() { + return this.impl; + } + + static final class BuilderImpl implements FoodProperties.Builder { + + private boolean canAlwaysEat = false; + private float saturation = 0; + private int nutrition = 0; + + @Override + public FoodProperties.Builder canAlwaysEat(final boolean canAlwaysEat) { + this.canAlwaysEat = canAlwaysEat; + return this; + } + + @Override + public FoodProperties.Builder saturation(final float saturation) { + this.saturation = saturation; + return this; + } + + @Override + public FoodProperties.Builder nutrition(final int nutrition) { + Preconditions.checkArgument(nutrition >= 0, "nutrition must be non-negative, was %s", nutrition); + this.nutrition = nutrition; + return this; + } + + @Override + public FoodProperties build() { + return new PaperFoodProperties(new net.minecraft.world.food.FoodProperties( + this.nutrition, + this.saturation, + this.canAlwaysEat + )); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAdventurePredicate.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAdventurePredicate.java new file mode 100644 index 0000000000..e6315cd0eb --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAdventurePredicate.java @@ -0,0 +1,75 @@ +package io.papermc.paper.datacomponent.item; + +import io.papermc.paper.block.BlockPredicate; +import io.papermc.paper.registry.RegistryKey; +import io.papermc.paper.registry.set.PaperRegistrySets; +import io.papermc.paper.util.MCUtil; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import java.util.List; +import java.util.Optional; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import org.bukkit.craftbukkit.util.Handleable; + +public record PaperItemAdventurePredicate( + net.minecraft.world.item.AdventureModePredicate impl +) implements ItemAdventurePredicate, Handleable { + + private static List convert(final net.minecraft.world.item.AdventureModePredicate nmsModifiers) { + return MCUtil.transformUnmodifiable(nmsModifiers.predicates, nms -> BlockPredicate.predicate() + .blocks(nms.blocks().map(blocks -> PaperRegistrySets.convertToApi(RegistryKey.BLOCK, blocks)).orElse(null)).build()); + } + + @Override + public net.minecraft.world.item.AdventureModePredicate getHandle() { + return this.impl; + } + + @Override + public boolean showInTooltip() { + return this.impl.showInTooltip(); + } + + @Override + public PaperItemAdventurePredicate showInTooltip(final boolean showInTooltip) { + return new PaperItemAdventurePredicate(this.impl.withTooltip(showInTooltip)); + } + + @Override + public List predicates() { + return convert(this.impl); + } + + static final class BuilderImpl implements ItemAdventurePredicate.Builder { + + private final List predicates = new ObjectArrayList<>(); + private boolean showInTooltip = 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 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/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemArmorTrim.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemArmorTrim.java new file mode 100644 index 0000000000..5d060c907f --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemArmorTrim.java @@ -0,0 +1,62 @@ +package io.papermc.paper.datacomponent.item; + +import org.bukkit.craftbukkit.inventory.trim.CraftTrimMaterial; +import org.bukkit.craftbukkit.inventory.trim.CraftTrimPattern; +import org.bukkit.craftbukkit.util.Handleable; +import org.bukkit.inventory.meta.trim.ArmorTrim; + +public record PaperItemArmorTrim( + net.minecraft.world.item.equipment.trim.ArmorTrim impl +) implements ItemArmorTrim, Handleable { + + @Override + public net.minecraft.world.item.equipment.trim.ArmorTrim getHandle() { + return this.impl; + } + + @Override + public boolean showInTooltip() { + return this.impl.showInTooltip(); + } + + @Override + public ItemArmorTrim showInTooltip(final boolean showInTooltip) { + return new PaperItemArmorTrim(this.impl.withTooltip(showInTooltip)); + } + + @Override + public ArmorTrim armorTrim() { + return new ArmorTrim(CraftTrimMaterial.minecraftHolderToBukkit(this.impl.material()), CraftTrimPattern.minecraftHolderToBukkit(this.impl.pattern())); + } + + static final class BuilderImpl implements ItemArmorTrim.Builder { + + private ArmorTrim armorTrim; + private boolean showInTooltip = true; + + BuilderImpl(final ArmorTrim armorTrim) { + this.armorTrim = armorTrim; + } + + @Override + public ItemArmorTrim.Builder showInTooltip(final boolean showInTooltip) { + this.showInTooltip = showInTooltip; + return this; + } + + @Override + public ItemArmorTrim.Builder armorTrim(final ArmorTrim armorTrim) { + this.armorTrim = armorTrim; + return this; + } + + @Override + public ItemArmorTrim build() { + return new PaperItemArmorTrim(new net.minecraft.world.item.equipment.trim.ArmorTrim( + CraftTrimMaterial.bukkitToMinecraftHolder(this.armorTrim.getMaterial()), + CraftTrimPattern.bukkitToMinecraftHolder(this.armorTrim.getPattern()), + this.showInTooltip + )); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAttributeModifiers.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAttributeModifiers.java new file mode 100644 index 0000000000..47ca2b8eb1 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemAttributeModifiers.java @@ -0,0 +1,97 @@ +package io.papermc.paper.datacomponent.item; + +import com.google.common.base.Preconditions; +import io.papermc.paper.util.MCUtil; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import java.util.List; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeModifier; +import org.bukkit.craftbukkit.CraftEquipmentSlot; +import org.bukkit.craftbukkit.attribute.CraftAttribute; +import org.bukkit.craftbukkit.attribute.CraftAttributeInstance; +import org.bukkit.craftbukkit.util.CraftNamespacedKey; +import org.bukkit.craftbukkit.util.Handleable; +import org.bukkit.inventory.EquipmentSlotGroup; +import org.jetbrains.annotations.Unmodifiable; + +public record PaperItemAttributeModifiers( + net.minecraft.world.item.component.ItemAttributeModifiers impl +) implements ItemAttributeModifiers, Handleable { + + private static List convert(final net.minecraft.world.item.component.ItemAttributeModifiers nmsModifiers) { + return MCUtil.transformUnmodifiable(nmsModifiers.modifiers(), nms -> new PaperEntry( + CraftAttribute.minecraftHolderToBukkit(nms.attribute()), + CraftAttributeInstance.convert(nms.modifier(), nms.slot()) + )); + } + + @Override + public net.minecraft.world.item.component.ItemAttributeModifiers getHandle() { + return this.impl; + } + + @Override + public boolean showInTooltip() { + return this.impl.showInTooltip(); + } + + @Override + public ItemAttributeModifiers showInTooltip(final boolean showInTooltip) { + return new PaperItemAttributeModifiers(this.impl.withTooltip(showInTooltip)); + } + + @Override + public @Unmodifiable List modifiers() { + return convert(this.impl); + } + + public record PaperEntry(Attribute attribute, AttributeModifier modifier) implements ItemAttributeModifiers.Entry { + } + + static final class BuilderImpl implements ItemAttributeModifiers.Builder { + + private final List entries = new ObjectArrayList<>(); + private boolean showInTooltip = net.minecraft.world.item.component.ItemAttributeModifiers.EMPTY.showInTooltip(); + + @Override + public Builder addModifier(final Attribute attribute, final AttributeModifier modifier) { + return this.addModifier(attribute, modifier, modifier.getSlotGroup()); + } + + @Override + public ItemAttributeModifiers.Builder addModifier(final Attribute attribute, final AttributeModifier modifier, final EquipmentSlotGroup equipmentSlotGroup) { + Preconditions.checkArgument( + this.entries.stream().noneMatch(e -> + e.modifier().id().equals(CraftNamespacedKey.toMinecraft(modifier.getKey())) && e.attribute().is(CraftNamespacedKey.toMinecraft(attribute.getKey())) + ), + "Cannot add 2 modifiers with identical keys on the same attribute (modifier %s for attribute %s)", + modifier.getKey(), attribute.getKey() + ); + + this.entries.add(new net.minecraft.world.item.component.ItemAttributeModifiers.Entry( + CraftAttribute.bukkitToMinecraftHolder(attribute), + CraftAttributeInstance.convert(modifier), + CraftEquipmentSlot.getNMSGroup(equipmentSlotGroup) + )); + return this; + } + + @Override + public ItemAttributeModifiers.Builder showInTooltip(final boolean showInTooltip) { + this.showInTooltip = showInTooltip; + return this; + } + + @Override + public ItemAttributeModifiers build() { + if (this.entries.isEmpty()) { + return new PaperItemAttributeModifiers(net.minecraft.world.item.component.ItemAttributeModifiers.EMPTY.withTooltip(this.showInTooltip)); + } + + return new PaperItemAttributeModifiers(new net.minecraft.world.item.component.ItemAttributeModifiers( + new ObjectArrayList<>(this.entries), + this.showInTooltip + )); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemContainerContents.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemContainerContents.java new file mode 100644 index 0000000000..2c4ecc2d5f --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemContainerContents.java @@ -0,0 +1,65 @@ +package io.papermc.paper.datacomponent.item; + +import com.google.common.base.Preconditions; +import io.papermc.paper.util.MCUtil; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import java.util.List; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.craftbukkit.util.Handleable; +import org.bukkit.inventory.ItemStack; + +public record PaperItemContainerContents( + net.minecraft.world.item.component.ItemContainerContents impl +) implements ItemContainerContents, Handleable { + + @Override + public net.minecraft.world.item.component.ItemContainerContents getHandle() { + return this.impl; + } + + @Override + public List contents() { + return MCUtil.transformUnmodifiable(this.impl.items, CraftItemStack::asBukkitCopy); + } + + static final class BuilderImpl implements ItemContainerContents.Builder { + + private final List items = new ObjectArrayList<>(); + + @Override + public ItemContainerContents.Builder add(final ItemStack stack) { + Preconditions.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 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/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemEnchantments.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemEnchantments.java new file mode 100644 index 0000000000..3cfb18f6a4 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemEnchantments.java @@ -0,0 +1,92 @@ +package io.papermc.paper.datacomponent.item; + +import com.google.common.base.Preconditions; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import net.minecraft.core.Holder; +import org.bukkit.craftbukkit.enchantments.CraftEnchantment; +import org.bukkit.craftbukkit.util.Handleable; +import org.bukkit.enchantments.Enchantment; + +public record PaperItemEnchantments( + net.minecraft.world.item.enchantment.ItemEnchantments impl, + Map enchantments // API values are stored externally as the concept of a lazy key transformer map does not make much sense +) implements ItemEnchantments, Handleable { + + public PaperItemEnchantments(final net.minecraft.world.item.enchantment.ItemEnchantments itemEnchantments) { + this(itemEnchantments, convert(itemEnchantments)); + } + + private static Map convert(final net.minecraft.world.item.enchantment.ItemEnchantments itemEnchantments) { + if (itemEnchantments.isEmpty()) { + return Collections.emptyMap(); + } + final Map map = new HashMap<>(itemEnchantments.size()); + for (final Object2IntMap.Entry> entry : itemEnchantments.entrySet()) { + map.put(CraftEnchantment.minecraftHolderToBukkit(entry.getKey()), entry.getIntValue()); + } + return Collections.unmodifiableMap(map); // TODO look into making a "transforming" map maybe? + } + + @Override + public boolean showInTooltip() { + return this.impl.showInTooltip; + } + + @Override + public ItemEnchantments showInTooltip(final boolean showInTooltip) { + return new PaperItemEnchantments(this.impl.withTooltip(showInTooltip), this.enchantments); + } + + @Override + public net.minecraft.world.item.enchantment.ItemEnchantments getHandle() { + return this.impl; + } + + static final class BuilderImpl implements ItemEnchantments.Builder { + + private final Map enchantments = new Object2ObjectOpenHashMap<>(); + private boolean showInTooltip = true; + + @Override + public ItemEnchantments.Builder add(final Enchantment enchantment, final int level) { + Preconditions.checkArgument( + level >= 1 && level <= net.minecraft.world.item.enchantment.Enchantment.MAX_LEVEL, + "level must be between %s and %s, was %s", + 1, net.minecraft.world.item.enchantment.Enchantment.MAX_LEVEL, + level + ); + this.enchantments.put(enchantment, level); + return this; + } + + @Override + public ItemEnchantments.Builder addAll(final Map enchantments) { + enchantments.forEach(this::add); + return this; + } + + @Override + public ItemEnchantments.Builder showInTooltip(final boolean showInTooltip) { + this.showInTooltip = showInTooltip; + return this; + } + + @Override + public ItemEnchantments build() { + final net.minecraft.world.item.enchantment.ItemEnchantments initialEnchantments = net.minecraft.world.item.enchantment.ItemEnchantments.EMPTY.withTooltip(this.showInTooltip); + if (this.enchantments.isEmpty()) { + return new PaperItemEnchantments(initialEnchantments); + } + + final net.minecraft.world.item.enchantment.ItemEnchantments.Mutable mutable = new net.minecraft.world.item.enchantment.ItemEnchantments.Mutable(initialEnchantments); + this.enchantments.forEach((enchantment, level) -> + mutable.set(CraftEnchantment.bukkitToMinecraftHolder(enchantment), level) + ); + return new PaperItemEnchantments(mutable.toImmutable()); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemLore.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemLore.java new file mode 100644 index 0000000000..3bb0c1aebb --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemLore.java @@ -0,0 +1,77 @@ +package io.papermc.paper.datacomponent.item; + +import com.google.common.base.Preconditions; +import io.papermc.paper.adventure.PaperAdventure; +import io.papermc.paper.util.MCUtil; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import java.util.ArrayList; +import java.util.List; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentLike; +import org.bukkit.craftbukkit.util.Handleable; +import org.jetbrains.annotations.Unmodifiable; + +public record PaperItemLore( + net.minecraft.world.item.component.ItemLore impl +) implements ItemLore, Handleable { + + @Override + public net.minecraft.world.item.component.ItemLore getHandle() { + return this.impl; + } + + @Override + public @Unmodifiable List lines() { + return MCUtil.transformUnmodifiable(this.impl.lines(), PaperAdventure::asAdventure); + } + + @Override + public @Unmodifiable List styledLines() { + return MCUtil.transformUnmodifiable(this.impl.styledLines(), PaperAdventure::asAdventure); + } + + static final class BuilderImpl implements ItemLore.Builder { + + private List lines = new ObjectArrayList<>(); + + private static void validateLineCount(final int current, final int add) { + final int newSize = current + add; + Preconditions.checkArgument( + newSize <= net.minecraft.world.item.component.ItemLore.MAX_LINES, + "Cannot have more than %s lines, had %s", + net.minecraft.world.item.component.ItemLore.MAX_LINES, + newSize + ); + } + + @Override + public ItemLore.Builder lines(final List lines) { + validateLineCount(0, lines.size()); + this.lines = new ArrayList<>(ComponentLike.asComponents(lines)); + return this; + } + + @Override + public ItemLore.Builder addLine(final ComponentLike line) { + validateLineCount(this.lines.size(), 1); + this.lines.add(line.asComponent()); + return this; + } + + @Override + public ItemLore.Builder addLines(final List lines) { + validateLineCount(this.lines.size(), lines.size()); + this.lines.addAll(ComponentLike.asComponents(lines)); + return this; + } + + @Override + public ItemLore build() { + if (this.lines.isEmpty()) { + return new PaperItemLore(net.minecraft.world.item.component.ItemLore.EMPTY); + } + + return new PaperItemLore(new net.minecraft.world.item.component.ItemLore(PaperAdventure.asVanilla(this.lines))); // asVanilla does a list clone + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemTool.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemTool.java new file mode 100644 index 0000000000..538a61eaa0 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperItemTool.java @@ -0,0 +1,100 @@ +package io.papermc.paper.datacomponent.item; + +import com.google.common.base.Preconditions; +import io.papermc.paper.registry.RegistryKey; +import io.papermc.paper.registry.set.PaperRegistrySets; +import io.papermc.paper.registry.set.RegistryKeySet; +import io.papermc.paper.util.MCUtil; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import net.kyori.adventure.util.TriState; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import org.bukkit.block.BlockType; +import org.bukkit.craftbukkit.util.Handleable; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +public record PaperItemTool( + net.minecraft.world.item.component.Tool impl +) implements Tool, Handleable { + + private static List convert(final List tool) { + return MCUtil.transformUnmodifiable(tool, nms -> new PaperRule( + PaperRegistrySets.convertToApi(RegistryKey.BLOCK, nms.blocks()), + nms.speed().orElse(null), + TriState.byBoolean(nms.correctForDrops().orElse(null)) + )); + } + + @Override + public net.minecraft.world.item.component.Tool getHandle() { + return this.impl; + } + + @Override + public @Unmodifiable List rules() { + return convert(this.impl.rules()); + } + + @Override + public float defaultMiningSpeed() { + return this.impl.defaultMiningSpeed(); + } + + @Override + public int damagePerBlock() { + return this.impl.damagePerBlock(); + } + + record PaperRule(RegistryKeySet blocks, @Nullable Float speed, TriState correctForDrops) implements Rule { + + public static PaperRule fromUnsafe(final RegistryKeySet blocks, final @Nullable Float speed, final TriState correctForDrops) { + Preconditions.checkArgument(speed == null || speed > 0, "speed must be positive"); + return new PaperRule(blocks, speed, correctForDrops); + } + } + + static final class BuilderImpl implements Builder { + + private final List rules = new ObjectArrayList<>(); + private int damage = 1; + private float miningSpeed = 1.0F; + + @Override + public Builder damagePerBlock(final int damage) { + Preconditions.checkArgument(damage >= 0, "damage must be non-negative, was %s", damage); + this.damage = damage; + return this; + } + + @Override + public Builder defaultMiningSpeed(final float miningSpeed) { + this.miningSpeed = miningSpeed; + return this; + } + + @Override + public Builder addRule(final Rule rule) { + this.rules.add(new net.minecraft.world.item.component.Tool.Rule( + PaperRegistrySets.convertToNms(Registries.BLOCK, BuiltInRegistries.BUILT_IN_CONVERSIONS.lookup(), rule.blocks()), + Optional.ofNullable(rule.speed()), + Optional.ofNullable(rule.correctForDrops().toBoolean()) + )); + return this; + } + + @Override + public Builder addRules(final Collection rules) { + rules.forEach(this::addRule); + return this; + } + + @Override + public Tool build() { + return new PaperItemTool(new net.minecraft.world.item.component.Tool(new ObjectArrayList<>(this.rules), this.miningSpeed, this.damage)); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperJukeboxPlayable.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperJukeboxPlayable.java new file mode 100644 index 0000000000..c43ccf7ccc --- /dev/null +++ b/paper-server/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 { + + @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/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperLodestoneTracker.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperLodestoneTracker.java new file mode 100644 index 0000000000..5b97249f6a --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperLodestoneTracker.java @@ -0,0 +1,53 @@ +package io.papermc.paper.datacomponent.item; + +import java.util.Optional; +import org.bukkit.Location; +import org.bukkit.craftbukkit.util.CraftLocation; +import org.bukkit.craftbukkit.util.Handleable; +import org.jspecify.annotations.Nullable; + +public record PaperLodestoneTracker( + net.minecraft.world.item.component.LodestoneTracker impl +) implements LodestoneTracker, Handleable { + + @Override + public net.minecraft.world.item.component.LodestoneTracker getHandle() { + return this.impl; + } + + @Override + public @Nullable Location location() { + return this.impl.target().map(CraftLocation::fromGlobalPos).orElse(null); + } + + @Override + public boolean tracked() { + return this.impl.tracked(); + } + + static final class BuilderImpl implements LodestoneTracker.Builder { + + private @Nullable Location location; + private boolean tracked = true; + + @Override + public LodestoneTracker.Builder location(final @Nullable Location location) { + this.location = location; + return this; + } + + @Override + public LodestoneTracker.Builder tracked(final boolean tracked) { + this.tracked = tracked; + return this; + } + + @Override + public LodestoneTracker build() { + return new PaperLodestoneTracker(new net.minecraft.world.item.component.LodestoneTracker( + Optional.ofNullable(this.location).map(CraftLocation::toGlobalPos), + this.tracked + )); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperMapDecorations.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperMapDecorations.java new file mode 100644 index 0000000000..322a1285b0 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperMapDecorations.java @@ -0,0 +1,97 @@ +package io.papermc.paper.datacomponent.item; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import org.bukkit.craftbukkit.map.CraftMapCursor; +import org.bukkit.craftbukkit.util.Handleable; +import org.bukkit.map.MapCursor; +import org.jspecify.annotations.Nullable; + +public record PaperMapDecorations( + net.minecraft.world.item.component.MapDecorations impl +) implements MapDecorations, Handleable { + + @Override + public net.minecraft.world.item.component.MapDecorations getHandle() { + return this.impl; + } + + @Override + public @Nullable DecorationEntry decoration(final String id) { + final net.minecraft.world.item.component.MapDecorations.Entry decoration = this.impl.decorations().get(id); + if (decoration == null) { + return null; + } + + return new PaperDecorationEntry(decoration); + } + + @Override + public Map decorations() { + if (this.impl.decorations().isEmpty()) { + return Collections.emptyMap(); + } + + final Set> entries = this.impl.decorations().entrySet(); + final Map decorations = new Object2ObjectOpenHashMap<>(entries.size()); + for (final Map.Entry entry : entries) { + decorations.put(entry.getKey(), new PaperDecorationEntry(entry.getValue())); + } + + return Collections.unmodifiableMap(decorations); + } + + public record PaperDecorationEntry(net.minecraft.world.item.component.MapDecorations.Entry entry) implements DecorationEntry { + + public static DecorationEntry toApi(final MapCursor.Type type, final double x, final double z, final float rotation) { + return new PaperDecorationEntry(new net.minecraft.world.item.component.MapDecorations.Entry(CraftMapCursor.CraftType.bukkitToMinecraftHolder(type), x, z, rotation)); + } + + @Override + public MapCursor.Type type() { + return CraftMapCursor.CraftType.minecraftHolderToBukkit(this.entry.type()); + } + + @Override + public double x() { + return this.entry.x(); + } + + @Override + public double z() { + return this.entry.z(); + } + + @Override + public float rotation() { + return this.entry.rotation(); + } + } + + static final class BuilderImpl implements Builder { + + private final Map entries = new Object2ObjectOpenHashMap<>(); + + @Override + public MapDecorations.Builder put(final String id, final DecorationEntry entry) { + this.entries.put(id, new net.minecraft.world.item.component.MapDecorations.Entry(CraftMapCursor.CraftType.bukkitToMinecraftHolder(entry.type()), entry.x(), entry.z(), entry.rotation())); + return this; + } + + @Override + public Builder putAll(final Map entries) { + entries.forEach(this::put); + return this; + } + + @Override + public MapDecorations build() { + if (this.entries.isEmpty()) { + return new PaperMapDecorations(net.minecraft.world.item.component.MapDecorations.EMPTY); + } + return new PaperMapDecorations(new net.minecraft.world.item.component.MapDecorations(new Object2ObjectOpenHashMap<>(this.entries))); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperMapId.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperMapId.java new file mode 100644 index 0000000000..a2b4cc372b --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperMapId.java @@ -0,0 +1,19 @@ +package io.papermc.paper.datacomponent.item; + +import org.bukkit.craftbukkit.util.Handleable; + +public record PaperMapId( + net.minecraft.world.level.saveddata.maps.MapId impl +) implements MapId, Handleable { + + @Override + public net.minecraft.world.level.saveddata.maps.MapId getHandle() { + return this.impl; + } + + @Override + public int id() { + return this.impl.id(); + } + +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperMapItemColor.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperMapItemColor.java new file mode 100644 index 0000000000..9b6fdfc9c1 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperMapItemColor.java @@ -0,0 +1,35 @@ +package io.papermc.paper.datacomponent.item; + +import org.bukkit.Color; +import org.bukkit.craftbukkit.util.Handleable; + +public record PaperMapItemColor( + net.minecraft.world.item.component.MapItemColor impl +) implements MapItemColor, Handleable { + + @Override + public net.minecraft.world.item.component.MapItemColor getHandle() { + return this.impl; + } + + @Override + public Color color() { + return Color.fromRGB(this.impl.rgb() & 0x00FFFFFF); // skip alpha channel + } + + static final class BuilderImpl implements Builder { + + private Color color = Color.fromRGB(net.minecraft.world.item.component.MapItemColor.DEFAULT.rgb()); + + @Override + public Builder color(final Color color) { + this.color = color; + return this; + } + + @Override + public MapItemColor build() { + return new PaperMapItemColor(new net.minecraft.world.item.component.MapItemColor(this.color.asRGB())); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperOminousBottleAmplifier.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperOminousBottleAmplifier.java new file mode 100644 index 0000000000..a7ed2aa21d --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperOminousBottleAmplifier.java @@ -0,0 +1,18 @@ +package io.papermc.paper.datacomponent.item; + +import org.bukkit.craftbukkit.util.Handleable; + +public record PaperOminousBottleAmplifier( + net.minecraft.world.item.component.OminousBottleAmplifier impl +) implements OminousBottleAmplifier, Handleable { + + @Override + public net.minecraft.world.item.component.OminousBottleAmplifier getHandle() { + return this.impl; + } + + @Override + public int amplifier() { + return this.impl.value(); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperPotDecorations.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperPotDecorations.java new file mode 100644 index 0000000000..bde757b51d --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperPotDecorations.java @@ -0,0 +1,83 @@ +package io.papermc.paper.datacomponent.item; + +import java.util.Optional; +import org.bukkit.craftbukkit.inventory.CraftItemType; +import org.bukkit.craftbukkit.util.Handleable; +import org.bukkit.inventory.ItemType; +import org.jspecify.annotations.Nullable; + +public record PaperPotDecorations( + net.minecraft.world.level.block.entity.PotDecorations impl +) implements PotDecorations, Handleable { + + @Override + public @Nullable ItemType back() { + return this.impl.back().map(CraftItemType::minecraftToBukkitNew).orElse(null); + } + + @Override + public @Nullable ItemType left() { + return this.impl.left().map(CraftItemType::minecraftToBukkitNew).orElse(null); + } + + @Override + public @Nullable ItemType right() { + return this.impl.right().map(CraftItemType::minecraftToBukkitNew).orElse(null); + } + + @Override + public @Nullable ItemType front() { + return this.impl.front().map(CraftItemType::minecraftToBukkitNew).orElse(null); + } + + @Override + public net.minecraft.world.level.block.entity.PotDecorations getHandle() { + return this.impl; + } + + static final class BuilderImpl implements PotDecorations.Builder { + + private @Nullable ItemType back; + private @Nullable ItemType left; + private @Nullable ItemType right; + private @Nullable ItemType front; + + @Override + public PotDecorations.Builder back(final @Nullable ItemType back) { + this.back = back; + return this; + } + + @Override + public PotDecorations.Builder left(final @Nullable ItemType left) { + this.left = left; + return this; + } + + @Override + public PotDecorations.Builder right(final @Nullable ItemType right) { + this.right = right; + return this; + } + + @Override + public PotDecorations.Builder front(final @Nullable ItemType front) { + this.front = front; + return this; + } + + @Override + public PotDecorations build() { + if (this.back == null && this.left == null && this.right == null && this.front == null) { + return new PaperPotDecorations(net.minecraft.world.level.block.entity.PotDecorations.EMPTY); + } + + return new PaperPotDecorations(new net.minecraft.world.level.block.entity.PotDecorations( + Optional.ofNullable(this.back).map(CraftItemType::bukkitToMinecraftNew), + Optional.ofNullable(this.left).map(CraftItemType::bukkitToMinecraftNew), + Optional.ofNullable(this.right).map(CraftItemType::bukkitToMinecraftNew), + Optional.ofNullable(this.front).map(CraftItemType::bukkitToMinecraftNew) + )); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperPotionContents.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperPotionContents.java new file mode 100644 index 0000000000..4712f8bbaa --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperPotionContents.java @@ -0,0 +1,103 @@ +package io.papermc.paper.datacomponent.item; + +import com.google.common.base.Preconditions; +import io.papermc.paper.util.MCUtil; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import java.util.List; +import java.util.Optional; +import net.minecraft.world.effect.MobEffectInstance; +import org.bukkit.Color; +import org.bukkit.craftbukkit.potion.CraftPotionType; +import org.bukkit.craftbukkit.potion.CraftPotionUtil; +import org.bukkit.craftbukkit.util.Handleable; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionType; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +public record PaperPotionContents( + net.minecraft.world.item.alchemy.PotionContents impl +) implements PotionContents, Handleable { + + @Override + public net.minecraft.world.item.alchemy.PotionContents getHandle() { + return this.impl; + } + + @Override + public @Unmodifiable List customEffects() { + return MCUtil.transformUnmodifiable(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 customEffects = new ObjectArrayList<>(); + private @Nullable PotionType type; + private @Nullable Color color; + private @Nullable String customName; + + @Override + public PotionContents.Builder potion(final @Nullable PotionType type) { + this.type = type; + return this; + } + + @Override + public PotionContents.Builder customColor(final @Nullable Color color) { + this.color = color; + return this; + } + + @Override + public Builder customName(final @Nullable String name) { + Preconditions.checkArgument(name == null || name.length() <= Short.MAX_VALUE, "Custom name is longer than %s characters", Short.MAX_VALUE); + this.customName = name; + return this; + } + + @Override + public PotionContents.Builder addCustomEffect(final PotionEffect effect) { + this.customEffects.add(CraftPotionUtil.fromBukkit(effect)); + return this; + } + + @Override + public PotionContents.Builder addCustomEffects(final List effects) { + effects.forEach(this::addCustomEffect); + return this; + } + + @Override + public PotionContents build() { + if (this.type == null && this.color == null && this.customEffects.isEmpty() && this.customName == null) { + return new PaperPotionContents(net.minecraft.world.item.alchemy.PotionContents.EMPTY); + } + + return new PaperPotionContents(new net.minecraft.world.item.alchemy.PotionContents( + Optional.ofNullable(this.type).map(CraftPotionType::bukkitToMinecraftHolder), + Optional.ofNullable(this.color).map(Color::asARGB), + new ObjectArrayList<>(this.customEffects), + Optional.ofNullable(this.customName) + )); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperRepairable.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperRepairable.java new file mode 100644 index 0000000000..96345e051c --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperRepairable.java @@ -0,0 +1,22 @@ +package io.papermc.paper.datacomponent.item; + +import io.papermc.paper.registry.RegistryKey; +import io.papermc.paper.registry.set.PaperRegistrySets; +import io.papermc.paper.registry.set.RegistryKeySet; +import org.bukkit.craftbukkit.util.Handleable; +import org.bukkit.inventory.ItemType; + +public record PaperRepairable( + net.minecraft.world.item.enchantment.Repairable impl +) implements Repairable, Handleable { + + @Override + public net.minecraft.world.item.enchantment.Repairable getHandle() { + return this.impl; + } + + @Override + public RegistryKeySet types() { + return PaperRegistrySets.convertToApi(RegistryKey.ITEM, this.impl.items()); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperResolvableProfile.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperResolvableProfile.java new file mode 100644 index 0000000000..7583a7efb4 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperResolvableProfile.java @@ -0,0 +1,105 @@ +package io.papermc.paper.datacomponent.item; + +import com.destroystokyo.paper.profile.CraftPlayerProfile; +import com.destroystokyo.paper.profile.PlayerProfile; +import com.destroystokyo.paper.profile.ProfileProperty; +import com.google.common.base.Preconditions; +import com.mojang.authlib.properties.Property; +import com.mojang.authlib.properties.PropertyMap; +import io.papermc.paper.util.MCUtil; +import java.util.Collection; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import net.minecraft.util.StringUtil; +import org.bukkit.craftbukkit.util.Handleable; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +public record PaperResolvableProfile( + net.minecraft.world.item.component.ResolvableProfile impl +) implements ResolvableProfile, Handleable { + + static PaperResolvableProfile toApi(final PlayerProfile profile) { + return new PaperResolvableProfile(new net.minecraft.world.item.component.ResolvableProfile(CraftPlayerProfile.asAuthlibCopy(profile))); + } + + @Override + public net.minecraft.world.item.component.ResolvableProfile getHandle() { + return this.impl; + } + + @Override + public @Nullable UUID uuid() { + return this.impl.id().orElse(null); + } + + @Override + public @Nullable String name() { + return this.impl.name().orElse(null); + } + + @Override + public @Unmodifiable Collection properties() { + return MCUtil.transformUnmodifiable(this.impl.properties().values(), input -> new ProfileProperty(input.name(), input.value(), input.signature())); + } + + @Override + public CompletableFuture resolve() { + return this.impl.resolve().thenApply(resolvableProfile -> CraftPlayerProfile.asBukkitCopy(resolvableProfile.gameProfile())); + } + + static final class BuilderImpl implements ResolvableProfile.Builder { + + private final PropertyMap propertyMap = new PropertyMap(); + private @Nullable String name; + private @Nullable UUID uuid; + + @Override + public ResolvableProfile.Builder name(final @Nullable String name) { + if (name != null) { + Preconditions.checkArgument(name.length() <= 16, "name cannot be more than 16 characters, was %s", name.length()); + Preconditions.checkArgument(StringUtil.isValidPlayerName(name), "name cannot include invalid characters, was %s", name); + } + this.name = name; + return this; + } + + @Override + public ResolvableProfile.Builder uuid(final @Nullable UUID uuid) { + this.uuid = uuid; + return this; + } + + @Override + public ResolvableProfile.Builder addProperty(final ProfileProperty property) { + // ProfileProperty constructor already has specific validations + final Property newProperty = new Property(property.getName(), property.getValue(), property.getSignature()); + if (!this.propertyMap.containsEntry(property.getName(), newProperty)) { // underlying map is a multimap that doesn't allow duplicate key-value pair + final int newSize = this.propertyMap.size() + 1; + Preconditions.checkArgument(newSize <= 16, "Cannot have more than 16 properties, was %s", newSize); + } + + this.propertyMap.put(property.getName(), newProperty); + return this; + } + + @Override + public ResolvableProfile.Builder addProperties(final Collection properties) { + properties.forEach(this::addProperty); + return this; + } + + @Override + public ResolvableProfile build() { + final PropertyMap shallowCopy = new PropertyMap(); + shallowCopy.putAll(this.propertyMap); + + return new PaperResolvableProfile(new net.minecraft.world.item.component.ResolvableProfile( + Optional.ofNullable(this.name), + Optional.ofNullable(this.uuid), + shallowCopy + )); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperSeededContainerLoot.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperSeededContainerLoot.java new file mode 100644 index 0000000000..1ee469b3b6 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperSeededContainerLoot.java @@ -0,0 +1,59 @@ +package io.papermc.paper.datacomponent.item; + +import io.papermc.paper.adventure.PaperAdventure; +import net.kyori.adventure.key.Key; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.storage.loot.LootTable; +import org.bukkit.craftbukkit.util.CraftNamespacedKey; +import org.bukkit.craftbukkit.util.Handleable; + +public record PaperSeededContainerLoot( + net.minecraft.world.item.component.SeededContainerLoot impl +) implements SeededContainerLoot, Handleable { + + @Override + public net.minecraft.world.item.component.SeededContainerLoot getHandle() { + return this.impl; + } + + @Override + public Key lootTable() { + return CraftNamespacedKey.fromMinecraft(this.impl.lootTable().location()); + } + + @Override + public long seed() { + return this.impl.seed(); + } + + static final class BuilderImpl implements SeededContainerLoot.Builder { + + private long seed = LootTable.RANDOMIZE_SEED; + private Key key; + + BuilderImpl(final Key key) { + this.key = key; + } + + @Override + public SeededContainerLoot.Builder lootTable(final Key key) { + this.key = key; + return this; + } + + @Override + public SeededContainerLoot.Builder seed(final long seed) { + this.seed = seed; + return this; + } + + @Override + public SeededContainerLoot build() { + return new PaperSeededContainerLoot(new net.minecraft.world.item.component.SeededContainerLoot( + ResourceKey.create(Registries.LOOT_TABLE, PaperAdventure.asVanilla(this.key)), + this.seed + )); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperSuspiciousStewEffects.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperSuspiciousStewEffects.java new file mode 100644 index 0000000000..41df23c7e7 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperSuspiciousStewEffects.java @@ -0,0 +1,58 @@ +package io.papermc.paper.datacomponent.item; + +import io.papermc.paper.potion.SuspiciousEffectEntry; +import io.papermc.paper.util.MCUtil; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import java.util.Collection; +import java.util.List; +import org.bukkit.craftbukkit.potion.CraftPotionEffectType; +import org.bukkit.craftbukkit.util.Handleable; +import org.jetbrains.annotations.Unmodifiable; + +import static io.papermc.paper.potion.SuspiciousEffectEntry.create; + +public record PaperSuspiciousStewEffects( + net.minecraft.world.item.component.SuspiciousStewEffects impl +) implements SuspiciousStewEffects, Handleable { + + @Override + public net.minecraft.world.item.component.SuspiciousStewEffects getHandle() { + return this.impl; + } + + @Override + public @Unmodifiable List effects() { + return MCUtil.transformUnmodifiable(this.impl.effects(), entry -> create(CraftPotionEffectType.minecraftHolderToBukkit(entry.effect()), entry.duration())); + } + + static final class BuilderImpl implements Builder { + + private final List effects = new ObjectArrayList<>(); + + @Override + public Builder add(final SuspiciousEffectEntry entry) { + this.effects.add(new net.minecraft.world.item.component.SuspiciousStewEffects.Entry( + org.bukkit.craftbukkit.potion.CraftPotionEffectType.bukkitToMinecraftHolder(entry.effect()), + entry.duration() + )); + return this; + } + + @Override + public Builder addAll(final Collection entries) { + entries.forEach(this::add); + return this; + } + + @Override + public SuspiciousStewEffects build() { + if (this.effects.isEmpty()) { + return new PaperSuspiciousStewEffects(net.minecraft.world.item.component.SuspiciousStewEffects.EMPTY); + } + + return new PaperSuspiciousStewEffects( + new net.minecraft.world.item.component.SuspiciousStewEffects(new ObjectArrayList<>(this.effects)) + ); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperUnbreakable.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperUnbreakable.java new file mode 100644 index 0000000000..edeb3308af --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperUnbreakable.java @@ -0,0 +1,39 @@ +package io.papermc.paper.datacomponent.item; + +import org.bukkit.craftbukkit.util.Handleable; + +public record PaperUnbreakable( + net.minecraft.world.item.component.Unbreakable impl +) implements Unbreakable, Handleable { + + @Override + public boolean showInTooltip() { + return this.impl.showInTooltip(); + } + + @Override + public Unbreakable showInTooltip(final boolean showInTooltip) { + return new PaperUnbreakable(this.impl.withTooltip(showInTooltip)); + } + + @Override + public net.minecraft.world.item.component.Unbreakable getHandle() { + return this.impl; + } + + static final class BuilderImpl implements Unbreakable.Builder { + + private boolean showInTooltip = true; + + @Override + public Unbreakable.Builder showInTooltip(final boolean showInTooltip) { + this.showInTooltip = showInTooltip; + return this; + } + + @Override + public Unbreakable build() { + return new PaperUnbreakable(new net.minecraft.world.item.component.Unbreakable(this.showInTooltip)); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperUseCooldown.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperUseCooldown.java new file mode 100644 index 0000000000..1aeab920fa --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperUseCooldown.java @@ -0,0 +1,56 @@ +package io.papermc.paper.datacomponent.item; + +import io.papermc.paper.adventure.PaperAdventure; +import java.util.Optional; +import net.kyori.adventure.key.Key; +import net.minecraft.resources.ResourceLocation; +import org.bukkit.craftbukkit.util.Handleable; +import org.jspecify.annotations.Nullable; + +public record PaperUseCooldown( + net.minecraft.world.item.component.UseCooldown impl +) implements UseCooldown, Handleable { + + @Override + public net.minecraft.world.item.component.UseCooldown getHandle() { + return this.impl; + } + + @Override + public float seconds() { + return this.impl.seconds(); + } + + @Override + public @Nullable Key cooldownGroup() { + return this.impl.cooldownGroup() + .map(PaperAdventure::asAdventure) + .orElse(null); + } + + + static final class BuilderImpl implements Builder { + + private final float seconds; + private Optional cooldownGroup = Optional.empty(); + + BuilderImpl(final float seconds) { + this.seconds = seconds; + } + + @Override + public Builder cooldownGroup(@Nullable final Key key) { + this.cooldownGroup = Optional.ofNullable(key) + .map(PaperAdventure::asVanilla); + + return this; + } + + @Override + public UseCooldown build() { + return new PaperUseCooldown( + new net.minecraft.world.item.component.UseCooldown(this.seconds, this.cooldownGroup) + ); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperUseRemainder.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperUseRemainder.java new file mode 100644 index 0000000000..c2c0450694 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperUseRemainder.java @@ -0,0 +1,20 @@ +package io.papermc.paper.datacomponent.item; + +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.craftbukkit.util.Handleable; +import org.bukkit.inventory.ItemStack; + +public record PaperUseRemainder( + net.minecraft.world.item.component.UseRemainder impl +) implements UseRemainder, Handleable { + + @Override + public net.minecraft.world.item.component.UseRemainder getHandle() { + return this.impl; + } + + @Override + public ItemStack transformInto() { + return CraftItemStack.asBukkitCopy(this.impl.convertInto()); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperWritableBookContent.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperWritableBookContent.java new file mode 100644 index 0000000000..559343a33b --- /dev/null +++ b/paper-server/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 { + + @Override + public net.minecraft.world.item.component.WritableBookContent getHandle() { + return this.impl; + } + + @Override + public @Unmodifiable List> 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> pages = new ObjectArrayList<>(); + + private static void validatePageLength(final String page) { + Preconditions.checkArgument( + page.length() <= net.minecraft.world.item.component.WritableBookContent.PAGE_EDIT_LENGTH, + "Cannot have page length more than %s, had %s", + net.minecraft.world.item.component.WritableBookContent.PAGE_EDIT_LENGTH, + page.length() + ); + } + + private static void validatePageCount(final int current, final int add) { + final int newSize = current + add; + Preconditions.checkArgument( + newSize <= net.minecraft.world.item.component.WritableBookContent.MAX_PAGES, + "Cannot have more than %s pages, had %s", + net.minecraft.world.item.component.WritableBookContent.MAX_PAGES, + newSize + ); + } + + @Override + public WritableBookContent.Builder addPage(final String page) { + validatePageLength(page); + validatePageCount(this.pages.size(), 1); + this.pages.add(Filterable.passThrough(page)); + return this; + } + + @Override + public WritableBookContent.Builder addPages(final List pages) { + validatePageCount(this.pages.size(), pages.size()); + for (final String page : pages) { + validatePageLength(page); + this.pages.add(Filterable.passThrough(page)); + } + return this; + } + + @Override + public WritableBookContent.Builder addFilteredPage(final Filtered page) { + validatePageLength(page.raw()); + if (page.filtered() != null) { + validatePageLength(page.filtered()); + } + validatePageCount(this.pages.size(), 1); + this.pages.add(new Filterable<>(page.raw(), Optional.ofNullable(page.filtered()))); + return this; + } + + @Override + public WritableBookContent.Builder addFilteredPages(final List> pages) { + validatePageCount(this.pages.size(), pages.size()); + for (final Filtered page : pages) { + validatePageLength(page.raw()); + if (page.filtered() != null) { + validatePageLength(page.filtered()); + } + this.pages.add(new Filterable<>(page.raw(), Optional.ofNullable(page.filtered()))); + } + return this; + } + + @Override + public WritableBookContent build() { + if (this.pages.isEmpty()) { + return new PaperWritableBookContent(net.minecraft.world.item.component.WritableBookContent.EMPTY); + } + + return new PaperWritableBookContent( + new net.minecraft.world.item.component.WritableBookContent(new ObjectArrayList<>(this.pages)) + ); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperWrittenBookContent.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperWrittenBookContent.java new file mode 100644 index 0000000000..037a6695bd --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/PaperWrittenBookContent.java @@ -0,0 +1,183 @@ +package io.papermc.paper.datacomponent.item; + +import com.google.common.base.Preconditions; +import io.papermc.paper.adventure.PaperAdventure; +import io.papermc.paper.text.Filtered; +import io.papermc.paper.util.MCUtil; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import java.util.List; +import java.util.Optional; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentLike; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import net.minecraft.server.network.Filterable; +import net.minecraft.util.GsonHelper; +import org.bukkit.craftbukkit.util.Handleable; +import org.jetbrains.annotations.Unmodifiable; + +import static io.papermc.paper.adventure.PaperAdventure.asAdventure; +import static io.papermc.paper.adventure.PaperAdventure.asVanilla; + +public record PaperWrittenBookContent( + net.minecraft.world.item.component.WrittenBookContent impl +) implements WrittenBookContent, Handleable { + + @Override + public net.minecraft.world.item.component.WrittenBookContent getHandle() { + return this.impl; + } + + @Override + public Filtered title() { + return Filtered.of(this.impl.title().raw(), this.impl.title().filtered().orElse(null)); + } + + @Override + public String author() { + return this.impl.author(); + } + + @Override + public int generation() { + return this.impl.generation(); + } + + @Override + public @Unmodifiable List> pages() { + return MCUtil.transformUnmodifiable( + 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> pages = new ObjectArrayList<>(); + private Filterable title; + private String author; + private int generation = 0; + private boolean resolved = false; + + BuilderImpl(final Filtered title, final String author) { + validateTitle(title.raw()); + if (title.filtered() != null) { + validateTitle(title.filtered()); + } + this.title = new Filterable<>(title.raw(), Optional.ofNullable(title.filtered())); + this.author = author; + } + + private static void validateTitle(final String title) { + Preconditions.checkArgument( + title.length() <= net.minecraft.world.item.component.WrittenBookContent.TITLE_MAX_LENGTH, + "Title cannot be longer than %s, was %s", + net.minecraft.world.item.component.WrittenBookContent.TITLE_MAX_LENGTH, + title.length() + ); + } + + private static void validatePageLength(final Component page) { + final String flagPage = GsonHelper.toStableString(GsonComponentSerializer.gson().serializeToTree(page)); + Preconditions.checkArgument( + flagPage.length() <= net.minecraft.world.item.component.WrittenBookContent.PAGE_LENGTH, + "Cannot have page length more than %s, had %s", + net.minecraft.world.item.component.WrittenBookContent.PAGE_LENGTH, + flagPage.length() + ); + } + + @Override + public WrittenBookContent.Builder title(final String title) { + validateTitle(title); + this.title = Filterable.passThrough(title); + return this; + } + + @Override + public WrittenBookContent.Builder filteredTitle(final Filtered title) { + validateTitle(title.raw()); + if (title.filtered() != null) { + validateTitle(title.filtered()); + } + this.title = new Filterable<>(title.raw(), Optional.ofNullable(title.filtered())); + return this; + } + + @Override + public WrittenBookContent.Builder author(final String author) { + this.author = author; + return this; + } + + @Override + public WrittenBookContent.Builder generation(final int generation) { + Preconditions.checkArgument( + generation >= 0 && generation <= net.minecraft.world.item.component.WrittenBookContent.MAX_GENERATION, + "generation must be between %s and %s, was %s", + 0, net.minecraft.world.item.component.WrittenBookContent.MAX_GENERATION, + generation + ); + this.generation = generation; + return this; + } + + @Override + public WrittenBookContent.Builder resolved(final boolean resolved) { + this.resolved = resolved; + return this; + } + + @Override + public WrittenBookContent.Builder addPage(final ComponentLike page) { + final Component component = page.asComponent(); + validatePageLength(component); + this.pages.add(Filterable.passThrough(asVanilla(component))); + return this; + } + + @Override + public WrittenBookContent.Builder addPages(final List pages) { + for (final ComponentLike page : pages) { + final Component component = page.asComponent(); + validatePageLength(component); + this.pages.add(Filterable.passThrough(asVanilla(component))); + } + return this; + } + + @Override + public WrittenBookContent.Builder addFilteredPage(final Filtered page) { + final Component raw = page.raw().asComponent(); + validatePageLength(raw); + Component filtered = null; + if (page.filtered() != null) { + filtered = page.filtered().asComponent(); + validatePageLength(filtered); + } + this.pages.add(new Filterable<>(asVanilla(raw), Optional.ofNullable(filtered).map(PaperAdventure::asVanilla))); + return this; + } + + @Override + public WrittenBookContent.Builder addFilteredPages(final List> pages) { + pages.forEach(this::addFilteredPage); + return this; + } + + @Override + public WrittenBookContent build() { + return new PaperWrittenBookContent(new net.minecraft.world.item.component.WrittenBookContent( + this.title, + this.author, + this.generation, + new ObjectArrayList<>(this.pages), + this.resolved + )); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/ConsumableTypesBridgeImpl.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/ConsumableTypesBridgeImpl.java new file mode 100644 index 0000000000..eab1883d69 --- /dev/null +++ b/paper-server/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; + +@ApiStatus.Internal +@NullMarked +public class ConsumableTypesBridgeImpl implements ConsumableTypesBridge { + + @Override + public ConsumeEffect.ApplyStatusEffects applyStatusEffects(final List effectList, final float probability) { + Preconditions.checkArgument(0 <= probability && probability <= 1, "probability must be between 0-1, was %s", probability); + return new PaperApplyStatusEffects( + new net.minecraft.world.item.consume_effects.ApplyStatusEffectsConsumeEffect( + new ArrayList<>(Lists.transform(effectList, CraftPotionUtil::fromBukkit)), + probability + ) + ); + } + + @Override + public ConsumeEffect.RemoveStatusEffects removeStatusEffects(final RegistryKeySet 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/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperApplyStatusEffects.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperApplyStatusEffects.java new file mode 100644 index 0000000000..0d2a4ba560 --- /dev/null +++ b/paper-server/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 { + + @Override + public List 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/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperClearAllStatusEffects.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperClearAllStatusEffects.java new file mode 100644 index 0000000000..2afcbbbeb4 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperClearAllStatusEffects.java @@ -0,0 +1,11 @@ +package io.papermc.paper.datacomponent.item.consumable; + +public record PaperClearAllStatusEffects( + net.minecraft.world.item.consume_effects.ClearAllStatusEffectsConsumeEffect impl +) implements ConsumeEffect.ClearAllStatusEffects, PaperConsumableEffectImpl { + + @Override + public net.minecraft.world.item.consume_effects.ClearAllStatusEffectsConsumeEffect getHandle() { + return this.impl; + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffectImpl.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffectImpl.java new file mode 100644 index 0000000000..05ede1d3f5 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffectImpl.java @@ -0,0 +1,7 @@ +package io.papermc.paper.datacomponent.item.consumable; + +import net.minecraft.world.item.consume_effects.ConsumeEffect; +import org.bukkit.craftbukkit.util.Handleable; + +public interface PaperConsumableEffectImpl extends Handleable { +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffects.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperConsumableEffects.java new file mode 100644 index 0000000000..ff07939ef0 --- /dev/null +++ b/paper-server/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/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperPlaySound.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperPlaySound.java new file mode 100644 index 0000000000..26a8ee292b --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperPlaySound.java @@ -0,0 +1,20 @@ +package io.papermc.paper.datacomponent.item.consumable; + +import io.papermc.paper.adventure.PaperAdventure; +import net.kyori.adventure.key.Key; +import net.minecraft.world.item.consume_effects.PlaySoundConsumeEffect; + +public record PaperPlaySound( + PlaySoundConsumeEffect impl +) implements ConsumeEffect.PlaySound, PaperConsumableEffectImpl { + + @Override + public Key sound() { + return PaperAdventure.asAdventure(this.impl.sound().value().location()); + } + + @Override + public PlaySoundConsumeEffect getHandle() { + return this.impl; + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperRemoveStatusEffects.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperRemoveStatusEffects.java new file mode 100644 index 0000000000..20e09c6eba --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperRemoveStatusEffects.java @@ -0,0 +1,21 @@ +package io.papermc.paper.datacomponent.item.consumable; + +import io.papermc.paper.registry.RegistryKey; +import io.papermc.paper.registry.set.PaperRegistrySets; +import io.papermc.paper.registry.set.RegistryKeySet; +import org.bukkit.potion.PotionEffectType; + +public record PaperRemoveStatusEffects( + net.minecraft.world.item.consume_effects.RemoveStatusEffectsConsumeEffect impl +) implements ConsumeEffect.RemoveStatusEffects, PaperConsumableEffectImpl { + + @Override + public RegistryKeySet removeEffects() { + return PaperRegistrySets.convertToApi(RegistryKey.MOB_EFFECT, this.impl.effects()); + } + + @Override + public net.minecraft.world.item.consume_effects.RemoveStatusEffectsConsumeEffect getHandle() { + return this.impl; + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperTeleportRandomly.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperTeleportRandomly.java new file mode 100644 index 0000000000..c21889e998 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/PaperTeleportRandomly.java @@ -0,0 +1,15 @@ +package io.papermc.paper.datacomponent.item.consumable; + +public record PaperTeleportRandomly( + net.minecraft.world.item.consume_effects.TeleportRandomlyConsumeEffect impl +) implements ConsumeEffect.TeleportRandomly, PaperConsumableEffectImpl { + @Override + public float diameter() { + return this.impl.diameter(); + } + + @Override + public net.minecraft.world.item.consume_effects.TeleportRandomlyConsumeEffect getHandle() { + return this.impl; + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/package-info.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/consumable/package-info.java new file mode 100644 index 0000000000..af6720a49a --- /dev/null +++ b/paper-server/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/paper-server/src/main/java/io/papermc/paper/datacomponent/item/package-info.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/item/package-info.java new file mode 100644 index 0000000000..02a6902566 --- /dev/null +++ b/paper-server/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/paper-server/src/main/java/io/papermc/paper/datacomponent/package-info.java b/paper-server/src/main/java/io/papermc/paper/datacomponent/package-info.java new file mode 100644 index 0000000000..62aa1061c3 --- /dev/null +++ b/paper-server/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/paper-server/src/main/java/io/papermc/paper/registry/PaperRegistries.java b/paper-server/src/main/java/io/papermc/paper/registry/PaperRegistries.java index dcca8bbba5..3ebc3dbc86 100644 --- a/paper-server/src/main/java/io/papermc/paper/registry/PaperRegistries.java +++ b/paper-server/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.DataComponentTypes; +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; @@ -93,6 +95,7 @@ public final class PaperRegistries { start(Registries.ATTRIBUTE, RegistryKey.ATTRIBUTE).craft(Attribute.class, CraftAttribute::new).build(), start(Registries.FLUID, RegistryKey.FLUID).craft(Fluid.class, CraftFluid::new).build(), start(Registries.SOUND_EVENT, RegistryKey.SOUND_EVENT).craft(Sound.class, CraftSound::new).build(), + start(Registries.DATA_COMPONENT_TYPE, RegistryKey.DATA_COMPONENT_TYPE).craft(DataComponentTypes.class, PaperDataComponentType::of).build(), // data-drivens start(Registries.BIOME, RegistryKey.BIOME).craft(Biome.class, CraftBiome::new).build().delayed(), diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java index 756c73a401..78975412da 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java +++ b/paper-server/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; } - // Paper end - return level; + ItemEnchantments itemEnchantments = this.handle.getOrDefault(DataComponents.ENCHANTMENTS, ItemEnchantments.EMPTY); + if (itemEnchantments.isEmpty()) { + return 0; + } + + Holder removedEnchantment = CraftEnchantment.bukkitToMinecraftHolder(ench); + if (itemEnchantments.keySet().contains(removedEnchantment)) { + int previousLevel = itemEnchantments.getLevel(removedEnchantment); + + ItemEnchantments.Mutable mutable = new ItemEnchantments.Mutable(itemEnchantments); // data component api doesn't really support mutable things once already set yet + mutable.removeIf(enchantment -> enchantment.equals(removedEnchantment)); + this.handle.set(DataComponents.ENCHANTMENTS, mutable.toImmutable()); + return previousLevel; + } + + return 0; + // Paper end } @Override @@ -324,7 +335,13 @@ public final class CraftItemStack extends ItemStack { @Override public Map getEnchantments() { - return this.hasItemMeta() ? this.getItemMeta().getEnchants() : ImmutableMap.of(); // Paper - use Item Meta + // Paper start + io.papermc.paper.datacomponent.item.ItemEnchantments itemEnchantments = this.getData(io.papermc.paper.datacomponent.DataComponentTypes.ENCHANTMENTS); // empty constant might be useful here + if (itemEnchantments == null) { + return java.util.Collections.emptyMap(); + } + return itemEnchantments.enchantments(); + // Paper end } static Map getEnchantments(net.minecraft.world.item.ItemStack item) { @@ -526,4 +543,119 @@ public final class CraftItemStack extends ItemStack { return this.pdcView; } // Paper end - pdc + // Paper start - data component API + @Override + public T getData(final io.papermc.paper.datacomponent.DataComponentType.Valued type) { + if (this.isEmpty()) { + return null; + } + return io.papermc.paper.datacomponent.PaperDataComponentType.convertDataComponentValue(this.handle.getComponents(), (io.papermc.paper.datacomponent.PaperDataComponentType.ValuedImpl) type); + } + + @Override + public boolean hasData(final io.papermc.paper.datacomponent.DataComponentType type) { + if (this.isEmpty()) { + return false; + } + return this.handle.has(io.papermc.paper.datacomponent.PaperDataComponentType.bukkitToMinecraft(type)); + } + + @Override + public java.util.Set getDataTypes() { + if (this.isEmpty()) { + return java.util.Collections.emptySet(); + } + return io.papermc.paper.datacomponent.PaperDataComponentType.minecraftToBukkit(this.handle.getComponents().keySet()); + } + + @Override + public void setData(final io.papermc.paper.datacomponent.DataComponentType.Valued type, final T value) { + Preconditions.checkArgument(value != null, "value cannot be null"); + if (this.isEmpty()) { + return; + } + this.setDataInternal((io.papermc.paper.datacomponent.PaperDataComponentType.ValuedImpl) type, value); + } + + @Override + public void setData(final io.papermc.paper.datacomponent.DataComponentType.NonValued type) { + if (this.isEmpty()) { + return; + } + this.setDataInternal((io.papermc.paper.datacomponent.PaperDataComponentType.NonValuedImpl) type, null); + } + + private void setDataInternal(final io.papermc.paper.datacomponent.PaperDataComponentType 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 void resetData(final io.papermc.paper.datacomponent.PaperDataComponentType type) { + final net.minecraft.core.component.DataComponentType 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 exclude, final boolean ignoreCount) { + // Extracted from base equals + final CraftItemStack craftStack = getCraftStack(item); + if (this.handle == craftStack.handle) return true; + if (this.handle == null || craftStack.handle == null) return false; + if (this.handle.isEmpty() && craftStack.handle.isEmpty()) return true; + + net.minecraft.world.item.ItemStack left = this.handle; + net.minecraft.world.item.ItemStack right = craftStack.handle; + if (!ignoreCount && left.getCount() != right.getCount()) { + return false; + } + if (!left.is(right.getItem())) { + return false; + } + + // It can be assumed that the prototype is equal since the type is the same. This way all we need to check is the patch + + // Fast path when excluded types is empty + if (exclude.isEmpty()) { + return left.getComponentsPatch().equals(right.getComponentsPatch()); + } + + // Collect all the NMS types into a set + java.util.Set> skippingTypes = new java.util.HashSet<>(exclude.size()); + for (io.papermc.paper.datacomponent.DataComponentType api : exclude) { + skippingTypes.add(io.papermc.paper.datacomponent.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/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java index 1b57649d0d..b0da057ce5 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemType.java @@ -150,7 +150,7 @@ public class CraftItemType implements ItemType.Typed, 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 implements ItemType.Typed, Han return rarity == null ? null : org.bukkit.inventory.ItemRarity.valueOf(rarity.name()); } // Paper end - expand ItemRarity API + // Paper start - data component API + @Override + public T getDefaultData(final io.papermc.paper.datacomponent.DataComponentType.Valued type) { + return io.papermc.paper.datacomponent.PaperDataComponentType.convertDataComponentValue(this.item.components(), ((io.papermc.paper.datacomponent.PaperDataComponentType.ValuedImpl) 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 getDefaultDataTypes() { + return io.papermc.paper.datacomponent.PaperDataComponentType.minecraftToBukkit(this.item.components().keySet()); + } + // Paper end - data component API } diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java index a944803771..82cb8cd163 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java +++ b/paper-server/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/paper-server/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.ItemComponentTypesBridge b/paper-server/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.ItemComponentTypesBridge new file mode 100644 index 0000000000..0fd276c2fd --- /dev/null +++ b/paper-server/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.ItemComponentTypesBridge @@ -0,0 +1 @@ +io.papermc.paper.datacomponent.item.ItemComponentTypesBridgesImpl diff --git a/paper-server/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.consumable.ConsumableTypesBridge b/paper-server/src/main/resources/META-INF/services/io.papermc.paper.datacomponent.item.consumable.ConsumableTypesBridge new file mode 100644 index 0000000000..852ab09718 --- /dev/null +++ b/paper-server/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/paper-server/src/test/java/io/papermc/paper/datacomponent/DataComponentTypesTest.java b/paper-server/src/test/java/io/papermc/paper/datacomponent/DataComponentTypesTest.java new file mode 100644 index 0000000000..1d707114f5 --- /dev/null +++ b/paper-server/src/test/java/io/papermc/paper/datacomponent/DataComponentTypesTest.java @@ -0,0 +1,58 @@ +package io.papermc.paper.datacomponent; + +import com.google.common.collect.Collections2; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import org.bukkit.craftbukkit.util.CraftNamespacedKey; +import org.bukkit.support.RegistryHelper; +import org.bukkit.support.environment.AllFeatures; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import java.lang.reflect.Field; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +@AllFeatures +public class DataComponentTypesTest { + + private static final Set NOT_IN_API = Set.of( + ResourceLocation.parse("custom_data"), + ResourceLocation.parse("entity_data"), + ResourceLocation.parse("bees"), + ResourceLocation.parse("debug_stick_state"), + ResourceLocation.parse("block_entity_data"), + ResourceLocation.parse("bucket_entity_data"), + ResourceLocation.parse("lock"), + ResourceLocation.parse("creative_slot_lock") + ); + + @Test + public void testAllDataComponentsAreMapped() throws IllegalAccessException { + final Set vanillaDataComponentTypes = new ObjectOpenHashSet<>( + RegistryHelper.getRegistry() + .lookupOrThrow(Registries.DATA_COMPONENT_TYPE) + .keySet() + ); + + for (final Field declaredField : DataComponentTypes.class.getDeclaredFields()) { + if (!DataComponentType.class.isAssignableFrom(declaredField.getType())) continue; + + final DataComponentType dataComponentType = (DataComponentType) declaredField.get(null); + if (!vanillaDataComponentTypes.remove(CraftNamespacedKey.toMinecraft(dataComponentType.getKey()))) { + Assertions.fail("API defined component type " + dataComponentType.key().asMinimalString() + " is unknown to vanilla registry"); + } + } + + if (!vanillaDataComponentTypes.containsAll(NOT_IN_API)) { + Assertions.fail("API defined data components that were marked as not-yet-implemented: " + NOT_IN_API.stream().filter(Predicate.not(vanillaDataComponentTypes::contains)).map(ResourceLocation::toString).collect(Collectors.joining(", "))); + } + + vanillaDataComponentTypes.removeAll(NOT_IN_API); + if (!vanillaDataComponentTypes.isEmpty()) { + Assertions.fail("API did not define following vanilla data component types: " + String.join(", ", Collections2.transform(vanillaDataComponentTypes, ResourceLocation::toString))); + } + } + +} diff --git a/paper-server/src/test/java/io/papermc/paper/item/ItemStackDataComponentEqualsTest.java b/paper-server/src/test/java/io/papermc/paper/item/ItemStackDataComponentEqualsTest.java new file mode 100644 index 0000000000..4ee0491763 --- /dev/null +++ b/paper-server/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/paper-server/src/test/java/io/papermc/paper/item/ItemStackDataComponentTest.java b/paper-server/src/test/java/io/papermc/paper/item/ItemStackDataComponentTest.java new file mode 100644 index 0000000000..00133403f8 --- /dev/null +++ b/paper-server/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 list = List.of(Component.text("1"), Component.text("2")); + testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.LORE, ItemLore.lore().lines(list).build(), ItemLore::lines, ItemMeta.class, ItemMeta::lore, ItemMeta::lore); + } + + @Test + void testItemRarity() { + testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.RARITY, ItemRarity.RARE, ItemMeta.class, ItemMeta::getRarity, ItemMeta::setRarity); + } + + @Test + void testItemEnchantments() { + final ItemStack stack = new ItemStack(Material.STONE); + Map enchantmentIntegerMap = Map.of(Enchantment.SOUL_SPEED, 1); + stack.setData(DataComponentTypes.ENCHANTMENTS, ItemEnchantments.itemEnchantments(enchantmentIntegerMap, false)); + + Assertions.assertTrue(stack.getItemMeta().hasItemFlag(ItemFlag.HIDE_ENCHANTS)); + Assertions.assertEquals(1, stack.getItemMeta().getEnchantLevel(Enchantment.SOUL_SPEED)); + Assertions.assertEquals(stack.getItemMeta().getEnchants(), enchantmentIntegerMap); + stack.unsetData(DataComponentTypes.ENCHANTMENTS); + Assertions.assertTrue(stack.getItemMeta().getEnchants().isEmpty()); + } + + @Test + void testItemAttributes() { + final ItemStack stack = new ItemStack(Material.STONE); + AttributeModifier modifier = new AttributeModifier(NamespacedKey.minecraft("test"), 5, AttributeModifier.Operation.ADD_NUMBER, EquipmentSlotGroup.ANY); + stack.setData(DataComponentTypes.ATTRIBUTE_MODIFIERS, ItemAttributeModifiers.itemAttributes().showInTooltip(false).addModifier(Attribute.ATTACK_DAMAGE, modifier).build()); + + Assertions.assertTrue(stack.getItemMeta().hasItemFlag(ItemFlag.HIDE_ATTRIBUTES)); + Assertions.assertEquals(modifier, ((List) stack.getItemMeta().getAttributeModifiers(Attribute.ATTACK_DAMAGE)).getFirst()); + stack.unsetData(DataComponentTypes.ATTRIBUTE_MODIFIERS); + Assertions.assertNull(stack.getItemMeta().getAttributeModifiers()); + } + + @Test + void 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 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 void testWithMeta(final ItemStack stack, final DataComponentType.Valued type, final T value, final Class metaType, final Function metaGetter, final BiConsumer metaSetter) { + testWithMeta(stack, type, value, Function.identity(), metaType, metaGetter, metaSetter); + } + + private static void testWithMeta(final ItemStack stack, final DataComponentType.Valued type, final T value, Function mapper, final Class metaType, final Function metaGetter, final BiConsumer metaSetter) { + ItemStack original = stack.clone(); + stack.setData(type, value); + + Assertions.assertEquals(value, stack.getData(type)); + + final ItemMeta meta = stack.getItemMeta(); + final M typedMeta = Assertions.assertInstanceOf(metaType, meta); + + Assertions.assertEquals(metaGetter.apply(typedMeta), mapper.apply(value)); + + // SETTING + metaSetter.accept(typedMeta, mapper.apply(value)); + original.setItemMeta(typedMeta); + Assertions.assertEquals(value, original.getData(type)); + } + + private static void testWithMeta(final ItemStack stack, final DataComponentType.NonValued type, final boolean value, final Class metaType, final Function metaGetter, final BiConsumer metaSetter) { + ItemStack original = stack.clone(); + stack.setData(type); + + Assertions.assertEquals(value, stack.hasData(type)); + + final ItemMeta meta = stack.getItemMeta(); + final M typedMeta = Assertions.assertInstanceOf(metaType, meta); + + Assertions.assertEquals(metaGetter.apply(typedMeta), value); + + // SETTING + metaSetter.accept(typedMeta, value); + original.setItemMeta(typedMeta); + Assertions.assertEquals(value, original.hasData(type)); + } +} diff --git a/paper-server/src/test/java/io/papermc/paper/item/MetaComparisonTest.java b/paper-server/src/test/java/io/papermc/paper/item/MetaComparisonTest.java new file mode 100644 index 0000000000..7cda799807 --- /dev/null +++ b/paper-server/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 set, Consumer get) { + ItemMeta craftMeta = CraftItemStack.getItemMeta(CraftItemStack.asNMSCopy(itemStack)); // TODO: This should be converted to use the old meta when this is added. + ItemMeta paperMeta = CraftItemStack.getItemMeta(CraftItemStack.asNMSCopy(itemStack)); + // Test craft meta + set.accept(craftMeta); + get.accept(craftMeta); + + // Test paper meta + set.accept(paperMeta); + get.accept(paperMeta); + } +} diff --git a/paper-server/src/test/java/org/bukkit/PerMaterialTest.java b/paper-server/src/test/java/org/bukkit/PerMaterialTest.java index 629fccec14..6961730365 100644 --- a/paper-server/src/test/java/org/bukkit/PerMaterialTest.java +++ b/paper-server/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/paper-server/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java b/paper-server/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java index b717a5ffa5..dc5fadb3d9 100644 --- a/paper-server/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java +++ b/paper-server/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