From 1a9de89d8d96e50821174a988fac741671dc661d Mon Sep 17 00:00:00 2001 From: PseudoKnight Date: Mon, 18 Sep 2023 19:48:07 -0700 Subject: [PATCH] Add support for custom tags in item meta --- .../laytonsmith/abstraction/MCItemMeta.java | 4 + .../abstraction/MCTagContainer.java | 67 ++++++ .../abstraction/bukkit/BukkitMCItemMeta.java | 10 + .../bukkit/BukkitMCTagContainer.java | 164 +++++++++++++ .../abstraction/enums/MCTagType.java | 220 ++++++++++++++++++ .../com/laytonsmith/core/ObjectGenerator.java | 16 ++ .../laytonsmith/core/functions/ItemMeta.java | 2 + src/main/resources/functionDocs/get_itemmeta | 1 + 8 files changed, 484 insertions(+) create mode 100644 src/main/java/com/laytonsmith/abstraction/MCTagContainer.java create mode 100644 src/main/java/com/laytonsmith/abstraction/bukkit/BukkitMCTagContainer.java create mode 100644 src/main/java/com/laytonsmith/abstraction/enums/MCTagType.java diff --git a/src/main/java/com/laytonsmith/abstraction/MCItemMeta.java b/src/main/java/com/laytonsmith/abstraction/MCItemMeta.java index 4dbb25ba2..0e23e54d6 100644 --- a/src/main/java/com/laytonsmith/abstraction/MCItemMeta.java +++ b/src/main/java/com/laytonsmith/abstraction/MCItemMeta.java @@ -128,4 +128,8 @@ public interface MCItemMeta extends AbstractionObject { List getAttributeModifiers(); void setAttributeModifiers(List modifiers); + + boolean hasCustomTags(); + + MCTagContainer getCustomTags(); } diff --git a/src/main/java/com/laytonsmith/abstraction/MCTagContainer.java b/src/main/java/com/laytonsmith/abstraction/MCTagContainer.java new file mode 100644 index 000000000..aab42a63a --- /dev/null +++ b/src/main/java/com/laytonsmith/abstraction/MCTagContainer.java @@ -0,0 +1,67 @@ +package com.laytonsmith.abstraction; + +import com.laytonsmith.abstraction.enums.MCTagType; + +import java.util.Collection; + +/** + * Minecraft NBT containers that can be used to read and modify tags in supported game objects. + * This includes item meta, entities, block entities, chunks, worlds, etc. + */ +public interface MCTagContainer extends AbstractionObject { + + /** + * Returns whether the tag container does not contain any tags. + * @return whether container is empty + */ + boolean isEmpty(); + + /** + * Gets a set of key objects for each tag that exists in this container. + * These are minecraft formatted namespaced keys. (e.g. "namespace:key") + * These key objects can be passed to other methods in this class. + * @return a set of keys + */ + Collection getKeys(); + + /** + * Returns the tag type with the given key. + * MCTagType can be used to convert tags to and from MethodScript constructs. + * Returns null if a tag with that key does not exist. + * @param key the tag key + * @return the type for the tag + */ + MCTagType getType(Object key); + + /** + * Returns the tag value with the given key and tag type. + * Returns null if a tag with that key and type does not exist. + * @param key the tag key + * @param type the tag type + * @return the value for the tag + */ + Object get(Object key, MCTagType type); + + /** + * Sets the tag value with the given key and tag type. + * Throws an IllegalArgumentException if the type and value do not match. + * @param key the tag key + * @param type the tag type + * @param value the tag value + */ + void set(Object key, MCTagType type, Object value); + + /** + * Deletes the tag with the given key from this container. + * @param key the tag key + */ + void remove(Object key); + + /** + * Creates a new tag container from this container context. + * This can then be used to nest a tag container with the {@link #set} method. + * @return a new tag container + */ + MCTagContainer newContainer(); + +} diff --git a/src/main/java/com/laytonsmith/abstraction/bukkit/BukkitMCItemMeta.java b/src/main/java/com/laytonsmith/abstraction/bukkit/BukkitMCItemMeta.java index 7e0eda67f..52f6bd9e4 100644 --- a/src/main/java/com/laytonsmith/abstraction/bukkit/BukkitMCItemMeta.java +++ b/src/main/java/com/laytonsmith/abstraction/bukkit/BukkitMCItemMeta.java @@ -6,6 +6,7 @@ import com.laytonsmith.abstraction.MCAttributeModifier; import com.laytonsmith.abstraction.MCEnchantment; import com.laytonsmith.abstraction.MCItemMeta; +import com.laytonsmith.abstraction.MCTagContainer; import com.laytonsmith.abstraction.blocks.MCBlockData; import com.laytonsmith.abstraction.blocks.MCMaterial; import com.laytonsmith.abstraction.bukkit.blocks.BukkitMCBlockData; @@ -225,4 +226,13 @@ public void setAttributeModifiers(List modifiers) { } im.setAttributeModifiers(map); } + + @Override + public boolean hasCustomTags() { + return !im.getPersistentDataContainer().isEmpty(); + } + + public MCTagContainer getCustomTags() { + return new BukkitMCTagContainer(im.getPersistentDataContainer()); + } } diff --git a/src/main/java/com/laytonsmith/abstraction/bukkit/BukkitMCTagContainer.java b/src/main/java/com/laytonsmith/abstraction/bukkit/BukkitMCTagContainer.java new file mode 100644 index 000000000..816b0782f --- /dev/null +++ b/src/main/java/com/laytonsmith/abstraction/bukkit/BukkitMCTagContainer.java @@ -0,0 +1,164 @@ +package com.laytonsmith.abstraction.bukkit; + +import com.laytonsmith.abstraction.MCTagContainer; +import com.laytonsmith.abstraction.enums.MCTagType; +import com.laytonsmith.commandhelper.CommandHelperPlugin; +import org.bukkit.NamespacedKey; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; + +import java.util.Collection; + +public class BukkitMCTagContainer implements MCTagContainer { + + PersistentDataContainer pdc; + + public BukkitMCTagContainer(PersistentDataContainer pdc) { + this.pdc = pdc; + } + + @Override + public MCTagContainer newContainer() { + return new BukkitMCTagContainer(pdc.getAdapterContext().newPersistentDataContainer()); + } + + @Override + public boolean isEmpty() { + return this.pdc.isEmpty(); + } + + @Override + public Collection getKeys() { + return pdc.getKeys(); + } + + @Override + public MCTagType getType(Object key) { + NamespacedKey namespacedKey = NamespacedKey(key); + // Check tag types in order of most frequently used + if(pdc.has(namespacedKey, PersistentDataType.STRING)) { + return MCTagType.STRING; + } else if(pdc.has(namespacedKey, PersistentDataType.INTEGER)) { + return MCTagType.INTEGER; + } else if(pdc.has(namespacedKey, PersistentDataType.BYTE)) { + return MCTagType.BYTE; + } else if(pdc.has(namespacedKey, PersistentDataType.DOUBLE)) { + return MCTagType.DOUBLE; + } else if(pdc.has(namespacedKey, PersistentDataType.LONG)) { + return MCTagType.LONG; + } else if(pdc.has(namespacedKey, PersistentDataType.FLOAT)) { + return MCTagType.FLOAT; + } else if(pdc.has(namespacedKey, PersistentDataType.TAG_CONTAINER)) { + return MCTagType.TAG_CONTAINER; + } else if(pdc.has(namespacedKey, PersistentDataType.BYTE_ARRAY)) { + return MCTagType.BYTE_ARRAY; + } else if(pdc.has(namespacedKey, PersistentDataType.SHORT)) { + return MCTagType.SHORT; + } else if(pdc.has(namespacedKey, PersistentDataType.INTEGER_ARRAY)) { + return MCTagType.INTEGER_ARRAY; + } else if(pdc.has(namespacedKey, PersistentDataType.LONG_ARRAY)) { + return MCTagType.LONG_ARRAY; + } else if(pdc.has(namespacedKey, PersistentDataType.TAG_CONTAINER_ARRAY)) { + return MCTagType.TAG_CONTAINER_ARRAY; + } + return null; + } + + @Override + public Object get(Object key, MCTagType type) { + PersistentDataType bukkitType = GetPersistentDataType(type); + Object value = pdc.get(NamespacedKey(key), bukkitType); + if(value instanceof PersistentDataContainer) { + return new BukkitMCTagContainer((PersistentDataContainer) value); + } else if(value instanceof PersistentDataContainer[] concreteContainers) { + MCTagContainer[] abstractContainers = new MCTagContainer[concreteContainers.length]; + for(int i = 0; i < concreteContainers.length; i++) { + abstractContainers[i] = new BukkitMCTagContainer(concreteContainers[i]); + } + return abstractContainers; + } + return value; + } + + @Override + public void set(Object key, MCTagType type, Object value) { + PersistentDataType bukkitType = GetPersistentDataType(type); + if(value instanceof MCTagContainer) { + value = ((MCTagContainer) value).getHandle(); + } else if(value instanceof MCTagContainer[] abstractContainers) { + PersistentDataContainer[] concreteContainers = new PersistentDataContainer[abstractContainers.length]; + for(int i = 0; i < abstractContainers.length; i++) { + concreteContainers[i] = (PersistentDataContainer) abstractContainers[i].getHandle(); + } + value = concreteContainers; + } + pdc.set(NamespacedKey(key), bukkitType, value); + } + + @Override + public void remove(Object key) { + pdc.remove(NamespacedKey(key)); + } + + private static NamespacedKey NamespacedKey(Object key) { + if(key instanceof NamespacedKey) { + return (NamespacedKey) key; + } else if(key instanceof String) { + NamespacedKey namespacedKey = NamespacedKey.fromString((String) key, CommandHelperPlugin.self); + if(namespacedKey != null) { + return namespacedKey; + } + } + throw new IllegalArgumentException("Invalid namespaced key."); + } + + private static PersistentDataType GetPersistentDataType(MCTagType type) { + switch(type) { + case BYTE: + return PersistentDataType.BYTE; + case BYTE_ARRAY: + return PersistentDataType.BYTE_ARRAY; + case DOUBLE: + return PersistentDataType.DOUBLE; + case FLOAT: + return PersistentDataType.FLOAT; + case INTEGER: + return PersistentDataType.INTEGER; + case INTEGER_ARRAY: + return PersistentDataType.INTEGER_ARRAY; + case LONG: + return PersistentDataType.LONG; + case LONG_ARRAY: + return PersistentDataType.LONG_ARRAY; + case SHORT: + return PersistentDataType.SHORT; + case STRING: + return PersistentDataType.STRING; + case TAG_CONTAINER: + return PersistentDataType.TAG_CONTAINER; + case TAG_CONTAINER_ARRAY: + return PersistentDataType.TAG_CONTAINER_ARRAY; + } + throw new IllegalArgumentException("Invalid persistent data type: " + type.name()); + } + + @Override + public Object getHandle() { + return pdc; + } + + @Override + public boolean equals(Object o) { + return o instanceof MCTagContainer && this.pdc.equals(((MCTagContainer) o).getHandle()); + } + + @Override + public int hashCode() { + return pdc.hashCode(); + } + + @Override + public String toString() { + return pdc.toString(); + } +} diff --git a/src/main/java/com/laytonsmith/abstraction/enums/MCTagType.java b/src/main/java/com/laytonsmith/abstraction/enums/MCTagType.java new file mode 100644 index 000000000..fc3f8df11 --- /dev/null +++ b/src/main/java/com/laytonsmith/abstraction/enums/MCTagType.java @@ -0,0 +1,220 @@ +package com.laytonsmith.abstraction.enums; + +import com.laytonsmith.abstraction.MCTagContainer; +import com.laytonsmith.core.ArgumentValidation; +import com.laytonsmith.core.constructs.CArray; +import com.laytonsmith.core.constructs.CDouble; +import com.laytonsmith.core.constructs.CInt; +import com.laytonsmith.core.constructs.CString; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.exceptions.CRE.CRECastException; +import com.laytonsmith.core.exceptions.CRE.CREFormatException; +import com.laytonsmith.core.natives.interfaces.Mixed; + +import java.util.function.Function; + +/** + * Minecraft NBT types, with functions to convert to and from MethodScript constructs. + */ +public enum MCTagType { + BYTE( + (Mixed v) -> ArgumentValidation.getInt8(v, v.getTarget()), + (Byte v) -> new CInt(((Number) v).longValue(), Target.UNKNOWN)), + BYTE_ARRAY( + (Mixed v) -> { + CArray array = ArgumentValidation.getArray(v, v.getTarget()); + if(array.isAssociative()) { + throw new CRECastException("Expected byte array to not be associative.", v.getTarget()); + } + byte[] bytes = new byte[(int) array.size()]; + int i = 0; + for(Mixed m : array) { + bytes[i++] = ArgumentValidation.getInt8(m, m.getTarget()); + } + return bytes; + }, + (byte[] array) -> { + CArray r = new CArray(Target.UNKNOWN); + for(int i : array) { + r.push(new CInt(i, Target.UNKNOWN), Target.UNKNOWN); + } + return r; + }), + DOUBLE( + (Mixed v) -> ArgumentValidation.getDouble(v, v.getTarget()), + (Double v) -> new CDouble(v, Target.UNKNOWN)), + FLOAT( + (Mixed v) -> ArgumentValidation.getDouble32(v, v.getTarget()), + (Float v) -> new CDouble(v.doubleValue(), Target.UNKNOWN)), + INTEGER( + (Mixed v) -> ArgumentValidation.getInt32(v, v.getTarget()), + (Integer v) -> new CInt(((Number) v).longValue(), Target.UNKNOWN)), + INTEGER_ARRAY( + (Mixed v) -> { + CArray array = ArgumentValidation.getArray(v, v.getTarget()); + if(array.isAssociative()) { + throw new CRECastException("Expected integer array to not be associative.", v.getTarget()); + } + int[] ints = new int[(int) array.size()]; + int i = 0; + for(Mixed m : array) { + ints[i++] = ArgumentValidation.getInt32(m, m.getTarget()); + } + return ints; + }, + (int[] array) -> { + CArray r = new CArray(Target.UNKNOWN); + for(int i : array) { + r.push(new CInt(i, Target.UNKNOWN), Target.UNKNOWN); + } + return r; + }), + LONG( + (Mixed v) -> ArgumentValidation.getInt(v, v.getTarget()), + (Long v) -> new CInt(((Number) v).longValue(), Target.UNKNOWN)), + LONG_ARRAY( + (Mixed v) -> { + CArray array = ArgumentValidation.getArray(v, v.getTarget()); + if(array.isAssociative()) { + throw new CRECastException("Expected long array to not be associative.", v.getTarget()); + } + long[] longs = new long[(int) array.size()]; + int i = 0; + for(Mixed m : array) { + longs[i++] = ArgumentValidation.getInt(m, m.getTarget()); + } + return longs; + }, + (long[] array) -> { + CArray ret = new CArray(Target.UNKNOWN); + for(long i : array) { + ret.push(new CInt(i, Target.UNKNOWN), Target.UNKNOWN); + } + return ret; + }), + SHORT( + (Mixed v) -> ArgumentValidation.getInt16(v, v.getTarget()), + (Short v) -> new CInt(((Number) v).longValue(), Target.UNKNOWN)), + STRING( + (Mixed v) -> v.val(), + (String v) -> new CString(v, Target.UNKNOWN)), + TAG_CONTAINER( + (Mixed v) -> { + throw new UnsupportedOperationException(); + }, + (MCTagContainer v) -> { + throw new UnsupportedOperationException(); + }), + TAG_CONTAINER_ARRAY( + (Mixed v) -> { + throw new UnsupportedOperationException(); + }, + (MCTagContainer[] v) -> { + throw new UnsupportedOperationException(); + }); + + private final Function conversion; + private final Function construction; + + MCTagType(Function conversion, Function construction) { + this.conversion = conversion; + this.construction = construction; + } + + /** + * Returns a Java object from a MethodScript construct. + * Throws a CRE if the passed value is not valid for the specific tag type. + * @param container the tag container context + * @param value MethodScript construct + * @return a Java object + */ + public Object convert(MCTagContainer container, Mixed value) throws ClassCastException { + if(this == TAG_CONTAINER) { + if(!value.isInstanceOf(CArray.TYPE)) { + throw new CREFormatException("Expected tag container to be an array.", value.getTarget()); + } + CArray containerArray = (CArray) value; + if(!containerArray.isAssociative()) { + throw new CREFormatException("Expected tag container array to be associative.", value.getTarget()); + } + for(String key : containerArray.stringKeySet()) { + Mixed possibleArray = containerArray.get(key, value.getTarget()); + if(!possibleArray.isInstanceOf(CArray.TYPE)) { + throw new CREFormatException("Expected tag entry to be an array.", possibleArray.getTarget()); + } + CArray entryArray = (CArray) possibleArray; + if(!entryArray.isAssociative()) { + throw new CREFormatException("Expected tag array to be associative.", entryArray.getTarget()); + } + Mixed entryType = entryArray.get("type", entryArray.getTarget()); + Mixed entryValue = entryArray.get("value", entryArray.getTarget()); + MCTagType tagType; + try { + tagType = MCTagType.valueOf(entryType.val()); + } catch (IllegalArgumentException ex) { + throw new CREFormatException("Tag type is not valid: " + entryType.val(), entryType.getTarget()); + } + Object tagValue; + if(tagType == MCTagType.TAG_CONTAINER) { + tagValue = tagType.convert(container.newContainer(), entryValue); + } else if(tagType == TAG_CONTAINER_ARRAY) { + tagValue = tagType.convert(container, entryValue); + } else { + tagValue = tagType.convert(container, entryValue); + } + try { + container.set(key, tagType, tagValue); + } catch (ClassCastException ex) { + throw new CREFormatException("Tag value does not match expected type.", entryValue.getTarget()); + } catch (IllegalArgumentException ex) { + throw new CREFormatException(ex.getMessage(), entryValue.getTarget()); + } + } + return container; + } else if(this == TAG_CONTAINER_ARRAY) { + if(!value.isInstanceOf(CArray.TYPE)) { + throw new CREFormatException("Expected tag container to be an array.", value.getTarget()); + } + CArray array = (CArray) value; + if(array.isAssociative()) { + throw new CREFormatException("Expected tag container array to not be associative.", array.getTarget()); + } + MCTagContainer[] containers = new MCTagContainer[(int) array.size()]; + int i = 0; + for(Mixed possibleContainer : array) { + containers[i++] = (MCTagContainer) TAG_CONTAINER.convert(container.newContainer(), possibleContainer); + } + return containers; + } + return conversion.apply(value); + } + + /** + * Returns a MethodScript construct from a Java object. + * Throws a ClassCastException if the passed value does not match the specific tag type. + * @param value a valid Java object + * @return a MethodScript construct + */ + public Mixed construct(Object value) throws ClassCastException { + if(this == TAG_CONTAINER) { + MCTagContainer container = (MCTagContainer) value; + CArray containerArray = CArray.GetAssociativeArray(Target.UNKNOWN); + for(Object key : container.getKeys()) { + CArray entry = CArray.GetAssociativeArray(Target.UNKNOWN); + MCTagType type = container.getType(key); + entry.set("type", type.name(), Target.UNKNOWN); + entry.set("value", type.construct(container.get(key, type)), Target.UNKNOWN); + containerArray.set(key.toString(), entry, Target.UNKNOWN); + } + return containerArray; + } else if(this == TAG_CONTAINER_ARRAY) { + MCTagContainer[] containers = (MCTagContainer[]) value; + CArray array = new CArray(Target.UNKNOWN, containers.length); + for(MCTagContainer container : containers) { + array.push(TAG_CONTAINER.construct(container), Target.UNKNOWN); + } + return array; + } + return (Mixed) construction.apply(value); + } +} diff --git a/src/main/java/com/laytonsmith/core/ObjectGenerator.java b/src/main/java/com/laytonsmith/core/ObjectGenerator.java index 2ceede08b..30e5c7751 100644 --- a/src/main/java/com/laytonsmith/core/ObjectGenerator.java +++ b/src/main/java/com/laytonsmith/core/ObjectGenerator.java @@ -71,6 +71,7 @@ import com.laytonsmith.abstraction.enums.MCPotionEffectType; import com.laytonsmith.abstraction.enums.MCPotionType; import com.laytonsmith.abstraction.enums.MCRecipeType; +import com.laytonsmith.abstraction.enums.MCTagType; import com.laytonsmith.abstraction.enums.MCTrimMaterial; import com.laytonsmith.abstraction.enums.MCTrimPattern; import com.laytonsmith.core.constructs.CArray; @@ -465,6 +466,12 @@ public Construct itemMeta(MCItemStack is, Target t) { ma.set("modifiers", modifiers, t); } + if(meta.hasCustomTags()) { + ma.set("tags", MCTagType.TAG_CONTAINER.construct(meta.getCustomTags()), t); + } else { + ma.set("tags", CNull.NULL, t); + } + MCMaterial material = is.getType(); if(material.getMaxDurability() > 0) { // Damageable items only @@ -879,6 +886,15 @@ public MCItemMeta itemMeta(Mixed c, MCMaterial mat, Target t) throws ConfigRunti } } + if(ma.containsKey("tags")) { + Mixed tagArray = ma.get("tags", t); + if(tagArray instanceof CNull) { + // no custom tags + } else { + MCTagType.TAG_CONTAINER.convert(meta.getCustomTags(), tagArray); + } + } + // Damageable items only if(mat.getMaxDurability() > 0) { if(ma.containsKey("damage")) { diff --git a/src/main/java/com/laytonsmith/core/functions/ItemMeta.java b/src/main/java/com/laytonsmith/core/functions/ItemMeta.java index c385b7524..97d3ca240 100644 --- a/src/main/java/com/laytonsmith/core/functions/ItemMeta.java +++ b/src/main/java/com/laytonsmith/core/functions/ItemMeta.java @@ -20,6 +20,7 @@ import com.laytonsmith.abstraction.enums.MCItemFlag; import com.laytonsmith.abstraction.enums.MCPatternShape; import com.laytonsmith.abstraction.enums.MCPotionType; +import com.laytonsmith.abstraction.enums.MCTagType; import com.laytonsmith.abstraction.enums.MCTrimMaterial; import com.laytonsmith.abstraction.enums.MCTrimPattern; import com.laytonsmith.annotations.api; @@ -128,6 +129,7 @@ public String docs() { docs = docs.replace("%AXOLOTL_TYPES%", StringUtils.Join(MCAxolotlType.values(), ", ", ", or ", " or ")); docs = docs.replace("%TRIM_PATTERNS%", StringUtils.Join(MCTrimPattern.values(), ", ", ", or ", " or ")); docs = docs.replace("%TRIM_MATERIALS%", StringUtils.Join(MCTrimMaterial.values(), ", ", ", or ", " or ")); + docs = docs.replace("%TAG_TYPES%", StringUtils.Join(MCTagType.values(), ", ", ", or ", " or ")); return docs; } diff --git a/src/main/resources/functionDocs/get_itemmeta b/src/main/resources/functionDocs/get_itemmeta index cc102ba3b..f485dde23 100644 --- a/src/main/resources/functionDocs/get_itemmeta +++ b/src/main/resources/functionDocs/get_itemmeta @@ -16,6 +16,7 @@ Below are the available fields in the item meta array. Fields can be null when t * '''flags''' : (array) Possible flags: ''%ITEM_FLAGS%''. * '''repair''' : (int) The cost to repair or combine this item in an anvil. * '''modifiers''' : (array) An array of attribute modifier arrays, each with keys: '''"attribute"''', '''"operation"''', '''"amount"''' (double), '''"uuid"''' (optional), '''"name"''' (optional), and '''"slot"''' (optional). Possible attributes: ''%ATTRIBUTES%''. Possible operations: ''%OPERATIONS%''. Possible slots: ''%SLOTS%''. +* '''tags''' : (array) An associative array of custom tags. A tag's key is namespaced (e.g. "commandhelper:mytag") and the value is an associative array containing the '''"type"''' and '''"value"''' of the tag. Possible types: ''%TAG_TYPES%''. |- | All Damageable Items |