summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBjarne Koll <LynxPlay101@gmail.com>2018-12-01 20:26:23 +1100
committermd_5 <git@md-5.net>2018-12-04 09:44:56 +1100
commit38e4c013b66d2a870e83fe61b2da6bad608c69af (patch)
tree2113fe1f227da35107f174404a68b997deb83790
parenta4c555b6b12661fbcfc442e91a9df1436508e552 (diff)
downloadcraftbukkit-38e4c013b66d2a870e83fe61b2da6bad608c69af.tar
craftbukkit-38e4c013b66d2a870e83fe61b2da6bad608c69af.tar.gz
craftbukkit-38e4c013b66d2a870e83fe61b2da6bad608c69af.tar.lz
craftbukkit-38e4c013b66d2a870e83fe61b2da6bad608c69af.tar.xz
craftbukkit-38e4c013b66d2a870e83fe61b2da6bad608c69af.zip
SPIGOT-4347: Add API to allow storing arbitrary values on ItemStacks
-rw-r--r--nms-patches/MojangsonParser.patch20
-rw-r--r--src/main/java/org/bukkit/craftbukkit/inventory/CraftCustomTagTypeRegistry.java217
-rw-r--r--src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java43
-rw-r--r--src/main/java/org/bukkit/craftbukkit/inventory/tags/CraftCustomItemTagContainer.java133
-rw-r--r--src/main/java/org/bukkit/craftbukkit/inventory/tags/CraftItemTagAdapterContext.java24
-rw-r--r--src/main/java/org/bukkit/craftbukkit/util/CraftNBTTagConfigSerializer.java80
-rw-r--r--src/test/java/org/bukkit/craftbukkit/inventory/ItemMetaCustomValueTest.java302
7 files changed, 818 insertions, 1 deletions
diff --git a/nms-patches/MojangsonParser.patch b/nms-patches/MojangsonParser.patch
new file mode 100644
index 00000000..abbaae7a
--- /dev/null
+++ b/nms-patches/MojangsonParser.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/server/MojangsonParser.java
++++ b/net/minecraft/server/MojangsonParser.java
+@@ -82,7 +82,7 @@
+ }
+ }
+
+- private NBTBase b(String s) {
++ public NBTBase b(String s) { // PAIL
+ try {
+ if (MojangsonParser.i.matcher(s).matches()) {
+ return new NBTTagFloat(Float.parseFloat(s.substring(0, s.length() - 1)));
+@@ -207,7 +207,7 @@
+ }
+ }
+
+- private NBTBase h() throws CommandSyntaxException {
++ public NBTBase h() throws CommandSyntaxException { // PAIL
+ this.a('[');
+ int i = this.n.getCursor();
+ char c0 = this.n.read();
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<Class, CustomTagAdapter> CREATE_ADAPTER = this::createAdapter;
+
+ private class CustomTagAdapter<T, Z extends NBTBase> {
+
+ private final Function<T, Z> builder;
+ private final Function<Z, T> extractor;
+
+ private final Class<T> primitiveType;
+ private final Class<Z> nbtBaseType;
+
+ public CustomTagAdapter(Class<T> primitiveType, Class<Z> nbtBaseType, Function<T, Z> builder, Function<Z, T> 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<Class, CustomTagAdapter> adapters = new HashMap<>();
+
+ /**
+ * Creates a suitable adapter instance for the primitive class type
+ *
+ * @param type the type to create an adapter for
+ * @param <T> 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 <T> CustomTagAdapter createAdapter(Class<T> 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 <T, Z extends NBTBase> CustomTagAdapter<T, Z> createAdapter(Class<T> primitiveType, Class<Z> nbtBaseType, Function<T, Z> builder, Function<Z, T> 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 <T> 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 <T> NBTBase wrap(Class<T> 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 <T> 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 <T> boolean isInstanceOf(Class<T> 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 <T> 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> T extract(Class<T> 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<String> HANDLED_TAGS = Sets.newHashSet();
+ private static final CraftCustomTagTypeRegistry TAG_TYPE_REGISTRY = new CraftCustomTagTypeRegistry();
private NBTTagCompound internalTag;
private final Map<String, NBTBase> unhandledTags = new HashMap<String, NBTBase>();
+ 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<String> keys = compound.getKeys();
+ for (String key : keys) {
+ publicItemTagContainer.put(key, compound.get(key));
+ }
+ }
Set<String> 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<String, NBTBase> e : unhandledTags.entrySet()) {
itemTag.set(e.getKey(), e.getValue());
}
+
+ if (!publicItemTagContainer.isEmpty()) {
+ NBTTagCompound bukkitCustomCompound = new NBTTagCompound();
+ Map<String, NBTBase> rawPublicMap = publicItemTagContainer.getRaw();
+
+ for (Map.Entry<String, NBTBase> nbtBaseEntry : rawPublicMap.entrySet()) {
+ bukkitCustomCompound.set(nbtBaseEntry.getKey(), nbtBaseEntry.getValue());
+ }
+ itemTag.set(BUKKIT_CUSTOM_TAG.NBT, bukkitCustomCompound);
+ }
}
static NBTTagList createStringList(List<String> 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<Attribute, AttributeModifier> first, Multimap<Attribute, AttributeModifier> 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<String, NBTBase> customTags = new HashMap<>();
+ private final CraftCustomTagTypeRegistry tagTypeRegistry;
+ private final CraftItemTagAdapterContext adapterContext;
+
+ public CraftCustomItemTagContainer(Map<String, NBTBase> customTags, CraftCustomTagTypeRegistry tagTypeRegistry) {
+ this(tagTypeRegistry);
+ this.customTags.putAll(customTags);
+ }
+
+ public CraftCustomItemTagContainer(CraftCustomTagTypeRegistry tagTypeRegistry) {
+ this.tagTypeRegistry = tagTypeRegistry;
+ this.adapterContext = new CraftItemTagAdapterContext(this.tagTypeRegistry);
+ }
+
+ @Override
+ public <T, Z> void setCustomTag(NamespacedKey key, ItemTagType<T, Z> 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 <T, Z> boolean hasCustomTag(NamespacedKey key, ItemTagType<T, Z> 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 <T, Z> Z getCustomTag(NamespacedKey key, ItemTagType<T, Z> 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<String, NBTBase> myRawMap = getRaw();
+ Map<String, NBTBase> theirRawMap = ((CraftCustomItemTagContainer) obj).getRaw();
+
+ return Objects.equals(myRawMap, theirRawMap);
+ }
+
+ public NBTTagCompound toTagCompound() {
+ NBTTagCompound tag = new NBTTagCompound();
+ for (Entry<String, NBTBase> 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<String, NBTBase> map) {
+ this.customTags.putAll(map);
+ }
+
+ public void putAll(NBTTagCompound compound) {
+ for (String key : compound.getKeys()) {
+ this.customTags.put(key, compound.get(key));
+ }
+ }
+
+ public Map<String, NBTBase> 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<String, Object> serialize() {
+ return (Map<String, Object>) 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<String, Object> 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<Object> 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<String, Object> entry : ((Map<String, Object>) object).entrySet()) {
+ compound.set(entry.getKey(), deserialize(entry.getValue()));
+ }
+
+ return compound;
+ } else if (object instanceof List) {
+ List<Object> list = (List<Object>) 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<String, Object> 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<String, NBTBase> 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<byte[], UUID> {
+
+ @Override
+ public Class<byte[]> getPrimitiveType() {
+ return byte[].class;
+ }
+
+ @Override
+ public Class<UUID> 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 <T, Z> void testPrimitiveCustomTag(ItemMeta meta, ItemTagType<T, Z> 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<T> implements ItemTagType<T, T> {
+
+ private final Class<T> primitiveType;
+
+ PrimitiveTagType(Class<T> primitiveType) {
+ this.primitiveType = primitiveType;
+ }
+
+ @Override
+ public Class<T> getPrimitiveType() {
+ return primitiveType;
+ }
+
+ @Override
+ public Class<T> getComplexType() {
+ return primitiveType;
+ }
+
+ @Override
+ public T toPrimitive(T complex, ItemTagAdapterContext context) {
+ return complex;
+ }
+
+ @Override
+ public T fromPrimitive(T primitive, ItemTagAdapterContext context) {
+ return primitive;
+ }
+ }
+}