/* * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. * Copyright (C) 2012 Kristian S. Stangeland * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program; * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA * 02111-1307 USA */ package com.comphenix.protocol.wrappers.nbt; import com.comphenix.protocol.reflect.FieldAccessException; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.StructureModifier; import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract; import com.comphenix.protocol.reflect.instances.DefaultInstances; import com.comphenix.protocol.utility.MinecraftReflection; import com.comphenix.protocol.utility.MinecraftVersion; import com.comphenix.protocol.wrappers.BukkitConverters; import com.comphenix.protocol.wrappers.nbt.io.NbtBinarySerializer; import com.google.common.base.Preconditions; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockState; import org.bukkit.inventory.ItemStack; import javax.annotation.Nonnull; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; /** * Factory methods for creating NBT elements, lists and compounds. * * @author Kristian */ @SuppressWarnings({"unchecked", "rawtypes"}) public class NbtFactory { private static final Map> CONSTRUCTORS = new ConcurrentHashMap<>(); // Used to create the underlying tag private static Method methodCreateTag; private static boolean methodCreateWithName; // Item stack trickery private static StructureModifier itemStackModifier; private static Method getTagType; /** * Attempt to cast this NBT tag as a compund. * * @param tag - the NBT tag to cast. * @return This instance as a compound. * @throws UnsupportedOperationException If this is not a compound. */ public static NbtCompound asCompound(NbtBase tag) { if (tag instanceof NbtCompound) return (NbtCompound) tag; else if (tag != null) throw new UnsupportedOperationException( "Cannot cast a " + tag.getClass() + "( " + tag.getType() + ") to TAG_COMPUND."); else throw new IllegalArgumentException("Tag cannot be NULL."); } /** * Attempt to cast this NBT tag as a list. * * @param tag - the NBT tag to cast. * @return This instance as a list. * @throws UnsupportedOperationException If this is not a list. */ public static NbtList asList(NbtBase tag) { if (tag instanceof NbtList) return (NbtList) tag; else if (tag != null) throw new UnsupportedOperationException( "Cannot cast a " + tag.getClass() + "( " + tag.getType() + ") to TAG_LIST."); else throw new IllegalArgumentException("Tag cannot be NULL."); } /** * Get a NBT wrapper from a NBT base. *

* This may clone the content if the NbtBase is not a NbtWrapper. * * @param Type * @param base - the base class. * @return A NBT wrapper. */ @SuppressWarnings("unchecked") public static NbtWrapper fromBase(NbtBase base) { if (base instanceof NbtWrapper) { return (NbtWrapper) base; } else { if (base.getType() == NbtType.TAG_COMPOUND) { // Load into a NBT-backed wrapper WrappedCompound copy = WrappedCompound.fromName(base.getName()); T value = base.getValue(); copy.setValue((Map>) value); return (NbtWrapper) copy; } else if (base.getType() == NbtType.TAG_LIST) { // As above NbtList copy = WrappedList.fromName(base.getName()); copy.setValue((List>) base.getValue()); return (NbtWrapper) copy; } else { // Copy directly NbtWrapper copy = ofWrapper(base.getType(), base.getName()); copy.setValue(base.getValue()); return copy; } } } /** * Set the NBT compound tag of a given item stack. *

* The item stack must be a wrapper for a CraftItemStack. Use * {@link MinecraftReflection#getBukkitItemStack(Object)} if not. * * @param stack - the item stack, cannot be air. * @param compound - the new NBT compound, or NULL to remove it. * @throws IllegalArgumentException If the stack is not a CraftItemStack, or it represents air. */ public static void setItemTag(ItemStack stack, NbtCompound compound) { checkItemStack(stack); StructureModifier> modifier = getStackModifier(stack); modifier.write(0, compound); } /** * Construct a wrapper for an NBT tag stored (in memory) in an item stack. This is where * auxiliary data such as enchanting, name and lore is stored. It doesn't include the items * material, damage value or count. *

* The item stack must be a wrapper for a CraftItemStack. Use * {@link MinecraftReflection#getBukkitItemStack(Object)} if not. * * @param stack - the item stack. * @return A wrapper for its NBT tag. */ public static NbtWrapper fromItemTag(ItemStack stack) { checkItemStack(stack); StructureModifier> modifier = getStackModifier(stack); NbtBase result = modifier.read(0); // Create the tag if it doesn't exist if (result == null) { result = NbtFactory.ofCompound("tag"); modifier.write(0, result); } return fromBase(result); } /** * Constructs a wrapper for a NBT tag in an ItemStack. This is where auxiliary * data such as enchantments, name, and lore is stored. It doesn't include the material, * damage value, or stack size. *

* This differs from {@link #fromItemTag(ItemStack)} in that the tag is not created if it * doesn't already exist. * * @param stack the ItemStack. Must be a CraftItemStack. Use {@link MinecraftReflection#getBukkitItemStack(Object)} * @return A wrapper for the NBT tag if it exists, an empty Optional if not */ public static Optional> fromItemOptional(ItemStack stack) { checkItemStack(stack); StructureModifier> modifier = getStackModifier(stack); NbtBase result = modifier.read(0); // Create the tag if it doesn't exist if (result == null) { return Optional.empty(); } return Optional.of(fromBase(result)); } /** * Load a NBT compound from a GZIP compressed file. * * @param file - the source file. * @return The compound. * @throws IOException Unable to load file. */ public static NbtCompound fromFile(String file) throws IOException { Preconditions.checkNotNull(file, "file cannot be NULL"); try (FileInputStream stream = new FileInputStream(file); DataInputStream input = new DataInputStream(new GZIPInputStream(stream))) { return NbtBinarySerializer.DEFAULT.deserializeCompound(input); } } /** * Save a NBT compound to a new compressed file, overwriting any existing files in the process. * * @param compound - the compound to save. * @param file - the destination file. * @throws IOException Unable to save compound. */ public static void toFile(NbtCompound compound, String file) throws IOException { Preconditions.checkNotNull(compound, "compound cannot be NULL"); Preconditions.checkNotNull(file, "file cannot be NULL"); try (FileOutputStream stream = new FileOutputStream(file); DataOutputStream output = new DataOutputStream(new GZIPOutputStream(stream))) { NbtBinarySerializer.DEFAULT.serialize(compound, output); } } /** * Retrieve the NBT tile entity that represents the given block. * * @param block - the block. * @return The NBT compound, or NULL if the state doesn't have a tile entity. */ public static NbtCompound readBlockState(Block block) { BlockState state = block.getState(); TileEntityAccessor accessor = TileEntityAccessor.getAccessor(state); return accessor != null ? accessor.readBlockState(state) : null; } /** * Write to the NBT tile entity in the given block. * * @param target - the target block. * @param blockState - the new tile entity. * @throws IllegalArgumentException If the block doesn't contain a tile entity. */ public static void writeBlockState(Block target, NbtCompound blockState) { BlockState state = target.getState(); TileEntityAccessor accessor = TileEntityAccessor.getAccessor(state); if (accessor != null) { accessor.writeBlockState(state, blockState); } else { throw new IllegalArgumentException("Unable to find tile entity in " + target); } } /** * Ensure that the given stack can store arbitrary NBT information. * * @param stack - the stack to check. */ private static void checkItemStack(ItemStack stack) { if (stack == null) throw new IllegalArgumentException("Stack cannot be NULL."); if (!MinecraftReflection.isCraftItemStack(stack)) throw new IllegalArgumentException("Stack must be a CraftItemStack."); if (stack.getType() == Material.AIR) throw new IllegalArgumentException("ItemStacks representing air cannot store NMS information."); } /** * Retrieve a structure modifier that automatically marshalls between NBT wrappers and their NMS counterpart. * * @param stack - the stack that will store the NBT compound. * @return The structure modifier. */ private static StructureModifier> getStackModifier(ItemStack stack) { Object nmsStack = MinecraftReflection.getMinecraftItemStack(stack); if (itemStackModifier == null) { itemStackModifier = new StructureModifier<>(nmsStack.getClass(), Object.class, false); } // Use the first and best NBT tag return itemStackModifier. withTarget(nmsStack). withType(MinecraftReflection.getNBTBaseClass(), BukkitConverters.getNbtConverter()); } /** * Initialize a NBT wrapper. *

* Use {@link #fromNMS(Object, String)} instead. * * @param Type * @param handle - the underlying net.minecraft.server object to wrap. * @return A NBT wrapper. */ @Deprecated public static NbtWrapper fromNMS(Object handle) { WrappedElement partial = new WrappedElement<>(handle); // See if this is actually a compound tag if (partial.getType() == NbtType.TAG_COMPOUND) return (NbtWrapper) new WrappedCompound(handle); else if (partial.getType() == NbtType.TAG_LIST) return new WrappedList(handle); else return partial; } /** * Initialize a NBT wrapper with a name. * * @param Type * @param name - the name of the tag, or NULL if not valid. * @param handle - the underlying net.minecraft.server object to wrap. * @return A NBT wrapper. */ public static NbtWrapper fromNMS(Object handle, String name) { WrappedElement partial = new WrappedElement<>(handle, name); // See if this is actually a compound tag if (partial.getType() == NbtType.TAG_COMPOUND) return (NbtWrapper) new WrappedCompound(handle, name); else if (partial.getType() == NbtType.TAG_LIST) return new WrappedList(handle, name); else return partial; } /** * Retrieve the NBT compound from a given NMS handle. * * @param handle - the underlying net.minecraft.server object to wrap. * @return A NBT compound wrapper */ public static NbtCompound fromNMSCompound(@Nonnull Object handle) { if (handle == null) throw new IllegalArgumentException("handle cannot be NULL."); return (NbtCompound) NbtFactory.>>fromNMS(handle); } /** * Constructs a NBT tag of type string. * * @param name - name of the tag. * @param value - value of the tag. * @return The constructed NBT tag. */ public static NbtBase of(String name, String value) { return ofWrapper(NbtType.TAG_STRING, name, value); } /** * Constructs a NBT tag of type byte. * * @param name - name of the tag. * @param value - value of the tag. * @return The constructed NBT tag. */ public static NbtBase of(String name, byte value) { return ofWrapper(NbtType.TAG_BYTE, name, value); } /** * Constructs a NBT tag of type short. * * @param name - name of the tag. * @param value - value of the tag. * @return The constructed NBT tag. */ public static NbtBase of(String name, short value) { return ofWrapper(NbtType.TAG_SHORT, name, value); } /** * Constructs a NBT tag of type int. * * @param name - name of the tag. * @param value - value of the tag. * @return The constructed NBT tag. */ public static NbtBase of(String name, int value) { return ofWrapper(NbtType.TAG_INT, name, value); } /** * Constructs a NBT tag of type long. * * @param name - name of the tag. * @param value - value of the tag. * @return The constructed NBT tag. */ public static NbtBase of(String name, long value) { return ofWrapper(NbtType.TAG_LONG, name, value); } /** * Constructs a NBT tag of type float. * * @param name - name of the tag. * @param value - value of the tag. * @return The constructed NBT tag. */ public static NbtBase of(String name, float value) { return ofWrapper(NbtType.TAG_FLOAT, name, value); } /** * Constructs a NBT tag of type double. * * @param name - name of the tag. * @param value - value of the tag. * @return The constructed NBT tag. */ public static NbtBase of(String name, double value) { return ofWrapper(NbtType.TAG_DOUBLE, name, value); } /** * Constructs a NBT tag of type byte array. * * @param name - name of the tag. * @param value - value of the tag. * @return The constructed NBT tag. */ public static NbtBase of(String name, byte[] value) { return ofWrapper(NbtType.TAG_BYTE_ARRAY, name, value); } /** * Constructs a NBT tag of type int array. * * @param name - name of the tag. * @param value - value of the tag. * @return The constructed NBT tag. */ public static NbtBase of(String name, int[] value) { return ofWrapper(NbtType.TAG_INT_ARRAY, name, value); } /** * Construct a new NBT compound initialized with a given list of NBT values. * * @param name - the name of the compound wrapper. * @param list - the list of elements to add. * @return The new wrapped NBT compound. */ public static NbtCompound ofCompound(String name, Collection> list) { return WrappedCompound.fromList(name, list); } /** * Construct a new NBT compound wrapper. * * @param name - the name of the compound wrapper. * @return The new wrapped NBT compound. */ public static NbtCompound ofCompound(String name) { return WrappedCompound.fromName(name); } /** * Construct a NBT list of out an array of values. * * @param Type * @param name - name of this list. * @param elements - elements to add. * @return The new filled NBT list. */ @SafeVarargs public static NbtList ofList(String name, T... elements) { return WrappedList.fromArray(name, elements); } /** * Construct a NBT list of out a list of values. * * @param Type * @param name - name of this list. * @param elements - elements to add. * @return The new filled NBT list. */ public static NbtList ofList(String name, Collection elements) { return WrappedList.fromList(name, elements); } /** * Create a new NBT wrapper from a given type. * * @param Type * @param type - the NBT type. * @param name - the name of the NBT tag. * @return The new wrapped NBT tag. * @throws FieldAccessException If we're unable to create the underlying tag. */ public static NbtWrapper ofWrapper(NbtType type, String name) { if (type == null) throw new IllegalArgumentException("type cannot be NULL."); if (type == NbtType.TAG_END) throw new IllegalArgumentException("Cannot create a TAG_END."); if (MinecraftVersion.BEE_UPDATE.atOrAbove()) { return createTagNew(type, name); } if (methodCreateTag == null) { Class base = MinecraftReflection.getNBTBaseClass(); // Use the base class try { methodCreateTag = findCreateMethod(base, byte.class, String.class); methodCreateWithName = true; } catch (Exception e) { methodCreateTag = findCreateMethod(base, byte.class); methodCreateWithName = false; } } try { // Delegate to the correct version if (methodCreateWithName) return createTagWithName(type, name); else return createTagSetName(type, name); } catch (Exception e) { // Inform the caller throw new FieldAccessException(String.format("Cannot create NBT element %s (type: %s)", name, type), e); } } /** * Find the create method of NBTBase. * * @param base - the base NBT. * @param params - the parameters. */ private static Method findCreateMethod(Class base, Class... params) { Method method = FuzzyReflection.fromClass(base, true).getMethodByReturnTypeAndParameters("createTag", base, params); method.setAccessible(true); return method; } // For Minecraft 1.6.4 and below @SuppressWarnings({"unchecked", "rawtypes"}) private static NbtWrapper createTagWithName(NbtType type, String name) throws Exception { Object handle = methodCreateTag.invoke(null, (byte) type.getRawID(), name); if (type == NbtType.TAG_COMPOUND) return (NbtWrapper) new WrappedCompound(handle); else if (type == NbtType.TAG_LIST) return new WrappedList(handle); else return new WrappedElement<>(handle); } // For Minecraft 1.7.2 to 1.14.4 @SuppressWarnings({"unchecked", "rawtypes"}) private static NbtWrapper createTagSetName(NbtType type, String name) throws Exception { Object handle = methodCreateTag.invoke(null, (byte) type.getRawID()); if (type == NbtType.TAG_COMPOUND) return (NbtWrapper) new WrappedCompound(handle, name); else if (type == NbtType.TAG_LIST) return new WrappedList(handle, name); else return new WrappedElement<>(handle, name); } @SafeVarargs private static NbtWrapper createTagNew(NbtType type, String name, T... values) { if (type == NbtType.TAG_END) { throw new IllegalArgumentException("Can't create END tags"); } int nbtId = type.getRawID(); Class valueType = type.getValueType(); Constructor constructor = CONSTRUCTORS.get(type); if (constructor == null) { if (getTagType == null) { Class tagTypes = MinecraftReflection.getNbtTagTypes(); FuzzyReflection fuzzy = FuzzyReflection.fromClass(tagTypes, false); getTagType = fuzzy.getMethod( FuzzyMethodContract.newBuilder().parameterCount(1).parameterExactType(int.class).build()); } Class nbtClass; try { nbtClass = getTagType.invoke(null, nbtId).getClass().getEnclosingClass(); } catch (ReflectiveOperationException ex) { throw new RuntimeException("Failed to determine NBT class from " + type, ex); } try { FuzzyReflection fuzzy = FuzzyReflection.fromClass(nbtClass, true); if (type == NbtType.TAG_LIST) { constructor = fuzzy.getConstructor(FuzzyMethodContract.newBuilder().parameterCount(0).build()); } else { constructor = fuzzy.getConstructor( FuzzyMethodContract.newBuilder().parameterCount(1).parameterSuperOf(valueType).build()); } constructor.setAccessible(true); } catch (Exception ex) { throw new RuntimeException("Failed to find NBT constructor in " + nbtClass, ex); } CONSTRUCTORS.put(type, constructor); } Object handle; T value = values.length > 0 ? values[0] : (T) DefaultInstances.DEFAULT.getDefault(valueType); try { if (type == NbtType.TAG_LIST) { handle = constructor.newInstance(); } else { handle = constructor.newInstance(value); } } catch (Exception ex) { throw new RuntimeException("Failed to create NBT wrapper for " + type, ex); } if (type == NbtType.TAG_COMPOUND) { return (NbtWrapper) new WrappedCompound(handle, name); } else if (type == NbtType.TAG_LIST) { return new WrappedList(handle, name); } else { return new WrappedElement<>(handle, name); } } /** * Create a new NBT wrapper from a given type. * * @param Type * @param type - the NBT type. * @param name - the name of the NBT tag. * @param value - the value of the new tag. * @return The new wrapped NBT tag. * @throws FieldAccessException If we're unable to create the underlying tag. */ public static NbtWrapper ofWrapper(NbtType type, String name, T value) { if (MinecraftVersion.BEE_UPDATE.atOrAbove()) { return createTagNew(type, name, value); } NbtWrapper created = ofWrapper(type, name); // Update the value created.setValue(value); return created; } /** * Create a new NBT wrapper from a given type. * * @param Type * @param type - type of the NBT value. * @param name - the name of the NBT tag. * @param value - the value of the new tag. * @return The new wrapped NBT tag. * @throws FieldAccessException If we're unable to create the underlying tag. * @throws IllegalArgumentException If the given class type is not valid NBT. */ public static NbtWrapper ofWrapper(Class type, String name, T value) { return ofWrapper(NbtType.getTypeFromClass(type), name, value); } }