From 38e4c013b66d2a870e83fe61b2da6bad608c69af Mon Sep 17 00:00:00 2001 From: Bjarne Koll Date: Sat, 1 Dec 2018 20:26:23 +1100 Subject: SPIGOT-4347: Add API to allow storing arbitrary values on ItemStacks --- .../inventory/CraftCustomTagTypeRegistry.java | 217 +++++++++++++++ .../craftbukkit/inventory/CraftMetaItem.java | 43 ++- .../tags/CraftCustomItemTagContainer.java | 133 +++++++++ .../inventory/tags/CraftItemTagAdapterContext.java | 24 ++ .../util/CraftNBTTagConfigSerializer.java | 80 ++++++ .../inventory/ItemMetaCustomValueTest.java | 302 +++++++++++++++++++++ 6 files changed, 798 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/bukkit/craftbukkit/inventory/CraftCustomTagTypeRegistry.java create mode 100644 src/main/java/org/bukkit/craftbukkit/inventory/tags/CraftCustomItemTagContainer.java create mode 100644 src/main/java/org/bukkit/craftbukkit/inventory/tags/CraftItemTagAdapterContext.java create mode 100644 src/main/java/org/bukkit/craftbukkit/util/CraftNBTTagConfigSerializer.java create mode 100644 src/test/java/org/bukkit/craftbukkit/inventory/ItemMetaCustomValueTest.java (limited to 'src') diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftCustomTagTypeRegistry.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftCustomTagTypeRegistry.java new file mode 100644 index 00000000..22c0abb9 --- /dev/null +++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftCustomTagTypeRegistry.java @@ -0,0 +1,217 @@ +package org.bukkit.craftbukkit.inventory; + +import com.google.common.primitives.Primitives; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import net.minecraft.server.NBTBase; +import net.minecraft.server.NBTTagByte; +import net.minecraft.server.NBTTagByteArray; +import net.minecraft.server.NBTTagCompound; +import net.minecraft.server.NBTTagDouble; +import net.minecraft.server.NBTTagFloat; +import net.minecraft.server.NBTTagInt; +import net.minecraft.server.NBTTagIntArray; +import net.minecraft.server.NBTTagLong; +import net.minecraft.server.NBTTagLongArray; +import net.minecraft.server.NBTTagShort; +import net.minecraft.server.NBTTagString; +import org.apache.commons.lang3.Validate; +import org.bukkit.craftbukkit.inventory.tags.CraftCustomItemTagContainer; +import org.bukkit.inventory.meta.tags.CustomItemTagContainer; + +/** + * This class represents a registry that contains the used adapters for. + */ +public final class CraftCustomTagTypeRegistry { + + private final Function CREATE_ADAPTER = this::createAdapter; + + private class CustomTagAdapter { + + private final Function builder; + private final Function extractor; + + private final Class primitiveType; + private final Class nbtBaseType; + + public CustomTagAdapter(Class primitiveType, Class nbtBaseType, Function builder, Function extractor) { + this.primitiveType = primitiveType; + this.nbtBaseType = nbtBaseType; + this.builder = builder; + this.extractor = extractor; + } + + /** + * This method will extract the value stored in the tag, according to + * the expected primitive type. + * + * @param base the base to extract from + * @return the value stored inside of the tag + * @throws ClassCastException if the passed base is not an instanced of + * the defined base type and therefore is not applicable to the + * extractor function + */ + T extract(NBTBase base) { + Validate.isInstanceOf(nbtBaseType, base, "The provided NBTBase was of the type %s. Expected type %s", base.getClass().getSimpleName(), nbtBaseType.getSimpleName()); + return this.extractor.apply(nbtBaseType.cast(base)); + } + + /** + * Builds a tag instance wrapping around the provided value object. + * + * @param value the value to store inside the created tag + * @return the new tag instance + * @throws ClassCastException if the passed value object is not of the + * defined primitive type and therefore is not applicable to the builder + * function + */ + Z build(Object value) { + Validate.isInstanceOf(primitiveType, value, "The provided value was of the type %s. Expected type %s", value.getClass().getSimpleName(), primitiveType.getSimpleName()); + return this.builder.apply(primitiveType.cast(value)); + } + + /** + * Returns if the tag instance matches the adapters one. + * + * @param base the base to check + * @return if the tag was an instance of the set type + */ + boolean isInstance(NBTBase base) { + return this.nbtBaseType.isInstance(base); + } + } + + private final Map adapters = new HashMap<>(); + + /** + * Creates a suitable adapter instance for the primitive class type + * + * @param type the type to create an adapter for + * @param the generic type of that class + * @return the created adapter instance + * @throws IllegalArgumentException if no suitable tag type adapter for this + * type was found + */ + private CustomTagAdapter createAdapter(Class type) { + if (!Primitives.isWrapperType(type)) { + type = Primitives.wrap(type); //Make sure we will always "switch" over the wrapper types + } + + /* + Primitives + */ + if (Objects.equals(Byte.class, type)) { + return createAdapter(Byte.class, NBTTagByte.class, NBTTagByte::new, NBTTagByte::g); // PAIL: rename asByte + } + if (Objects.equals(Short.class, type)) { + return createAdapter(Short.class, NBTTagShort.class, NBTTagShort::new, NBTTagShort::f); // PAIL: rename asShort + } + if (Objects.equals(Integer.class, type)) { + return createAdapter(Integer.class, NBTTagInt.class, NBTTagInt::new, NBTTagInt::e); // PAIL: rename asInteger + } + if (Objects.equals(Long.class, type)) { + return createAdapter(Long.class, NBTTagLong.class, NBTTagLong::new, NBTTagLong::d); // PAIL: rename asLong + } + if (Objects.equals(Float.class, type)) { + return createAdapter(Float.class, NBTTagFloat.class, NBTTagFloat::new, NBTTagFloat::i); // PAIL: rename asFloat + } + if (Objects.equals(Double.class, type)) { + return createAdapter(Double.class, NBTTagDouble.class, NBTTagDouble::new, NBTTagDouble::asDouble); + } + + /* + String + */ + if (Objects.equals(String.class, type)) { + return createAdapter(String.class, NBTTagString.class, NBTTagString::new, NBTTagString::b_); // PAIL: rename getString + } + + /* + Primitive Arrays + */ + if (Objects.equals(byte[].class, type)) { + return createAdapter(byte[].class, NBTTagByteArray.class, array -> new NBTTagByteArray(Arrays.copyOf(array, array.length)), n -> Arrays.copyOf(n.c(), n.size())); // PAIL: rename getByteArray + } + if (Objects.equals(int[].class, type)) { + return createAdapter(int[].class, NBTTagIntArray.class, array -> new NBTTagIntArray(Arrays.copyOf(array, array.length)), n -> Arrays.copyOf(n.d(), n.size())); // PAIL: rename getIntegerArray + } + if (Objects.equals(long[].class, type)) { + return createAdapter(long[].class, NBTTagLongArray.class, array -> new NBTTagLongArray(Arrays.copyOf(array, array.length)), n -> Arrays.copyOf(n.d(), n.size())); // PAIL: rename getLongArray + } + + /* + Note that this will map the interface CustomItemTagContainer directly to the CraftBukkit implementation + Passing any other instance of this form to the tag type registry will throw a ClassCastException as defined in CustomTagAdapter#build + */ + if (Objects.equals(CustomItemTagContainer.class, type)) { + return createAdapter(CraftCustomItemTagContainer.class, NBTTagCompound.class, CraftCustomItemTagContainer::toTagCompound, tag -> { + CraftCustomItemTagContainer container = new CraftCustomItemTagContainer(this); + for (String key : tag.getKeys()) { + container.put(key, tag.get(key)); + } + return container; + }); + } + + throw new IllegalArgumentException("Could not find a valid CustomTagAdapter implementation for the requested type " + type.getSimpleName()); + } + + private CustomTagAdapter createAdapter(Class primitiveType, Class nbtBaseType, Function builder, Function extractor) { + return new CustomTagAdapter<>(primitiveType, nbtBaseType, builder, extractor); + } + + /** + * Wraps the passed value into a tag instance. + * + * @param type the type of the passed value + * @param value the value to be stored in the tag + * @param the generic type of the value + * @return the created tag instance + * @throws IllegalArgumentException if no suitable tag type adapter for this + * type was found + */ + public NBTBase wrap(Class type, T value) { + return this.adapters.computeIfAbsent(type, CREATE_ADAPTER).build(value); + } + + /** + * Returns if the tag instance matches the provided primitive type. + * + * @param type the type of the primitive value + * @param base the base instance to check + * @param the generic type of the type + * @return if the base stores values of the primitive type passed + * @throws IllegalArgumentException if no suitable tag type adapter for this + * type was found + */ + public boolean isInstanceOf(Class type, NBTBase base) { + return this.adapters.computeIfAbsent(type, CREATE_ADAPTER).isInstance(base); + } + + /** + * Extracts the value out of the provided tag. + * + * @param type the type of the value to extract + * @param tag the tag to extract the value from + * @param the generic type of the value stored inside the tag + * @return the extracted value + * @throws IllegalArgumentException if the passed base is not an instanced + * of the defined base type and therefore is not applicable to the extractor + * function + * @throws IllegalArgumentException if the found object is not of type + * passed + * @throws IllegalArgumentException if no suitable tag type adapter for this + * type was found + */ + public T extract(Class type, NBTBase tag) throws ClassCastException, IllegalArgumentException { + CustomTagAdapter adapter = this.adapters.computeIfAbsent(type, CREATE_ADAPTER); + Validate.isTrue(adapter.isInstance(tag), "`The found tag instance cannot store %s as it is a %s", type.getSimpleName(), tag.getClass().getSimpleName()); + + Object foundValue = adapter.extract(tag); + Validate.isInstanceOf(type, foundValue, "The found object is of the type %s. Expected type %s", foundValue.getClass().getSimpleName(), type.getSimpleName()); + return type.cast(foundValue); + } +} diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java index 60cdd1dc..19e52317 100644 --- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java +++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java @@ -42,8 +42,10 @@ import org.bukkit.craftbukkit.Overridden; import org.bukkit.craftbukkit.attribute.CraftAttributeInstance; import org.bukkit.craftbukkit.attribute.CraftAttributeMap; import org.bukkit.craftbukkit.inventory.CraftMetaItem.ItemMetaKey.Specific; +import org.bukkit.craftbukkit.inventory.tags.CraftCustomItemTagContainer; import org.bukkit.craftbukkit.util.CraftChatMessage; import org.bukkit.craftbukkit.util.CraftMagicNumbers; +import org.bukkit.craftbukkit.util.CraftNBTTagConfigSerializer; import org.bukkit.craftbukkit.util.CraftNamespacedKey; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.EquipmentSlot; @@ -51,6 +53,7 @@ import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.meta.Damageable; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.Repairable; +import org.bukkit.inventory.meta.tags.CustomItemTagContainer; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -244,6 +247,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable { static final ItemMetaKey UNBREAKABLE = new ItemMetaKey("Unbreakable"); @Specific(Specific.To.NBT) static final ItemMetaKey DAMAGE = new ItemMetaKey("Damage"); + static final ItemMetaKey BUKKIT_CUSTOM_TAG = new ItemMetaKey("PublicBukkitValues"); private IChatBaseComponent displayName; private IChatBaseComponent locName; @@ -256,9 +260,11 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable { private int damage; private static final Set HANDLED_TAGS = Sets.newHashSet(); + private static final CraftCustomTagTypeRegistry TAG_TYPE_REGISTRY = new CraftCustomTagTypeRegistry(); private NBTTagCompound internalTag; private final Map unhandledTags = new HashMap(); + private final CraftCustomItemTagContainer publicItemTagContainer = new CraftCustomItemTagContainer(TAG_TYPE_REGISTRY); CraftMetaItem(CraftMetaItem meta) { if (meta == null) { @@ -285,6 +291,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable { this.unbreakable = meta.unbreakable; this.damage = meta.damage; this.unhandledTags.putAll(meta.unhandledTags); + this.publicItemTagContainer.putAll(meta.publicItemTagContainer.getRaw()); this.internalTag = meta.internalTag; if (this.internalTag != null) { @@ -339,6 +346,13 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable { if (tag.hasKey(DAMAGE.NBT)) { damage = tag.getInt(DAMAGE.NBT); } + if (tag.hasKey(BUKKIT_CUSTOM_TAG.NBT)) { + NBTTagCompound compound = tag.getCompound(BUKKIT_CUSTOM_TAG.NBT); + Set keys = compound.getKeys(); + for (String key : keys) { + publicItemTagContainer.put(key, compound.get(key)); + } + } Set keys = tag.getKeys(); for (String key : keys) { @@ -476,6 +490,11 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable { Logger.getLogger(CraftMetaItem.class.getName()).log(Level.SEVERE, null, ex); } } + + Map nbtMap = SerializableMeta.getObject(Map.class, map, BUKKIT_CUSTOM_TAG.BUKKIT, true); + if (nbtMap != null) { + this.publicItemTagContainer.putAll((NBTTagCompound) CraftNBTTagConfigSerializer.deserialize(nbtMap)); + } } void deserializeInternal(NBTTagCompound tag, Object context) { @@ -575,6 +594,16 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable { for (Map.Entry e : unhandledTags.entrySet()) { itemTag.set(e.getKey(), e.getValue()); } + + if (!publicItemTagContainer.isEmpty()) { + NBTTagCompound bukkitCustomCompound = new NBTTagCompound(); + Map rawPublicMap = publicItemTagContainer.getRaw(); + + for (Map.Entry nbtBaseEntry : rawPublicMap.entrySet()) { + bukkitCustomCompound.set(nbtBaseEntry.getKey(), nbtBaseEntry.getValue()); + } + itemTag.set(BUKKIT_CUSTOM_TAG.NBT, bukkitCustomCompound); + } } static NBTTagList createStringList(List list) { @@ -659,7 +688,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable { @Overridden boolean isEmpty() { - return !(hasDisplayName() || hasLocalizedName() || hasEnchants() || hasLore() || hasRepairCost() || !unhandledTags.isEmpty() || hideFlag != 0 || isUnbreakable() || hasDamage() || hasAttributeModifiers()); + return !(hasDisplayName() || hasLocalizedName() || hasEnchants() || hasLore() || hasRepairCost() || !unhandledTags.isEmpty() || !publicItemTagContainer.isEmpty() || hideFlag != 0 || isUnbreakable() || hasDamage() || hasAttributeModifiers()); } public String getDisplayName() { @@ -926,6 +955,11 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable { return removed > 0; } + @Override + public CustomItemTagContainer getCustomTagContainer() { + return this.publicItemTagContainer; + } + private static boolean compareModifiers(Multimap first, Multimap second) { if (first == null || second == null) { return false; @@ -986,6 +1020,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable { && (this.hasRepairCost() ? that.hasRepairCost() && this.repairCost == that.repairCost : !that.hasRepairCost()) && (this.hasAttributeModifiers() ? that.hasAttributeModifiers() && compareModifiers(this.attributeModifiers, that.attributeModifiers) : !that.hasAttributeModifiers()) && (this.unhandledTags.equals(that.unhandledTags)) + && (this.publicItemTagContainer.equals(that.publicItemTagContainer)) && (this.hideFlag == that.hideFlag) && (this.isUnbreakable() == that.isUnbreakable()) && (this.hasDamage() ? that.hasDamage() && this.damage == that.damage : !that.hasDamage()); @@ -1015,6 +1050,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable { hash = 61 * hash + (hasEnchants() ? this.enchantments.hashCode() : 0); hash = 61 * hash + (hasRepairCost() ? this.repairCost : 0); hash = 61 * hash + unhandledTags.hashCode(); + hash = 61 * hash + (!publicItemTagContainer.isEmpty() ? publicItemTagContainer.hashCode() : 0); hash = 61 * hash + hideFlag; hash = 61 * hash + (isUnbreakable() ? 1231 : 1237); hash = 61 * hash + (hasDamage() ? this.damage : 0); @@ -1104,6 +1140,10 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable { } } + if (!publicItemTagContainer.isEmpty()) { // Store custom tags, wrapped in their compound + builder.put(BUKKIT_CUSTOM_TAG.BUKKIT, publicItemTagContainer.serialize()); + } + return builder; } @@ -1199,6 +1239,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable { HIDEFLAGS.NBT, UNBREAKABLE.NBT, DAMAGE.NBT, + BUKKIT_CUSTOM_TAG.NBT, ATTRIBUTES.NBT, ATTRIBUTES_IDENTIFIER.NBT, ATTRIBUTES_NAME.NBT, diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/tags/CraftCustomItemTagContainer.java b/src/main/java/org/bukkit/craftbukkit/inventory/tags/CraftCustomItemTagContainer.java new file mode 100644 index 00000000..fe663bba --- /dev/null +++ b/src/main/java/org/bukkit/craftbukkit/inventory/tags/CraftCustomItemTagContainer.java @@ -0,0 +1,133 @@ +package org.bukkit.craftbukkit.inventory.tags; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import net.minecraft.server.NBTBase; +import net.minecraft.server.NBTTagCompound; +import org.apache.commons.lang.Validate; +import org.bukkit.NamespacedKey; +import org.bukkit.craftbukkit.inventory.CraftCustomTagTypeRegistry; +import org.bukkit.craftbukkit.util.CraftNBTTagConfigSerializer; +import org.bukkit.inventory.meta.tags.CustomItemTagContainer; +import org.bukkit.inventory.meta.tags.ItemTagAdapterContext; +import org.bukkit.inventory.meta.tags.ItemTagType; + +public final class CraftCustomItemTagContainer implements CustomItemTagContainer { + + private final Map customTags = new HashMap<>(); + private final CraftCustomTagTypeRegistry tagTypeRegistry; + private final CraftItemTagAdapterContext adapterContext; + + public CraftCustomItemTagContainer(Map customTags, CraftCustomTagTypeRegistry tagTypeRegistry) { + this(tagTypeRegistry); + this.customTags.putAll(customTags); + } + + public CraftCustomItemTagContainer(CraftCustomTagTypeRegistry tagTypeRegistry) { + this.tagTypeRegistry = tagTypeRegistry; + this.adapterContext = new CraftItemTagAdapterContext(this.tagTypeRegistry); + } + + @Override + public void setCustomTag(NamespacedKey key, ItemTagType type, Z value) { + Validate.notNull(key, "The provided key for the custom value was null"); + Validate.notNull(type, "The provided type for the custom value was null"); + Validate.notNull(value, "The provided value for the custom value was null"); + + this.customTags.put(key.toString(), tagTypeRegistry.wrap(type.getPrimitiveType(), type.toPrimitive(value, adapterContext))); + } + + @Override + public boolean hasCustomTag(NamespacedKey key, ItemTagType type) { + Validate.notNull(key, "The provided key for the custom value was null"); + Validate.notNull(type, "The provided type for the custom value was null"); + + NBTBase value = this.customTags.get(key.toString()); + if (value == null) { + return false; + } + + return tagTypeRegistry.isInstanceOf(type.getPrimitiveType(), value); + } + + @Override + public Z getCustomTag(NamespacedKey key, ItemTagType type) { + Validate.notNull(key, "The provided key for the custom value was null"); + Validate.notNull(type, "The provided type for the custom value was null"); + + NBTBase value = this.customTags.get(key.toString()); + if (value == null) { + return null; + } + + return type.fromPrimitive(tagTypeRegistry.extract(type.getPrimitiveType(), value), adapterContext); + } + + @Override + public void removeCustomTag(NamespacedKey key) { + Validate.notNull(key, "The provided key for the custom value was null"); + + this.customTags.remove(key.toString()); + } + + @Override + public boolean isEmpty() { + return this.customTags.isEmpty(); + } + + @Override + public ItemTagAdapterContext getAdapterContext() { + return this.adapterContext; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof CraftCustomItemTagContainer)) { + return false; + } + + Map myRawMap = getRaw(); + Map theirRawMap = ((CraftCustomItemTagContainer) obj).getRaw(); + + return Objects.equals(myRawMap, theirRawMap); + } + + public NBTTagCompound toTagCompound() { + NBTTagCompound tag = new NBTTagCompound(); + for (Entry entry : this.customTags.entrySet()) { + tag.set(entry.getKey(), entry.getValue()); + } + return tag; + } + + public void put(String key, NBTBase base) { + this.customTags.put(key, base); + } + + public void putAll(Map map) { + this.customTags.putAll(map); + } + + public void putAll(NBTTagCompound compound) { + for (String key : compound.getKeys()) { + this.customTags.put(key, compound.get(key)); + } + } + + public Map getRaw() { + return this.customTags; + } + + @Override + public int hashCode() { + int hashCode = 3; + hashCode += this.customTags.hashCode(); // We will simply add the maps hashcode + return hashCode; + } + + public Map serialize() { + return (Map) CraftNBTTagConfigSerializer.serialize(toTagCompound()); + } +} diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/tags/CraftItemTagAdapterContext.java b/src/main/java/org/bukkit/craftbukkit/inventory/tags/CraftItemTagAdapterContext.java new file mode 100644 index 00000000..5b81cad1 --- /dev/null +++ b/src/main/java/org/bukkit/craftbukkit/inventory/tags/CraftItemTagAdapterContext.java @@ -0,0 +1,24 @@ +package org.bukkit.craftbukkit.inventory.tags; + +import org.bukkit.craftbukkit.inventory.CraftCustomTagTypeRegistry; +import org.bukkit.inventory.meta.tags.CustomItemTagContainer; +import org.bukkit.inventory.meta.tags.ItemTagAdapterContext; + +public final class CraftItemTagAdapterContext implements ItemTagAdapterContext { + + private final CraftCustomTagTypeRegistry registry; + + public CraftItemTagAdapterContext(CraftCustomTagTypeRegistry registry) { + this.registry = registry; + } + + /** + * Creates a new and empty tag container instance + * + * @return the fresh container instance + */ + @Override + public CustomItemTagContainer newTagContainer() { + return new CraftCustomItemTagContainer(this.registry); + } +} diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftNBTTagConfigSerializer.java b/src/main/java/org/bukkit/craftbukkit/util/CraftNBTTagConfigSerializer.java new file mode 100644 index 00000000..da81d782 --- /dev/null +++ b/src/main/java/org/bukkit/craftbukkit/util/CraftNBTTagConfigSerializer.java @@ -0,0 +1,80 @@ +package org.bukkit.craftbukkit.util; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import net.minecraft.server.MojangsonParser; +import net.minecraft.server.NBTBase; +import net.minecraft.server.NBTList; +import net.minecraft.server.NBTTagCompound; +import net.minecraft.server.NBTTagList; +import net.minecraft.server.NBTTagString; + +public class CraftNBTTagConfigSerializer { + + private static final Pattern ARRAY = Pattern.compile("^\\[.*]"); + private static final MojangsonParser MOJANGSON_PARSER = new MojangsonParser(new StringReader("")); + + public static Object serialize(NBTBase base) { + if (base instanceof NBTTagCompound) { + Map innerMap = new HashMap<>(); + for (String key : ((NBTTagCompound) base).getKeys()) { + innerMap.put(key, serialize(((NBTTagCompound) base).get(key))); + } + + return innerMap; + } else if (base instanceof NBTTagList) { + List baseList = new ArrayList<>(); + for (int i = 0; i < ((NBTList) base).size(); i++) { + baseList.add(serialize(((NBTList) base).get(i))); + } + + return baseList; + } else if (base instanceof NBTTagString) { + return base.b_(); //PAIL Rename getString + } + + return base.toString(); + } + + public static NBTBase deserialize(Object object) { + if (object instanceof Map) { + NBTTagCompound compound = new NBTTagCompound(); + for (Map.Entry entry : ((Map) object).entrySet()) { + compound.set(entry.getKey(), deserialize(entry.getValue())); + } + + return compound; + } else if (object instanceof List) { + List list = (List) object; + if (list.isEmpty()) { + return new NBTTagList(); // Default + } + + NBTTagList tagList = new NBTTagList(); + for (Object tag : list) { + tagList.add(deserialize(tag)); + } + + return tagList; + } else if (object instanceof String) { + String string = (String) object; + + if (ARRAY.matcher(string).matches()) { + try { + return new MojangsonParser(new StringReader(string)).h(); // PAIL Rename parseTagList + } catch (CommandSyntaxException e) { + throw new RuntimeException("Could not deserialize found list ", e); + } + } else { + return MOJANGSON_PARSER.b(string); // PAIL Rename parse tagBase + } + } + + throw new RuntimeException("Could not deserialize NBTBase"); + } +} diff --git a/src/test/java/org/bukkit/craftbukkit/inventory/ItemMetaCustomValueTest.java b/src/test/java/org/bukkit/craftbukkit/inventory/ItemMetaCustomValueTest.java new file mode 100644 index 00000000..14e7ae8c --- /dev/null +++ b/src/test/java/org/bukkit/craftbukkit/inventory/ItemMetaCustomValueTest.java @@ -0,0 +1,302 @@ +package org.bukkit.craftbukkit.inventory; + +import java.io.StringReader; +import java.lang.reflect.Array; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Map; +import java.util.UUID; +import net.minecraft.server.NBTBase; +import net.minecraft.server.NBTTagCompound; +import net.minecraft.server.NBTTagIntArray; +import net.minecraft.server.NBTTagList; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.craftbukkit.inventory.tags.CraftCustomItemTagContainer; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.tags.CustomItemTagContainer; +import org.bukkit.inventory.meta.tags.ItemTagAdapterContext; +import org.bukkit.inventory.meta.tags.ItemTagType; +import org.bukkit.support.AbstractTestingBase; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import org.junit.Before; +import org.junit.Test; + +public class ItemMetaCustomValueTest extends AbstractTestingBase { + + private static NamespacedKey VALID_KEY; + + @Before + public void setup() { + VALID_KEY = new NamespacedKey("test", "validkey"); + } + + /* + Sets a test + */ + @Test(expected = IllegalArgumentException.class) + public void testSetNoAdapter() { + ItemMeta itemMeta = createNewItemMeta(); + itemMeta.getCustomTagContainer().setCustomTag(VALID_KEY, new PrimitiveTagType<>(boolean.class), true); + } + + /* + Contains a tag + */ + @Test(expected = IllegalArgumentException.class) + public void testHasNoAdapter() { + ItemMeta itemMeta = createNewItemMeta(); + itemMeta.getCustomTagContainer().setCustomTag(VALID_KEY, ItemTagType.INTEGER, 1); // We gotta set this so we at least try to compare it + itemMeta.getCustomTagContainer().hasCustomTag(VALID_KEY, new PrimitiveTagType<>(boolean.class)); + } + + /* + Getting a tag + */ + @Test(expected = IllegalArgumentException.class) + public void testGetNoAdapter() { + ItemMeta itemMeta = createNewItemMeta(); + itemMeta.getCustomTagContainer().setCustomTag(VALID_KEY, ItemTagType.INTEGER, 1); //We gotta set this so we at least try to compare it + itemMeta.getCustomTagContainer().getCustomTag(VALID_KEY, new PrimitiveTagType<>(boolean.class)); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetWrongType() { + ItemMeta itemMeta = createNewItemMeta(); + itemMeta.getCustomTagContainer().setCustomTag(VALID_KEY, ItemTagType.INTEGER, 1); + itemMeta.getCustomTagContainer().getCustomTag(VALID_KEY, ItemTagType.STRING); + } + + @Test + public void testDifferentNamespace() { + NamespacedKey namespacedKeyA = new NamespacedKey("plugin-a", "damage"); + NamespacedKey namespacedKeyB = new NamespacedKey("plugin-b", "damage"); + + ItemMeta meta = createNewItemMeta(); + meta.getCustomTagContainer().setCustomTag(namespacedKeyA, ItemTagType.LONG, 15L); + meta.getCustomTagContainer().setCustomTag(namespacedKeyB, ItemTagType.LONG, 160L); + + assertEquals(15L, (long) meta.getCustomTagContainer().getCustomTag(namespacedKeyA, ItemTagType.LONG)); + assertEquals(160L, (long) meta.getCustomTagContainer().getCustomTag(namespacedKeyB, ItemTagType.LONG)); + } + + private ItemMeta createNewItemMeta() { + return Bukkit.getItemFactory().getItemMeta(Material.DIAMOND_PICKAXE); + } + + private NamespacedKey requestKey(String keyName) { + return new NamespacedKey("test-plugin", keyName.toLowerCase()); + } + + /* + Removing a tag + */ + @Test + public void testNBTTagStoring() { + CraftMetaItem itemMeta = createComplexItemMeta(); + + NBTTagCompound compound = new NBTTagCompound(); + itemMeta.applyToItem(compound); + + assertEquals(itemMeta, new CraftMetaItem(compound)); + } + + @Test + public void testMapStoring() { + CraftMetaItem itemMeta = createComplexItemMeta(); + + Map serialize = itemMeta.serialize(); + assertEquals(itemMeta, new CraftMetaItem(serialize)); + } + + @Test + public void testYAMLStoring() { + ItemStack stack = new ItemStack(Material.DIAMOND); + CraftMetaItem meta = createComplexItemMeta(); + stack.setItemMeta(meta); + + YamlConfiguration configuration = new YamlConfiguration(); + configuration.set("testpath", stack); + + String configValue = configuration.saveToString(); + YamlConfiguration loadedConfig = YamlConfiguration.loadConfiguration(new StringReader(configValue)); + + assertEquals(stack, loadedConfig.getSerializable("testpath", ItemStack.class)); + assertNotEquals(new ItemStack(Material.DIAMOND), loadedConfig.getSerializable("testpath", ItemStack.class)); + } + + private CraftMetaItem createComplexItemMeta() { + CraftMetaItem itemMeta = (CraftMetaItem) createNewItemMeta(); + itemMeta.setDisplayName("Item Display Name"); + + itemMeta.getCustomTagContainer().setCustomTag(requestKey("custom-long"), ItemTagType.LONG, 4L); //Add random primitive values + itemMeta.getCustomTagContainer().setCustomTag(requestKey("custom-byte-array"), ItemTagType.BYTE_ARRAY, new byte[]{ + 0, 1, 2, 10 + }); + itemMeta.getCustomTagContainer().setCustomTag(requestKey("custom-string"), ItemTagType.STRING, "Hello there world"); + + CustomItemTagContainer innerContainer = itemMeta.getCustomTagContainer().getAdapterContext().newTagContainer(); //Add a inner container + innerContainer.setCustomTag(VALID_KEY, ItemTagType.LONG, 5L); + itemMeta.getCustomTagContainer().setCustomTag(requestKey("custom-inner-compound"), ItemTagType.TAG_CONTAINER, innerContainer); + + Map rawMap = ((CraftCustomItemTagContainer) itemMeta.getCustomTagContainer()).getRaw(); //Adds a tag list as well (even tho is has no API yet) + NBTTagList nbtList = new NBTTagList(); + nbtList.add(new NBTTagIntArray(Arrays.asList(1, 5, 3))); + nbtList.add(new NBTTagIntArray(Arrays.asList(42, 51))); + rawMap.put("nbttaglist", nbtList); + return itemMeta; + } + + /* + Test complex object storage + */ + @Test + public void storeUUIDOnItemTest() { + ItemMeta itemMeta = createNewItemMeta(); + UUIDItemTagType uuidItemTagType = new UUIDItemTagType(); + UUID uuid = UUID.fromString("434eea72-22a6-4c61-b5ef-945874a5c478"); + + itemMeta.getCustomTagContainer().setCustomTag(VALID_KEY, uuidItemTagType, uuid); + assertTrue(itemMeta.getCustomTagContainer().hasCustomTag(VALID_KEY, uuidItemTagType)); + assertEquals(uuid, itemMeta.getCustomTagContainer().getCustomTag(VALID_KEY, uuidItemTagType)); + } + + @Test + public void encapsulatedContainers() { + NamespacedKey innerKey = new NamespacedKey("plugin-a", "inner"); + + ItemMeta meta = createNewItemMeta(); + ItemTagAdapterContext context = meta.getCustomTagContainer().getAdapterContext(); + + CustomItemTagContainer thirdContainer = context.newTagContainer(); + thirdContainer.setCustomTag(VALID_KEY, ItemTagType.LONG, 3L); + + CustomItemTagContainer secondContainer = context.newTagContainer(); + secondContainer.setCustomTag(VALID_KEY, ItemTagType.LONG, 2L); + secondContainer.setCustomTag(innerKey, ItemTagType.TAG_CONTAINER, thirdContainer); + + meta.getCustomTagContainer().setCustomTag(VALID_KEY, ItemTagType.LONG, 1L); + meta.getCustomTagContainer().setCustomTag(innerKey, ItemTagType.TAG_CONTAINER, secondContainer); + + assertEquals(3L, meta.getCustomTagContainer() + .getCustomTag(innerKey, ItemTagType.TAG_CONTAINER) + .getCustomTag(innerKey, ItemTagType.TAG_CONTAINER) + .getCustomTag(VALID_KEY, ItemTagType.LONG).longValue()); + + assertEquals(2L, meta.getCustomTagContainer() + .getCustomTag(innerKey, ItemTagType.TAG_CONTAINER) + .getCustomTag(VALID_KEY, ItemTagType.LONG).longValue()); + + assertEquals(1L, meta.getCustomTagContainer() + .getCustomTag(VALID_KEY, ItemTagType.LONG).longValue()); + } + + class UUIDItemTagType implements ItemTagType { + + @Override + public Class getPrimitiveType() { + return byte[].class; + } + + @Override + public Class getComplexType() { + return UUID.class; + } + + @Override + public byte[] toPrimitive(UUID complex, ItemTagAdapterContext context) { + ByteBuffer bb = ByteBuffer.wrap(new byte[16]); + bb.putLong(complex.getMostSignificantBits()); + bb.putLong(complex.getLeastSignificantBits()); + return bb.array(); + } + + @Override + public UUID fromPrimitive(byte[] primitive, ItemTagAdapterContext context) { + ByteBuffer bb = ByteBuffer.wrap(primitive); + long firstLong = bb.getLong(); + long secondLong = bb.getLong(); + return new UUID(firstLong, secondLong); + } + } + + @Test + public void testPrimitiveCustomTags() { + ItemMeta itemMeta = createNewItemMeta(); + + testPrimitiveCustomTag(itemMeta, ItemTagType.BYTE, (byte) 1); + testPrimitiveCustomTag(itemMeta, ItemTagType.SHORT, (short) 1); + testPrimitiveCustomTag(itemMeta, ItemTagType.INTEGER, 1); + testPrimitiveCustomTag(itemMeta, ItemTagType.LONG, 1L); + testPrimitiveCustomTag(itemMeta, ItemTagType.FLOAT, 1.34F); + testPrimitiveCustomTag(itemMeta, ItemTagType.DOUBLE, 151.123); + + testPrimitiveCustomTag(itemMeta, ItemTagType.STRING, "test"); + + testPrimitiveCustomTag(itemMeta, ItemTagType.BYTE_ARRAY, new byte[]{ + 1, 4, 2, Byte.MAX_VALUE + }); + testPrimitiveCustomTag(itemMeta, ItemTagType.INTEGER_ARRAY, new int[]{ + 1, 4, 2, Integer.MAX_VALUE + }); + testPrimitiveCustomTag(itemMeta, ItemTagType.LONG_ARRAY, new long[]{ + 1L, 4L, 2L, Long.MAX_VALUE + }); + } + + private void testPrimitiveCustomTag(ItemMeta meta, ItemTagType type, Z value) { + NamespacedKey tagKey = new NamespacedKey("test", String.valueOf(type.hashCode())); + + meta.getCustomTagContainer().setCustomTag(tagKey, type, value); + assertTrue(meta.getCustomTagContainer().hasCustomTag(tagKey, type)); + + Z foundValue = meta.getCustomTagContainer().getCustomTag(tagKey, type); + if (foundValue.getClass().isArray()) { // Compare arrays using reflection access + int length = Array.getLength(foundValue); + int originalLength = Array.getLength(value); + for (int i = 0; i < length && i < originalLength; i++) { + assertEquals(Array.get(value, i), Array.get(foundValue, i)); + } + } else { + assertEquals(foundValue, value); + } + + meta.getCustomTagContainer().removeCustomTag(tagKey); + assertFalse(meta.getCustomTagContainer().hasCustomTag(tagKey, type)); + } + + class PrimitiveTagType implements ItemTagType { + + private final Class primitiveType; + + PrimitiveTagType(Class primitiveType) { + this.primitiveType = primitiveType; + } + + @Override + public Class getPrimitiveType() { + return primitiveType; + } + + @Override + public Class getComplexType() { + return primitiveType; + } + + @Override + public T toPrimitive(T complex, ItemTagAdapterContext context) { + return complex; + } + + @Override + public T fromPrimitive(T primitive, ItemTagAdapterContext context) { + return primitive; + } + } +} -- cgit v1.2.3