DataComponent API

Exposes the data component logic used by vanilla ItemStack to API
consumers as a version-specific API.
The types and methods introduced by this patch do not follow the general
API deprecation contracts and will be adapted to each new minecraft
release without backwards compatibility measures.

== AT ==
public net/minecraft/world/item/component/ItemContainerContents MAX_SIZE
public net/minecraft/world/item/component/ItemContainerContents items
This commit is contained in:
Owen1212055
2024-04-28 19:53:01 -04:00
parent 2a20fde332
commit 25c25923e9
64 changed files with 4397 additions and 29 deletions

View File

@@ -0,0 +1,58 @@
package io.papermc.paper.datacomponent;
import com.google.common.collect.Collections2;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import org.bukkit.craftbukkit.util.CraftNamespacedKey;
import org.bukkit.support.RegistryHelper;
import org.bukkit.support.environment.AllFeatures;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@AllFeatures
public class DataComponentTypesTest {
private static final Set<ResourceLocation> NOT_IN_API = Set.of(
ResourceLocation.parse("custom_data"),
ResourceLocation.parse("entity_data"),
ResourceLocation.parse("bees"),
ResourceLocation.parse("debug_stick_state"),
ResourceLocation.parse("block_entity_data"),
ResourceLocation.parse("bucket_entity_data"),
ResourceLocation.parse("lock"),
ResourceLocation.parse("creative_slot_lock")
);
@Test
public void testAllDataComponentsAreMapped() throws IllegalAccessException {
final Set<ResourceLocation> vanillaDataComponentTypes = new ObjectOpenHashSet<>(
RegistryHelper.getRegistry()
.lookupOrThrow(Registries.DATA_COMPONENT_TYPE)
.keySet()
);
for (final Field declaredField : DataComponentTypes.class.getDeclaredFields()) {
if (!DataComponentType.class.isAssignableFrom(declaredField.getType())) continue;
final DataComponentType dataComponentType = (DataComponentType) declaredField.get(null);
if (!vanillaDataComponentTypes.remove(CraftNamespacedKey.toMinecraft(dataComponentType.getKey()))) {
Assertions.fail("API defined component type " + dataComponentType.key().asMinimalString() + " is unknown to vanilla registry");
}
}
if (!vanillaDataComponentTypes.containsAll(NOT_IN_API)) {
Assertions.fail("API defined data components that were marked as not-yet-implemented: " + NOT_IN_API.stream().filter(Predicate.not(vanillaDataComponentTypes::contains)).map(ResourceLocation::toString).collect(Collectors.joining(", ")));
}
vanillaDataComponentTypes.removeAll(NOT_IN_API);
if (!vanillaDataComponentTypes.isEmpty()) {
Assertions.fail("API did not define following vanilla data component types: " + String.join(", ", Collections2.transform(vanillaDataComponentTypes, ResourceLocation::toString)));
}
}
}

View File

@@ -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));
}
}

View File

@@ -0,0 +1,416 @@
package io.papermc.paper.item;
import io.papermc.paper.datacomponent.DataComponentType;
import io.papermc.paper.datacomponent.DataComponentTypes;
import io.papermc.paper.datacomponent.item.ChargedProjectiles;
import io.papermc.paper.datacomponent.item.CustomModelData;
import io.papermc.paper.datacomponent.item.DyedItemColor;
import io.papermc.paper.datacomponent.item.Fireworks;
import io.papermc.paper.datacomponent.item.FoodProperties;
import io.papermc.paper.datacomponent.item.ItemArmorTrim;
import io.papermc.paper.datacomponent.item.ItemAttributeModifiers;
import io.papermc.paper.datacomponent.item.ItemEnchantments;
import io.papermc.paper.datacomponent.item.ItemLore;
import io.papermc.paper.datacomponent.item.JukeboxPlayable;
import io.papermc.paper.datacomponent.item.MapId;
import io.papermc.paper.datacomponent.item.MapItemColor;
import io.papermc.paper.datacomponent.item.PotDecorations;
import io.papermc.paper.datacomponent.item.Tool;
import io.papermc.paper.datacomponent.item.Unbreakable;
import io.papermc.paper.registry.RegistryAccess;
import io.papermc.paper.registry.RegistryKey;
import io.papermc.paper.registry.set.RegistrySet;
import io.papermc.paper.registry.tag.TagKey;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Function;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.util.TriState;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.item.EitherHolder;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.JukeboxSongs;
import org.bukkit.Color;
import org.bukkit.FireworkEffect;
import org.bukkit.JukeboxSong;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeModifier;
import org.bukkit.block.BlockState;
import org.bukkit.block.BlockType;
import org.bukkit.block.DecoratedPot;
import org.bukkit.craftbukkit.inventory.CraftItemStack;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.EquipmentSlotGroup;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemRarity;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.ItemType;
import org.bukkit.inventory.meta.ArmorMeta;
import org.bukkit.inventory.meta.BlockStateMeta;
import org.bukkit.inventory.meta.CrossbowMeta;
import org.bukkit.inventory.meta.Damageable;
import org.bukkit.inventory.meta.FireworkMeta;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.KnowledgeBookMeta;
import org.bukkit.inventory.meta.LeatherArmorMeta;
import org.bukkit.inventory.meta.MapMeta;
import org.bukkit.inventory.meta.Repairable;
import org.bukkit.inventory.meta.components.FoodComponent;
import org.bukkit.inventory.meta.components.JukeboxPlayableComponent;
import org.bukkit.inventory.meta.components.ToolComponent;
import org.bukkit.inventory.meta.trim.ArmorTrim;
import org.bukkit.inventory.meta.trim.TrimMaterial;
import org.bukkit.inventory.meta.trim.TrimPattern;
import org.bukkit.support.RegistryHelper;
import org.bukkit.support.environment.AllFeatures;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@AllFeatures
class ItemStackDataComponentTest {
@Test
void testMaxStackSize() {
testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.MAX_STACK_SIZE, 32, ItemMeta.class, ItemMeta::getMaxStackSize, ItemMeta::setMaxStackSize);
}
@Test
void testMaxDamage() {
testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.MAX_DAMAGE, 120, Damageable.class, Damageable::getMaxDamage, Damageable::setMaxDamage);
}
@Test
void testDamage() {
testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.DAMAGE, 120, Damageable.class, Damageable::getDamage, Damageable::setDamage);
}
@Test
void testUnbreakable() {
final ItemStack stack = new ItemStack(Material.STONE);
stack.setData(DataComponentTypes.UNBREAKABLE, Unbreakable.unbreakable().showInTooltip(false).build());
Assertions.assertTrue(stack.getItemMeta().isUnbreakable());
Assertions.assertTrue(stack.getItemMeta().getItemFlags().contains(ItemFlag.HIDE_UNBREAKABLE));
stack.unsetData(DataComponentTypes.UNBREAKABLE);
Assertions.assertFalse(stack.getItemMeta().isUnbreakable());
}
@Test
void testHideAdditionalTooltip() {
final ItemStack stack = new ItemStack(Material.STONE);
stack.setData(DataComponentTypes.HIDE_ADDITIONAL_TOOLTIP);
Assertions.assertTrue(stack.getItemMeta().getItemFlags().contains(ItemFlag.HIDE_ADDITIONAL_TOOLTIP));
stack.unsetData(DataComponentTypes.HIDE_ADDITIONAL_TOOLTIP);
Assertions.assertFalse(stack.getItemMeta().getItemFlags().contains(ItemFlag.HIDE_ADDITIONAL_TOOLTIP));
}
@Test
void testHideTooltip() {
ItemStack stack = new ItemStack(Material.STONE);
stack.setData(DataComponentTypes.HIDE_TOOLTIP);
Assertions.assertEquals(stack.getItemMeta().isHideTooltip(), stack.hasData(DataComponentTypes.HIDE_TOOLTIP));
Assertions.assertTrue(stack.getItemMeta().isHideTooltip());
stack.unsetData(DataComponentTypes.HIDE_TOOLTIP);
Assertions.assertFalse(stack.getItemMeta().isHideTooltip());
stack = new ItemStack(Material.STONE);
stack.unsetData(DataComponentTypes.HIDE_TOOLTIP);
Assertions.assertFalse(stack.getItemMeta().isHideTooltip());
Assertions.assertEquals(stack.getItemMeta().isHideTooltip(), stack.hasData(DataComponentTypes.HIDE_TOOLTIP));
}
@Test
void testRepairCost() {
final ItemStack stack = new ItemStack(Material.STONE);
testWithMeta(stack, DataComponentTypes.REPAIR_COST, 120, Repairable.class, Repairable::getRepairCost, Repairable::setRepairCost);
}
@Test
void testCustomName() {
testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.CUSTOM_NAME, Component.text("HELLO!!!!!!"), ItemMeta.class, ItemMeta::displayName, ItemMeta::displayName);
}
@Test
void testItemName() {
testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.ITEM_NAME, Component.text("HELLO!!!!!! ITEM NAME"), ItemMeta.class, ItemMeta::itemName, ItemMeta::itemName);
}
@Test
void testItemLore() {
List<Component> list = List.of(Component.text("1"), Component.text("2"));
testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.LORE, ItemLore.lore().lines(list).build(), ItemLore::lines, ItemMeta.class, ItemMeta::lore, ItemMeta::lore);
}
@Test
void testItemRarity() {
testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.RARITY, ItemRarity.RARE, ItemMeta.class, ItemMeta::getRarity, ItemMeta::setRarity);
}
@Test
void testItemEnchantments() {
final ItemStack stack = new ItemStack(Material.STONE);
Map<Enchantment, Integer> enchantmentIntegerMap = Map.of(Enchantment.SOUL_SPEED, 1);
stack.setData(DataComponentTypes.ENCHANTMENTS, ItemEnchantments.itemEnchantments(enchantmentIntegerMap, false));
Assertions.assertTrue(stack.getItemMeta().hasItemFlag(ItemFlag.HIDE_ENCHANTS));
Assertions.assertEquals(1, stack.getItemMeta().getEnchantLevel(Enchantment.SOUL_SPEED));
Assertions.assertEquals(stack.getItemMeta().getEnchants(), enchantmentIntegerMap);
stack.unsetData(DataComponentTypes.ENCHANTMENTS);
Assertions.assertTrue(stack.getItemMeta().getEnchants().isEmpty());
}
@Test
void testItemAttributes() {
final ItemStack stack = new ItemStack(Material.STONE);
AttributeModifier modifier = new AttributeModifier(NamespacedKey.minecraft("test"), 5, AttributeModifier.Operation.ADD_NUMBER, EquipmentSlotGroup.ANY);
stack.setData(DataComponentTypes.ATTRIBUTE_MODIFIERS, ItemAttributeModifiers.itemAttributes().showInTooltip(false).addModifier(Attribute.ATTACK_DAMAGE, modifier).build());
Assertions.assertTrue(stack.getItemMeta().hasItemFlag(ItemFlag.HIDE_ATTRIBUTES));
Assertions.assertEquals(modifier, ((List<AttributeModifier>) stack.getItemMeta().getAttributeModifiers(Attribute.ATTACK_DAMAGE)).getFirst());
stack.unsetData(DataComponentTypes.ATTRIBUTE_MODIFIERS);
Assertions.assertNull(stack.getItemMeta().getAttributeModifiers());
}
@Test
void testLegacyCustomModelData() {
testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.CUSTOM_MODEL_DATA, CustomModelData.customModelData().addFloat(1).build(), customModelData -> customModelData.floats().get(0).intValue(), ItemMeta.class, ItemMeta::getCustomModelData, ItemMeta::setCustomModelData);
}
@Test
void testEnchantmentGlintOverride() {
testWithMeta(new ItemStack(Material.STONE), DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, true, ItemMeta.class, ItemMeta::getEnchantmentGlintOverride, ItemMeta::setEnchantmentGlintOverride);
}
@Test
void testFood() {
FoodProperties properties = FoodProperties.food()
.canAlwaysEat(true)
.saturation(1.3F)
.nutrition(1)
.build();
final ItemStack stack = new ItemStack(Material.CROSSBOW);
stack.setData(DataComponentTypes.FOOD, properties);
ItemMeta meta = stack.getItemMeta();
FoodComponent component = meta.getFood();
Assertions.assertEquals(properties.canAlwaysEat(), component.canAlwaysEat());
Assertions.assertEquals(properties.saturation(), component.getSaturation());
Assertions.assertEquals(properties.nutrition(), component.getNutrition());
stack.unsetData(DataComponentTypes.FOOD);
meta = stack.getItemMeta();
Assertions.assertFalse(meta.hasFood());
}
@Test
void testTool() {
Tool properties = Tool.tool()
.damagePerBlock(1)
.defaultMiningSpeed(2F)
.addRules(List.of(
Tool.rule(
RegistrySet.keySetFromValues(RegistryKey.BLOCK, List.of(BlockType.STONE, BlockType.GRAVEL)),
2F,
TriState.TRUE
),
Tool.rule(
RegistryAccess.registryAccess().getRegistry(RegistryKey.BLOCK).getTag(TagKey.create(RegistryKey.BLOCK, NamespacedKey.minecraft("bamboo_blocks"))),
2F,
TriState.TRUE
)
))
.build();
final ItemStack stack = new ItemStack(Material.CROSSBOW);
stack.setData(DataComponentTypes.TOOL, properties);
ItemMeta meta = stack.getItemMeta();
ToolComponent component = meta.getTool();
Assertions.assertEquals(properties.damagePerBlock(), component.getDamagePerBlock());
Assertions.assertEquals(properties.defaultMiningSpeed(), component.getDefaultMiningSpeed());
int idx = 0;
for (ToolComponent.ToolRule effect : component.getRules()) {
Assertions.assertEquals(properties.rules().get(idx).speed(), effect.getSpeed());
Assertions.assertEquals(properties.rules().get(idx).correctForDrops().toBoolean(), effect.isCorrectForDrops());
Assertions.assertEquals(properties.rules().get(idx).blocks().resolve(Registry.BLOCK), effect.getBlocks().stream().map(Material::asBlockType).toList());
idx++;
}
stack.unsetData(DataComponentTypes.TOOL);
meta = stack.getItemMeta();
Assertions.assertFalse(meta.hasTool());
}
@Test
void testJukeboxPlayable() {
JukeboxPlayable properties = JukeboxPlayable.jukeboxPlayable(JukeboxSong.MALL).build();
final ItemStack stack = new ItemStack(Material.BEEF);
stack.setData(DataComponentTypes.JUKEBOX_PLAYABLE, properties);
ItemMeta meta = stack.getItemMeta();
JukeboxPlayableComponent component = meta.getJukeboxPlayable();
Assertions.assertEquals(properties.jukeboxSong(), component.getSong());
stack.unsetData(DataComponentTypes.JUKEBOX_PLAYABLE);
meta = stack.getItemMeta();
Assertions.assertFalse(meta.hasJukeboxPlayable());
}
@Test
void testDyedColor() {
final ItemStack stack = new ItemStack(Material.LEATHER_CHESTPLATE);
Color color = Color.BLUE;
stack.setData(DataComponentTypes.DYED_COLOR, DyedItemColor.dyedItemColor(color, false));
Assertions.assertTrue(stack.getItemMeta().hasItemFlag(ItemFlag.HIDE_DYE));
Assertions.assertEquals(color, ((LeatherArmorMeta) stack.getItemMeta()).getColor());
stack.unsetData(DataComponentTypes.DYED_COLOR);
Assertions.assertFalse(((LeatherArmorMeta) stack.getItemMeta()).isDyed());
}
@Test
void testMapColor() {
testWithMeta(new ItemStack(Material.FILLED_MAP), DataComponentTypes.MAP_COLOR, MapItemColor.mapItemColor().color(Color.BLUE).build(), MapItemColor::color, MapMeta.class, MapMeta::getColor, MapMeta::setColor);
}
@Test
void testMapId() {
testWithMeta(new ItemStack(Material.FILLED_MAP), DataComponentTypes.MAP_ID, MapId.mapId(1), MapId::id, MapMeta.class, MapMeta::getMapId, MapMeta::setMapId);
}
@Test
void testFireworks() {
testWithMeta(new ItemStack(Material.FIREWORK_ROCKET), DataComponentTypes.FIREWORKS, Fireworks.fireworks(List.of(FireworkEffect.builder().build()), 1), Fireworks::effects, FireworkMeta.class, FireworkMeta::getEffects, (fireworkMeta, effects) -> {
fireworkMeta.clearEffects();
fireworkMeta.addEffects(effects);
});
testWithMeta(new ItemStack(Material.FIREWORK_ROCKET), DataComponentTypes.FIREWORKS, Fireworks.fireworks(List.of(FireworkEffect.builder().build()), 1), Fireworks::flightDuration, FireworkMeta.class, FireworkMeta::getPower, FireworkMeta::setPower);
}
@Test
void testTrim() {
final ItemStack stack = new ItemStack(Material.LEATHER_CHESTPLATE);
ItemArmorTrim armorTrim = ItemArmorTrim.itemArmorTrim(new ArmorTrim(TrimMaterial.AMETHYST, TrimPattern.BOLT), false);
stack.setData(DataComponentTypes.TRIM, armorTrim);
Assertions.assertTrue(stack.getItemMeta().hasItemFlag(ItemFlag.HIDE_ARMOR_TRIM));
Assertions.assertEquals(armorTrim.armorTrim(), ((ArmorMeta) stack.getItemMeta()).getTrim());
stack.unsetData(DataComponentTypes.TRIM);
Assertions.assertFalse(stack.getItemMeta().hasItemFlag(ItemFlag.HIDE_ARMOR_TRIM));
Assertions.assertFalse(((ArmorMeta) stack.getItemMeta()).hasTrim());
}
@Test
void testChargedProjectiles() {
final ItemStack stack = new ItemStack(Material.CROSSBOW);
ItemStack projectile = new ItemStack(Material.FIREWORK_ROCKET);
stack.setData(DataComponentTypes.CHARGED_PROJECTILES, ChargedProjectiles.chargedProjectiles().add(projectile).build());
CrossbowMeta meta = (CrossbowMeta) stack.getItemMeta();
Assertions.assertEquals(meta.getChargedProjectiles().getFirst(), projectile);
stack.unsetData(DataComponentTypes.CHARGED_PROJECTILES);
meta = (CrossbowMeta) stack.getItemMeta();
Assertions.assertTrue(meta.getChargedProjectiles().isEmpty());
}
@Test
void testPot() {
final ItemStack stack = new ItemStack(Material.DECORATED_POT);
stack.setData(DataComponentTypes.POT_DECORATIONS, PotDecorations.potDecorations().back(ItemType.DANGER_POTTERY_SHERD).build());
BlockState state = ((BlockStateMeta) stack.getItemMeta()).getBlockState();
DecoratedPot decoratedPot = (DecoratedPot) state;
Assertions.assertEquals(decoratedPot.getSherd(DecoratedPot.Side.BACK), Material.DANGER_POTTERY_SHERD);
stack.unsetData(DataComponentTypes.POT_DECORATIONS);
decoratedPot = (DecoratedPot) ((BlockStateMeta) stack.getItemMeta()).getBlockState();
Assertions.assertTrue(decoratedPot.getSherds().values().stream().allMatch((m) -> m.asItemType() == ItemType.BRICK));
}
@Test
void testRecipes() {
final ItemStack stack = new ItemStack(Material.KNOWLEDGE_BOOK);
stack.setData(DataComponentTypes.RECIPES, List.of(Key.key("paper:fun_recipe")));
final ItemMeta itemMeta = stack.getItemMeta();
Assertions.assertInstanceOf(KnowledgeBookMeta.class, itemMeta);
final List<NamespacedKey> recipes = ((KnowledgeBookMeta) itemMeta).getRecipes();
Assertions.assertEquals(recipes, List.of(new NamespacedKey("paper", "fun_recipe")));
}
@Test
void testJukeboxWithEitherKey() {
final ItemStack apiStack = CraftItemStack.asBukkitCopy(new net.minecraft.world.item.ItemStack(Items.MUSIC_DISC_5));
final JukeboxPlayable data = apiStack.getData(DataComponentTypes.JUKEBOX_PLAYABLE);
Assertions.assertNotNull(data);
Assertions.assertEquals(JukeboxSong.FIVE, data.jukeboxSong());
}
@Test
void testJukeboxWithEitherHolder() {
final net.minecraft.world.item.ItemStack internalStack = new net.minecraft.world.item.ItemStack(Items.STONE);
internalStack.set(DataComponents.JUKEBOX_PLAYABLE, new net.minecraft.world.item.JukeboxPlayable(
new EitherHolder<>(RegistryHelper.getRegistry().lookupOrThrow(Registries.JUKEBOX_SONG).getOrThrow(JukeboxSongs.FIVE)),
true
));
final ItemStack apiStack = CraftItemStack.asBukkitCopy(internalStack);
final JukeboxPlayable data = apiStack.getData(DataComponentTypes.JUKEBOX_PLAYABLE);
Assertions.assertNotNull(data);
Assertions.assertEquals(JukeboxSong.FIVE, data.jukeboxSong());
}
private static <T, M extends ItemMeta> void testWithMeta(final ItemStack stack, final DataComponentType.Valued<T> type, final T value, final Class<M> metaType, final Function<M, T> metaGetter, final BiConsumer<M, T> metaSetter) {
testWithMeta(stack, type, value, Function.identity(), metaType, metaGetter, metaSetter);
}
private static <T, M extends ItemMeta, R> void testWithMeta(final ItemStack stack, final DataComponentType.Valued<T> type, final T value, Function<T, R> mapper, final Class<M> metaType, final Function<M, R> metaGetter, final BiConsumer<M, R> metaSetter) {
ItemStack original = stack.clone();
stack.setData(type, value);
Assertions.assertEquals(value, stack.getData(type));
final ItemMeta meta = stack.getItemMeta();
final M typedMeta = Assertions.assertInstanceOf(metaType, meta);
Assertions.assertEquals(metaGetter.apply(typedMeta), mapper.apply(value));
// SETTING
metaSetter.accept(typedMeta, mapper.apply(value));
original.setItemMeta(typedMeta);
Assertions.assertEquals(value, original.getData(type));
}
private static <M extends ItemMeta> void testWithMeta(final ItemStack stack, final DataComponentType.NonValued type, final boolean value, final Class<M> metaType, final Function<M, Boolean> metaGetter, final BiConsumer<M, Boolean> metaSetter) {
ItemStack original = stack.clone();
stack.setData(type);
Assertions.assertEquals(value, stack.hasData(type));
final ItemMeta meta = stack.getItemMeta();
final M typedMeta = Assertions.assertInstanceOf(metaType, meta);
Assertions.assertEquals(metaGetter.apply(typedMeta), value);
// SETTING
metaSetter.accept(typedMeta, value);
original.setItemMeta(typedMeta);
Assertions.assertEquals(value, original.hasData(type));
}
}

View File

@@ -0,0 +1,281 @@
package io.papermc.paper.item;
import com.destroystokyo.paper.profile.CraftPlayerProfile;
import com.destroystokyo.paper.profile.PlayerProfile;
import java.util.UUID;
import java.util.function.Consumer;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Color;
import org.bukkit.Material;
import org.bukkit.craftbukkit.inventory.CraftItemFactory;
import org.bukkit.craftbukkit.inventory.CraftItemStack;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemFactory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.PotionMeta;
import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.support.environment.AllFeatures;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
// TODO: This should technically be used to compare legacy meta vs the newly implemented
@AllFeatures
public class MetaComparisonTest {
private static final ItemFactory FACTORY = CraftItemFactory.instance();
@Test
public void testMetaApplication() {
ItemStack itemStack = new ItemStack(Material.STONE);
ItemMeta meta = itemStack.getItemMeta();
meta.setCustomModelData(1);
ItemMeta converted = FACTORY.asMetaFor(meta, Material.GOLD_INGOT);
Assertions.assertEquals(converted.getCustomModelData(), meta.getCustomModelData());
ItemMeta convertedAdvanced = FACTORY.asMetaFor(meta, Material.PLAYER_HEAD);
Assertions.assertEquals(convertedAdvanced.getCustomModelData(), meta.getCustomModelData());
}
@Test
public void testMetaApplicationDowngrading() {
ItemStack itemStack = new ItemStack(Material.PLAYER_HEAD);
PlayerProfile profile = Bukkit.createProfile("Owen1212055");
SkullMeta meta = (SkullMeta) itemStack.getItemMeta();
meta.setPlayerProfile(profile);
SkullMeta converted = (SkullMeta) FACTORY.asMetaFor(meta, Material.PLAYER_HEAD);
Assertions.assertEquals(converted.getPlayerProfile(), meta.getPlayerProfile());
SkullMeta downgraded = (SkullMeta) FACTORY.asMetaFor(FACTORY.asMetaFor(meta, Material.STONE), Material.PLAYER_HEAD);
Assertions.assertNull(downgraded.getPlayerProfile());
}
@Test
public void testMetaApplicationDowngradingPotion() {
ItemStack itemStack = new ItemStack(Material.POTION);
Color color = Color.BLUE;
PotionMeta meta = (PotionMeta) itemStack.getItemMeta();
meta.setColor(color);
PotionMeta converted = (PotionMeta) FACTORY.asMetaFor(meta, Material.POTION);
Assertions.assertEquals(converted.getColor(), color);
PotionMeta downgraded = (PotionMeta) FACTORY.asMetaFor(FACTORY.asMetaFor(meta, Material.STONE), Material.POTION);
Assertions.assertNull(downgraded.getColor());
}
@Test
public void testNullMeta() {
ItemStack itemStack = new ItemStack(Material.AIR);
Assertions.assertFalse(itemStack.hasItemMeta());
Assertions.assertNull(itemStack.getItemMeta());
}
@Test
public void testPotionMeta() {
PotionEffect potionEffect = new PotionEffect(PotionEffectType.SPEED, 10, 10, false);
ItemStack nmsItemStack = new ItemStack(Material.POTION, 1);
testSetAndGet(nmsItemStack,
(meta) -> ((PotionMeta) meta).addCustomEffect(potionEffect, true),
(meta) -> Assertions.assertEquals(potionEffect, ((PotionMeta) meta).getCustomEffects().getFirst())
);
}
@Test
public void testEnchantment() {
ItemStack stack = new ItemStack(Material.STICK, 1);
testSetAndGet(stack,
(meta) -> Assertions.assertTrue(meta.addEnchant(Enchantment.SHARPNESS, 1, true)),
(meta) -> Assertions.assertEquals(1, meta.getEnchantLevel(Enchantment.SHARPNESS))
);
}
@Test
@Disabled
public void testPlayerHead() {
PlayerProfile profile = new CraftPlayerProfile(UUID.randomUUID(), "Owen1212055");
ItemStack stack = new ItemStack(Material.PLAYER_HEAD, 1);
testSetAndGet(stack,
(meta) -> ((SkullMeta) meta).setPlayerProfile(profile),
(meta) -> {
Assertions.assertTrue(((SkullMeta) meta).hasOwner());
Assertions.assertEquals(profile, ((SkullMeta) meta).getPlayerProfile());
}
);
testSetAndGet(stack,
(meta) -> ((SkullMeta) meta).setOwner("Owen1212055"),
(meta) -> {
Assertions.assertTrue(((SkullMeta) meta).hasOwner());
Assertions.assertEquals("Owen1212055", ((SkullMeta) meta).getOwner());
}
);
}
@Test
public void testBookMetaAuthor() {
ItemStack stack = new ItemStack(Material.WRITTEN_BOOK, 1);
// Legacy string
testSetAndGet(stack,
(meta) -> ((BookMeta) meta).setAuthor("Owen1212055"),
(meta) -> Assertions.assertEquals("Owen1212055", ((BookMeta) meta).getAuthor())
);
// Component Colored
Component coloredName = Component.text("Owen1212055", NamedTextColor.DARK_GRAY);
testSetAndGet(stack,
(meta) -> ((BookMeta) meta).author(coloredName),
(meta) -> Assertions.assertEquals(coloredName, ((BookMeta) meta).author())
);
// Simple text
Component name = Component.text("Owen1212055");
testSetAndGet(stack,
(meta) -> ((BookMeta) meta).author(name),
(meta) -> Assertions.assertEquals(name, ((BookMeta) meta).author())
);
}
@Test
public void testBookMetaTitle() {
ItemStack stack = new ItemStack(Material.WRITTEN_BOOK, 1);
// Legacy string
testSetAndGet(stack,
(meta) -> ((BookMeta) meta).setTitle("Owen1212055"),
(meta) -> Assertions.assertEquals("Owen1212055", ((BookMeta) meta).getTitle())
);
// Component Colored
Component coloredName = Component.text("Owen1212055", NamedTextColor.DARK_GRAY);
testSetAndGet(stack,
(meta) -> ((BookMeta) meta).title(coloredName),
(meta) -> Assertions.assertEquals(coloredName, ((BookMeta) meta).title())
);
// Simple text
Component name = Component.text("Owen1212055");
testSetAndGet(stack,
(meta) -> ((BookMeta) meta).title(name),
(meta) -> Assertions.assertEquals(name, ((BookMeta) meta).title())
);
}
@Test
public void testWriteableBookPages() {
ItemStack stack = new ItemStack(Material.WRITABLE_BOOK, 1);
// Writeable books are serialized as plain text, but has weird legacy color support.
// So, we need to test to make sure that all works here.
// Legacy string
testSetAndGet(stack,
(meta) -> ((BookMeta) meta).addPage("Owen1212055"),
(meta) -> Assertions.assertEquals("Owen1212055", ((BookMeta) meta).getPage(1))
);
// Legacy string colored
String translatedLegacy = ChatColor.translateAlternateColorCodes('&', "&7Owen1212055");
testSetAndGet(stack,
(meta) -> ((BookMeta) meta).addPage(translatedLegacy),
(meta) -> Assertions.assertEquals(translatedLegacy, ((BookMeta) meta).getPage(1))
);
// Component Colored
Component coloredName = Component.text("Owen1212055", NamedTextColor.DARK_GRAY);
testSetAndGet(stack,
(meta) -> ((BookMeta) meta).addPages(coloredName),
(meta) -> Assertions.assertEquals(coloredName, ((BookMeta) meta).page(1))
);
// Simple text
Component name = Component.text("Owen1212055");
testSetAndGet(stack,
(meta) -> ((BookMeta) meta).addPages(name),
(meta) -> Assertions.assertEquals(name, ((BookMeta) meta).page(1))
);
// Simple text + hover... should NOT be saved
// As this is plain text
Component nameWithHover = Component.text("Owen1212055")
.hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("Hover")));
testSetAndGet(stack,
(meta) -> ((BookMeta) meta).addPages(nameWithHover),
(meta) -> Assertions.assertEquals(name, ((BookMeta) meta).page(1))
);
}
@Test
public void testWrittenBookPages() {
ItemStack stack = new ItemStack(Material.WRITTEN_BOOK, 1);
// Writeable books are serialized as plain text, but has weird legacy color support.
// So, we need to test to make sure that all works here.
// Legacy string
testSetAndGet(stack,
(meta) -> ((BookMeta) meta).addPage("Owen1212055"),
(meta) -> Assertions.assertEquals("Owen1212055", ((BookMeta) meta).getPage(1))
);
// Legacy string colored
String translatedLegacy = ChatColor.translateAlternateColorCodes('&', "&7Owen1212055");
testSetAndGet(stack,
(meta) -> ((BookMeta) meta).addPage(translatedLegacy),
(meta) -> Assertions.assertEquals(translatedLegacy, ((BookMeta) meta).getPage(1))
);
// Component Colored
Component coloredName = Component.text("Owen1212055", NamedTextColor.DARK_GRAY);
testSetAndGet(stack,
(meta) -> ((BookMeta) meta).addPages(coloredName),
(meta) -> Assertions.assertEquals(coloredName, ((BookMeta) meta).page(1))
);
// Simple text
Component name = Component.text("Owen1212055");
testSetAndGet(stack,
(meta) -> ((BookMeta) meta).addPages(name),
(meta) -> Assertions.assertEquals(name, ((BookMeta) meta).page(1))
);
// Simple text + hover... should be saved
Component nameWithHover = Component.text("Owen1212055")
.hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("Hover")));
testSetAndGet(stack,
(meta) -> ((BookMeta) meta).addPages(nameWithHover),
(meta) -> Assertions.assertEquals(nameWithHover, ((BookMeta) meta).page(1))
);
}
private void testSetAndGet(ItemStack itemStack, Consumer<ItemMeta> set, Consumer<ItemMeta> get) {
ItemMeta craftMeta = CraftItemStack.getItemMeta(CraftItemStack.asNMSCopy(itemStack)); // TODO: This should be converted to use the old meta when this is added.
ItemMeta paperMeta = CraftItemStack.getItemMeta(CraftItemStack.asNMSCopy(itemStack));
// Test craft meta
set.accept(craftMeta);
get.accept(craftMeta);
// Test paper meta
set.accept(paperMeta);
get.accept(paperMeta);
}
}

View File

@@ -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

View File

@@ -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