From 671654aaaf914ee826117cc190e1d9899a46c41e Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Mon, 7 Jan 2013 10:14:25 +0100 Subject: [PATCH] Add support for reading and writing NBT tags in packets. This wraps the internal NBT read/write system in Minecraft. --- .../protocol/events/PacketContainer.java | 12 + .../reflect/compiler/BackgroundCompiler.java | 2 - .../protocol/utility/MinecraftReflection.java | 8 + .../protocol/wrappers/BukkitConverters.java | 28 ++ .../wrappers/nbt/AbstractConverted.java | 34 ++ .../wrappers/nbt/ConvertedCollection.java | 119 +++++ .../protocol/wrappers/nbt/ConvertedList.java | 138 ++++++ .../protocol/wrappers/nbt/ConvertedMap.java | 139 ++++++ .../protocol/wrappers/nbt/ConvertedSet.java | 10 + .../protocol/wrappers/nbt/NbtBase.java | 52 +++ .../protocol/wrappers/nbt/NbtCompound.java | 440 ++++++++++++++++++ .../protocol/wrappers/nbt/NbtElement.java | 213 +++++++++ .../protocol/wrappers/nbt/NbtFactory.java | 250 ++++++++++ .../protocol/wrappers/nbt/NbtList.java | 311 +++++++++++++ .../protocol/wrappers/nbt/NbtType.java | 141 ++++++ .../protocol/wrappers/nbt/NbtWrapper.java | 24 + .../protocol/wrappers/nbt/NbtFactoryTest.java | 46 ++ 17 files changed, 1965 insertions(+), 2 deletions(-) create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/AbstractConverted.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/ConvertedCollection.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/ConvertedList.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/ConvertedMap.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/ConvertedSet.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtBase.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtCompound.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtElement.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtFactory.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtList.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtType.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtWrapper.java create mode 100644 ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/nbt/NbtFactoryTest.java diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java index b819ee04..1751b370 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java @@ -56,6 +56,7 @@ import com.comphenix.protocol.wrappers.BukkitConverters; import com.comphenix.protocol.wrappers.ChunkPosition; import com.comphenix.protocol.wrappers.WrappedDataWatcher; import com.comphenix.protocol.wrappers.WrappedWatchableObject; +import com.comphenix.protocol.wrappers.nbt.NbtWrapper; import com.google.common.base.Function; import com.google.common.collect.Maps; @@ -364,6 +365,17 @@ public class PacketContainer implements Serializable { ChunkPosition.getConverter()); } + /** + * Retrieves a read/write structure for NBT classes. + * @return A modifier for NBT classes. + */ + public StructureModifier> getNbtModifier() { + // Allow access to the NBT class in packet 130 + return structureModifier.withType( + MinecraftReflection.getNBTBaseClass(), + BukkitConverters.getNbtConverter()); + } + /** * Retrieves a read/write structure for collections of chunk positions. *

diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java index e8447303..4359e080 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java @@ -24,8 +24,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; import javax.annotation.Nullable; diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java index 31094d57..f9924a3a 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -376,6 +376,14 @@ public class MinecraftReflection { return getMinecraftClass("WatchableObject"); } + /** + * Retrieve the NBT base class. + * @return The NBT base class. + */ + public static Class getNBTBaseClass() { + return getMinecraftClass("NBTBase"); + } + /** * Retrieve the ItemStack[] class. * @return The ItemStack[] class. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java index 7d6f421e..7b78bb53 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java @@ -34,6 +34,8 @@ import com.comphenix.protocol.reflect.EquivalentConverter; import com.comphenix.protocol.reflect.FieldAccessException; import com.comphenix.protocol.reflect.instances.DefaultInstances; import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.wrappers.nbt.NbtFactory; +import com.comphenix.protocol.wrappers.nbt.NbtWrapper; /** * Contains several useful equivalent converters for normal Bukkit types. @@ -208,6 +210,32 @@ public class BukkitConverters { }); } + /** + * Retrieve an equivalent converter for net.minecraft.server NBT classes and their wrappers. + * @return An equivalent converter for NBT. + */ + public static EquivalentConverter> getNbtConverter() { + return getIgnoreNull(new EquivalentConverter>() { + @Override + public Object getGeneric(Class genericType, NbtWrapper specific) { + return specific.getHandle(); + } + + @Override + public NbtWrapper getSpecific(Object generic) { + return NbtFactory.fromNMS(generic); + } + + @Override + @SuppressWarnings("unchecked") + public Class> getSpecificType() { + // Damn you Java AGAIN + Class dummy = NbtWrapper.class; + return (Class>) dummy; + } + }); + } + /** * Retrieve a converter for NMS entities and Bukkit entities. * @param world - the current world. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/AbstractConverted.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/AbstractConverted.java new file mode 100644 index 00000000..9084f2bf --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/AbstractConverted.java @@ -0,0 +1,34 @@ +package com.comphenix.protocol.wrappers.nbt; + +import javax.annotation.Nullable; + +import com.google.common.base.Function; + +abstract class AbstractConverted { + /** + * Convert a value from the inner map to the outer visible map. + * @param inner - the inner value. + * @return The outer value. + */ + protected abstract VOuter toOuter(VInner inner); + + /** + * Convert a value from the outer map to the internal inner map. + * @param outer - the outer value. + * @return The inner value. + */ + protected abstract VInner toInner(VOuter outer); + + /** + * Retrieve a function delegate that converts inner objects to outer objects. + * @return A function delegate. + */ + protected Function getOuterConverter() { + return new Function() { + @Override + public VOuter apply(@Nullable VInner param) { + return toOuter(param); + } + }; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/ConvertedCollection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/ConvertedCollection.java new file mode 100644 index 00000000..7faf22de --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/ConvertedCollection.java @@ -0,0 +1,119 @@ +package com.comphenix.protocol.wrappers.nbt; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import com.google.common.collect.Iterators; +import com.google.common.collect.Lists; + +abstract class ConvertedCollection extends AbstractConverted implements Collection { + // Inner collection + private Collection inner; + + public ConvertedCollection(Collection inner) { + this.inner = inner; + } + + @Override + public boolean add(VOuter e) { + return inner.add(toInner(e)); + } + + @Override + public boolean addAll(Collection c) { + boolean modified = false; + + for (VOuter outer : c) + modified |= add(outer); + return modified; + } + + @Override + public void clear() { + inner.clear(); + } + + @Override + @SuppressWarnings("unchecked") + public boolean contains(Object o) { + return inner.contains(toInner((VOuter) o)); + } + + @Override + public boolean containsAll(Collection c) { + for (Object outer : c) { + if (!contains(outer)) + return false; + } + return true; + } + + @Override + public boolean isEmpty() { + return inner.isEmpty(); + } + + @Override + public Iterator iterator() { + return Iterators.transform(inner.iterator(), getOuterConverter()); + } + + @Override + @SuppressWarnings("unchecked") + public boolean remove(Object o) { + return inner.remove(toInner((VOuter) o)); + } + + @Override + public boolean removeAll(Collection c) { + boolean modified = false; + + for (Object outer : c) + modified |= remove(outer); + return modified; + } + + @Override + @SuppressWarnings("unchecked") + public boolean retainAll(Collection c) { + List innerCopy = Lists.newArrayList(); + + // Convert all the elements + for (Object outer : c) + innerCopy.add(toInner((VOuter) outer)); + return inner.retainAll(innerCopy); + } + + @Override + public int size() { + return inner.size(); + } + + @Override + @SuppressWarnings("unchecked") + public Object[] toArray() { + Object[] array = inner.toArray(); + + for (int i = 0; i < array.length; i++) + array[i] = toOuter((VInner) array[i]); + return array; + } + + @Override + @SuppressWarnings("unchecked") + public T[] toArray(T[] a) { + T[] array = a; + int index = 0; + + if (array.length < size()) { + array = (T[]) Array.newInstance(a.getClass().getComponentType(), size()); + } + + // Build the output array + for (VInner innerValue : inner) + array[index++] = (T) toOuter(innerValue); + return array; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/ConvertedList.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/ConvertedList.java new file mode 100644 index 00000000..5fcf0286 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/ConvertedList.java @@ -0,0 +1,138 @@ +package com.comphenix.protocol.wrappers.nbt; + +import java.util.Collection; +import java.util.List; +import java.util.ListIterator; + +abstract class ConvertedList extends ConvertedCollection implements List { + private List inner; + + public ConvertedList(List inner) { + super(inner); + this.inner = inner; + } + + @Override + public void add(int index, VOuter element) { + inner.add(index, toInner(element)); + } + + @Override + public boolean addAll(int index, Collection c) { + return inner.addAll(index, getInnerCollection(c)); + } + + @Override + public VOuter get(int index) { + return toOuter(inner.get(index)); + } + + @Override + @SuppressWarnings("unchecked") + public int indexOf(Object o) { + return inner.indexOf(toInner((VOuter) o)); + } + + @Override + @SuppressWarnings("unchecked") + public int lastIndexOf(Object o) { + return inner.lastIndexOf(toInner((VOuter) o)); + } + + @Override + public ListIterator listIterator() { + return listIterator(0); + } + + @Override + public ListIterator listIterator(int index) { + final ListIterator innerIterator = inner.listIterator(index); + + return new ListIterator() { + @Override + public void add(VOuter e) { + innerIterator.add(toInner(e)); + } + + @Override + public boolean hasNext() { + return innerIterator.hasNext(); + } + + @Override + public boolean hasPrevious() { + return innerIterator.hasPrevious(); + } + + @Override + public VOuter next() { + return toOuter(innerIterator.next()); + } + + @Override + public int nextIndex() { + return innerIterator.nextIndex(); + } + + @Override + public VOuter previous() { + return toOuter(innerIterator.previous()); + } + + @Override + public int previousIndex() { + return innerIterator.previousIndex(); + } + + @Override + public void remove() { + innerIterator.remove(); + } + + @Override + public void set(VOuter e) { + innerIterator.set(toInner(e)); + } + }; + } + + @Override + public VOuter remove(int index) { + return toOuter(inner.remove(index)); + } + + @Override + public VOuter set(int index, VOuter element) { + return toOuter(inner.set(index, toInner(element))); + } + + @Override + public List subList(int fromIndex, int toIndex) { + return new ConvertedList(inner.subList(fromIndex, toIndex)) { + @Override + protected VInner toInner(VOuter outer) { + return ConvertedList.this.toInner(outer); + } + + @Override + protected VOuter toOuter(VInner inner) { + return ConvertedList.this.toOuter(inner); + } + }; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private ConvertedCollection getInnerCollection(Collection c) { + return new ConvertedCollection(c) { + @Override + protected VOuter toInner(VInner outer) { + return ConvertedList.this.toOuter(outer); + } + + @Override + protected VInner toOuter(VOuter inner) { + return ConvertedList.this.toInner(inner); + } + }; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/ConvertedMap.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/ConvertedMap.java new file mode 100644 index 00000000..62ec3ffa --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/ConvertedMap.java @@ -0,0 +1,139 @@ +package com.comphenix.protocol.wrappers.nbt; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +abstract class ConvertedMap extends AbstractConverted implements Map { + // Inner map + private Map inner; + + public ConvertedMap(Map inner) { + if (inner == null) + throw new IllegalArgumentException("Inner map cannot be NULL."); + this.inner = inner; + } + + @Override + public void clear() { + inner.clear(); + } + + @Override + public boolean containsKey(Object key) { + return inner.containsKey(key); + } + + @Override + @SuppressWarnings("unchecked") + public boolean containsValue(Object value) { + return inner.containsValue(toInner((VOuter) value)); + } + + @Override + public Set> entrySet() { + return new ConvertedSet, Entry>(inner.entrySet()) { + @Override + protected Entry toInner(final Entry outer) { + return new Entry() { + @Override + public Key getKey() { + return outer.getKey(); + } + + @Override + public VInner getValue() { + return ConvertedMap.this.toInner(outer.getValue()); + } + + @Override + public VInner setValue(VInner value) { + return ConvertedMap.this.toInner(outer.setValue(ConvertedMap.this.toOuter(value))); + } + + @Override + public String toString() { + return String.format("\"%s\": %s", getKey(), getValue()); + } + }; + } + + @Override + protected Entry toOuter(final Entry inner) { + return new Entry() { + @Override + public Key getKey() { + return inner.getKey(); + } + + @Override + public VOuter getValue() { + return ConvertedMap.this.toOuter(inner.getValue()); + } + + @Override + public VOuter setValue(VOuter value) { + return ConvertedMap.this.toOuter(inner.setValue(ConvertedMap.this.toInner(value))); + } + + @Override + public String toString() { + return String.format("\"%s\": %s", getKey(), getValue()); + } + }; + } + }; + } + + @Override + public VOuter get(Object key) { + return toOuter(inner.get(key)); + } + + @Override + public boolean isEmpty() { + return inner.isEmpty(); + } + + @Override + public Set keySet() { + return inner.keySet(); + } + + @Override + public VOuter put(Key key, VOuter value) { + return toOuter(inner.put(key, toInner(value))); + } + + @Override + public void putAll(Map m) { + for (Entry entry : m.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public VOuter remove(Object key) { + return toOuter(inner.remove(key)); + } + + @Override + public int size() { + return inner.size(); + } + + @Override + public Collection values() { + return new ConvertedCollection(inner.values()) { + @Override + protected VOuter toOuter(VInner inner) { + return ConvertedMap.this.toOuter(inner); + } + + @Override + protected VInner toInner(VOuter outer) { + return ConvertedMap.this.toInner(outer); + } + }; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/ConvertedSet.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/ConvertedSet.java new file mode 100644 index 00000000..10e86287 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/ConvertedSet.java @@ -0,0 +1,10 @@ +package com.comphenix.protocol.wrappers.nbt; + +import java.util.Collection; +import java.util.Set; + +abstract class ConvertedSet extends ConvertedCollection implements Set { + public ConvertedSet(Collection inner) { + super(inner); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtBase.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtBase.java new file mode 100644 index 00000000..c6925309 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtBase.java @@ -0,0 +1,52 @@ +package com.comphenix.protocol.wrappers.nbt; + +import com.comphenix.protocol.wrappers.nbt.NbtBase; +import com.comphenix.protocol.wrappers.nbt.NbtType; + +/** + * Represents a generic container for an NBT element. + * @author Kristian + * + * @param - type of the value that is stored. + */ +public interface NbtBase { + /** + * Retrieve the type of this NBT element. + * @return The type of this NBT element. + */ + public abstract NbtType getType(); + + /** + * Retrieve the name of this NBT tag. + *

+ * This will be an empty string if the NBT tag is stored in a list. + * @return Name of the tag. + */ + public abstract String getName(); + + /** + * Set the name of this NBT tag. + *

+ * This will be ignored if the NBT tag is stored in a list. + * @param name - name of the tag. + */ + public abstract void setName(String name); + + /** + * Retrieve the value of this NBT tag. + * @return Value of this tag. + */ + public abstract TType getValue(); + + /** + * Set the value of this NBT tag. + * @param newValue - the new value of this tag. + */ + public abstract void setValue(TType newValue); + + /** + * Clone the current NBT tag. + * @return The cloned tag. + */ + public abstract NbtBase clone(); +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtCompound.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtCompound.java new file mode 100644 index 00000000..0da68c11 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtCompound.java @@ -0,0 +1,440 @@ +package com.comphenix.protocol.wrappers.nbt; + +import java.io.DataOutput; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * Represents a mapping of arbitrary NBT elements and their unique names. + *

+ * Use {@link NbtFactory} to load or create an instance. + * + * @author Kristian + */ +public class NbtCompound implements NbtWrapper>>, Iterable> { + // A list container + private NbtElement> container; + + // Saved wrapper map + private ConvertedMap> savedMap; + + /** + * Construct a new NBT compound wrapper. + * @param name - the name of the wrapper. + * @return The wrapped NBT compound. + */ + public static NbtCompound fromName(String name) { + // Simplify things for the caller + return (NbtCompound) NbtFactory.>>ofType(NbtType.TAG_COMPOUND, name); + } + + /** + * Construct a new NBT compound wrapper 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 fromList(String name, Collection> list) { + NbtCompound copy = new NbtCompound(name); + + for (NbtBase base : list) + copy.getValue().put(base.getName(), base); + return copy; + } + + /** + * Construct a wrapped compound from a given NMS handle. + * @param handle - the NMS handle. + */ + NbtCompound(Object handle) { + this.container = new NbtElement>(handle); + } + + @Override + public Object getHandle() { + return container.getHandle(); + } + + @Override + public NbtType getType() { + return NbtType.TAG_COMPOUND; + } + + @Override + public String getName() { + return container.getName(); + } + + @Override + public void setName(String name) { + container.setName(name); + } + + /** + * Retrieve a Set view of the keys of each entry in this compound. + * @return The keys of each entry. + */ + public Set getKeys() { + return getValue().keySet(); + } + + /** + * Retrieve a Collection view of the entries in this compound. + * @return A view of each NBT tag in this compound. + */ + public Collection> asCollection(){ + return getValue().values(); + } + + @Override + public Map> getValue() { + // Return a wrapper map + if (savedMap == null) { + savedMap = new ConvertedMap>(container.getValue()) { + @Override + protected Object toInner(NbtBase outer) { + if (outer == null) + return null; + return NbtFactory.fromBase(outer).getHandle(); + } + + protected NbtBase toOuter(Object inner) { + if (inner == null) + return null; + return NbtFactory.fromNMS(inner); + }; + + @Override + public String toString() { + return NbtCompound.this.toString(); + } + }; + } + return savedMap; + } + + @Override + public void setValue(Map> newValue) { + // Write all the entries + for (Map.Entry> entry : newValue.entrySet()) { + put(entry.getValue()); + } + } + + /** + * Retrieve the value of a given entry. + * @param key - key of the entry to retrieve. + * @return The value of this entry. + */ + @SuppressWarnings("unchecked") + public NbtBase getValue(String key) { + return (NbtBase) getValue().get(key); + } + + /** + * Retrieve a value, or throw an exception. + * @param key - the key to retrieve. + * @return The value of the entry. + */ + private NbtBase getValueExact(String key) { + NbtBase value = getValue(key); + + // Only return a legal key + if (value != null) + return value; + else + throw new IllegalArgumentException("Cannot find key " + key); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public NbtBase>> clone() { + return (NbtBase) container.clone(); + } + + /** + * Set a entry based on its name. + * @param entry - entry with a name and value. + * @return This compound, for chaining. + */ + public NbtCompound put(NbtBase entry) { + getValue().put(entry.getName(), entry); + return this; + } + + /** + * Retrieve the string value of an entry identified by a given key. + * @param key - the key of the entry. + * @return The string value of the entry. + */ + public String getString(String key) { + return (String) getValueExact(key).getValue(); + } + + /** + * Associate a NBT string value with the given key. + * @param key - the key and NBT name. + * @param value - the value. + * @return This current compound, for chaining. + */ + public NbtCompound put(String key, String value) { + getValue().put(key, NbtFactory.of(key, value)); + return this; + } + + /** + * Retrieve the byte value of an entry identified by a given key. + * @param key - the key of the entry. + * @return The byte value of the entry. + */ + public Byte getByte(String key) { + return (Byte) getValueExact(key).getValue(); + } + + /** + * Associate a NBT byte value with the given key. + * @param key - the key and NBT name. + * @param value - the value. + * @return This current compound, for chaining. + */ + public NbtCompound put(String key, byte value) { + getValue().put(key, NbtFactory.of(key, value)); + return this; + } + + /** + * Retrieve the short value of an entry identified by a given key. + * @param key - the key of the entry. + * @return The short value of the entry. + */ + public Short getShort(String key) { + return (Short) getValueExact(key).getValue(); + } + + /** + * Associate a NBT short value with the given key. + * @param key - the key and NBT name. + * @param value - the value. + * @return This current compound, for chaining. + */ + public NbtCompound put(String key, short value) { + getValue().put(key, NbtFactory.of(key, value)); + return this; + } + + /** + * Retrieve the integer value of an entry identified by a given key. + * @param key - the key of the entry. + * @return The integer value of the entry. + */ + public Integer getInteger(String key) { + return (Integer) getValueExact(key).getValue(); + } + + /** + * Associate a NBT integer value with the given key. + * @param key - the key and NBT name. + * @param value - the value. + * @return This current compound, for chaining. + */ + public NbtCompound put(String key, int value) { + getValue().put(key, NbtFactory.of(key, value)); + return this; + } + + /** + * Retrieve the long value of an entry identified by a given key. + * @param key - the key of the entry. + * @return The long value of the entry. + */ + public Long getLong(String key) { + return (Long) getValueExact(key).getValue(); + } + + /** + * Associate a NBT long value with the given key. + * @param key - the key and NBT name. + * @param value - the value. + * @return This current compound, for chaining. + */ + public NbtCompound put(String key, long value) { + getValue().put(key, NbtFactory.of(key, value)); + return this; + } + + /** + * Retrieve the float value of an entry identified by a given key. + * @param key - the key of the entry. + * @return The float value of the entry. + */ + public Float getFloat(String key) { + return (Float) getValueExact(key).getValue(); + } + + /** + * Associate a NBT float value with the given key. + * @param key - the key and NBT name. + * @param value - the value. + * @return This current compound, for chaining. + */ + public NbtCompound put(String key, float value) { + getValue().put(key, NbtFactory.of(key, value)); + return this; + } + + /** + * Retrieve the double value of an entry identified by a given key. + * @param key - the key of the entry. + * @return The double value of the entry. + */ + public Double getDouble(String key) { + return (Double) getValueExact(key).getValue(); + } + + /** + * Associate a NBT double value with the given key. + * @param key - the key and NBT name. + * @param value - the value. + * @return This current compound, for chaining. + */ + public NbtCompound put(String key, double value) { + getValue().put(key, NbtFactory.of(key, value)); + return this; + } + + /** + * Retrieve the byte array value of an entry identified by a given key. + * @param key - the key of the entry. + * @return The byte array value of the entry. + */ + public byte[] getByteArray(String key) { + return (byte[]) getValueExact(key).getValue(); + } + + /** + * Associate a NBT byte array value with the given key. + * @param key - the key and NBT name. + * @param value - the value. + * @return This current compound, for chaining. + */ + public NbtCompound put(String key, byte[] value) { + getValue().put(key, NbtFactory.of(key, value)); + return this; + } + + /** + * Retrieve the integer array value of an entry identified by a given key. + * @param key - the key of the entry. + * @return The integer array value of the entry. + */ + public int[] getIntegerArray(String key) { + return (int[]) getValueExact(key).getValue(); + } + + /** + * Associate a NBT integer array value with the given key. + * @param key - the key and NBT name. + * @param value - the value. + * @return This current compound, for chaining. + */ + public NbtCompound put(String key, int[] value) { + getValue().put(key, NbtFactory.of(key, value)); + return this; + } + + /** + * Retrieve the compound (map) value of an entry identified by a given key. + * @param key - the key of the entry. + * @return The compound value of the entry. + */ + @SuppressWarnings("rawtypes") + public NbtCompound getCompound(String key) { + return (NbtCompound) ((NbtBase) getValueExact(key)); + } + + /** + * Associate a NBT compound with its name as key. + * @param compound - the compound value. + * @return This current compound, for chaining. + */ + public NbtCompound put(NbtCompound compound) { + getValue().put(compound.getName(), compound); + return this; + } + + /** + * Retrieve the NBT list value of an entry identified by a given key. + * @param key - the key of the entry. + * @return The NBT list value of the entry. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public NbtList getList(String key) { + return (NbtList) getValueExact(key); + } + + /** + * Associate a NBT list with the given key. + * @param list - the list value. + * @return This current compound, for chaining. + */ + public NbtCompound put(NbtList list) { + getValue().put(list.getName(), list); + return this; + } + + /** + * Associate a new NBT list with the given key. + * @param key - the key and name of the new NBT list. + * @param list - the list of NBT elements. + * @return This current compound, for chaining. + */ + public NbtCompound put(String key, Collection> list) { + return put(NbtList.fromList(key, list)); + } + + @Override + public void write(DataOutput destination) { + NbtFactory.toStream(container, destination); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof NbtCompound) { + NbtCompound other = (NbtCompound) obj; + return container.equals(other.container); + } + return false; + } + + @Override + public int hashCode() { + return container.hashCode(); + } + + @Override + public Iterator> iterator() { + return getValue().values().iterator(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + + builder.append("{"); + builder.append("\"name\": \"" + getName() + "\""); + + for (NbtBase element : this) { + builder.append(", "); + + // Wrap in quotation marks + if (element.getType() == NbtType.TAG_STRING) + builder.append("\"" + element.getName() + "\": \"" + element.getValue() + "\""); + else + builder.append("\"" + element.getName() + "\": " + element.getValue()); + } + + builder.append("}"); + return builder.toString(); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtElement.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtElement.java new file mode 100644 index 00000000..5ba7c16a --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtElement.java @@ -0,0 +1,213 @@ +package com.comphenix.protocol.wrappers.nbt; + +import java.io.DataOutput; +import java.lang.reflect.Method; + +import com.comphenix.protocol.reflect.FieldAccessException; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.StructureModifier; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.google.common.base.Objects; + +/** + * Represents an arbitrary NBT tag element, composite or not. + *

+ * Use {@link NbtFactory} to load or create an instance. + * @author Kristian + * + * @param - type of the value field. + */ +public class NbtElement implements NbtWrapper { + // Structure modifier for the base class + private static StructureModifier baseModifier; + + // For retrieving the current type ID + private static Method methodGetTypeID; + + // For cloning handles + private static Method methodClone; + + // Structure modifiers for the different NBT elements + private static StructureModifier[] modifiers = new StructureModifier[NbtType.values().length]; + + // The underlying NBT object + private Object handle; + + // Saved type + private NbtType type; + + /** + * Initialize a NBT wrapper for a generic element. + * @param handle - the NBT element to wrap. + */ + NbtElement(Object handle) { + this.handle = handle; + } + + /** + * Retrieve the modifier (with no target) that is used to read and write the NBT name. + * @return A modifier for accessing the NBT name. + */ + protected static StructureModifier getBaseModifier() { + if (baseModifier == null) { + Class base = MinecraftReflection.getNBTBaseClass(); + + // This will be the same for all classes, so we'll share modifier + baseModifier = new StructureModifier(base, Object.class, false).withType(String.class); + } + + return baseModifier.withType(String.class); + } + + /** + * Retrieve a modifier (with no target) that is used to read and write the NBT value. + * @return The value modifier. + */ + protected StructureModifier getCurrentModifier() { + NbtType type = getType(); + + return getCurrentBaseModifier().withType(type.getValueType()); + } + + /** + * Get the object modifier (with no target) for the current underlying NBT object. + * @return The generic modifier. + */ + @SuppressWarnings("unchecked") + protected StructureModifier getCurrentBaseModifier() { + int index = getType().ordinal(); + StructureModifier modifier = (StructureModifier) modifiers[index]; + + // Double checked locking + if (modifier == null) { + synchronized (this) { + if (modifiers[index] == null) { + modifiers[index] = new StructureModifier(handle.getClass(), MinecraftReflection.getNBTBaseClass(), false); + } + modifier = (StructureModifier) modifiers[index]; + } + } + + return modifier; + } + + /** + * Retrieve the underlying NBT tag object. + * @return The underlying Minecraft tag object. + */ + @Override + public Object getHandle() { + return handle; + } + + @Override + public NbtType getType() { + if (methodGetTypeID == null) { + // Use the base class + methodGetTypeID = FuzzyReflection.fromClass(MinecraftReflection.getNBTBaseClass()). + getMethodByParameters("getTypeID", byte.class, new Class[0]); + } + if (type == null) { + try { + type = NbtType.getTypeFromID((Byte) methodGetTypeID.invoke(handle)); + } catch (Exception e) { + throw new FieldAccessException("Cannot get NBT type of " + handle, e); + } + } + + return type; + } + + NbtType getSubType() { + int subID = getCurrentBaseModifier().withType(byte.class).withTarget(handle).read(0); + return NbtType.getTypeFromID(subID); + } + + void setSubType(NbtType type) { + byte subID = (byte) type.getRawID(); + getCurrentBaseModifier().withType(byte.class).withTarget(handle).write(0, subID); + } + + @Override + public String getName() { + return getBaseModifier().withTarget(handle).read(0); + } + + @Override + public void setName(String name) { + getBaseModifier().withTarget(handle).write(0, name); + } + + @Override + public TType getValue() { + return getCurrentModifier().withTarget(handle).read(0); + } + + @Override + public void setValue(TType newValue) { + getCurrentModifier().withTarget(handle).write(0, newValue); + } + + @Override + public void write(DataOutput destination) { + NbtFactory.toStream(this, destination); + } + + @Override + public NbtBase clone() { + if (methodClone == null) { + Class base = MinecraftReflection.getNBTBaseClass(); + + // Use the base class + methodClone = FuzzyReflection.fromClass(base). + getMethodByParameters("clone", base, new Class[0]); + } + + try { + return NbtFactory.fromNMS(methodClone.invoke(handle)); + } catch (Exception e) { + throw new FieldAccessException("Unable to clone " + handle, e); + } + } + + @Override + public int hashCode() { + return Objects.hashCode(getName(), getType(), getValue()); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof NbtBase) { + NbtBase other = (NbtBase) obj; + + // Make sure we're dealing with the same type + if (other.getType().equals(getType())) { + return Objects.equal(getName(), other.getName()) && + Objects.equal(getValue(), other.getValue()); + } + } + return false; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + String name = getName(); + + result.append("{"); + + if (name != null && name.length() > 0) + result.append("name: '" + name + "', "); + + result.append("value: "); + + // Wrap quotation marks + if (getType() == NbtType.TAG_STRING) + result.append("'" + getValue() + "'"); + else + result.append(getValue()); + + result.append("}"); + return result.toString(); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtFactory.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtFactory.java new file mode 100644 index 00000000..5a4bf3bf --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtFactory.java @@ -0,0 +1,250 @@ +package com.comphenix.protocol.wrappers.nbt; + +import java.io.DataInput; +import java.io.DataOutput; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import com.comphenix.protocol.reflect.FieldAccessException; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.utility.MinecraftReflection; + +/** + * Factory methods for creating NBT elements, lists and compounds. + * + * @author Kristian + */ +public class NbtFactory { + // Used to create the underlying tag + private static Method methodCreateTag; + + // Used to read and write NBT + private static Method methodWrite; + private static Method methodLoad; + + /** + * Get a NBT wrapper from a NBT base. + * @param base - the base class. + * @return A NBT wrapper. + */ + @SuppressWarnings("unchecked") + public static NbtWrapper fromBase(NbtBase base) { + if (base instanceof NbtElement) { + return (NbtElement) base; + } else if (base instanceof NbtCompound) { + return (NbtWrapper) base; + } else if (base instanceof NbtList) { + return (NbtWrapper) base; + } else { + if (base.getType() == NbtType.TAG_COMPOUND) { + // Load into a NBT-backed wrapper + NbtCompound copy = NbtCompound.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 = NbtList.fromName(base.getName()); + + copy.setValue((List>) base.getValue()); + return (NbtWrapper) copy; + + } else { + // Copy directly + NbtWrapper copy = ofType(base.getType(), base.getName()); + + copy.setValue(base.getValue()); + return copy; + } + } + } + + /** + * Initialize a NBT wrapper. + * @param handle - the underlying net.minecraft.server object to wrap. + * @return A NBT wrapper. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static NbtWrapper fromNMS(Object handle) { + NbtElement partial = new NbtElement(handle); + + // See if this is actually a compound tag + if (partial.getType() == NbtType.TAG_COMPOUND) + return (NbtWrapper) new NbtCompound(handle); + else if (partial.getType() == NbtType.TAG_LIST) + return new NbtList(handle); + else + return partial; + } + + /** + * Write the content of a wrapped NBT tag to a stream. + * @param value - the NBT tag to write. + * @param destination - the destination stream. + */ + public static void toStream(NbtBase value, DataOutput destination) { + if (methodWrite == null) { + Class base = MinecraftReflection.getNBTBaseClass(); + + // Use the base class + methodWrite = FuzzyReflection.fromClass(base). + getMethodByParameters("writeNBT", base, DataOutput.class); + } + + try { + methodWrite.invoke(null, fromBase(value).getHandle(), destination); + } catch (Exception e) { + throw new FieldAccessException("Unable to write NBT " + value, e); + } + } + + /** + * Load an NBT tag from a stream. + * @param source - the input stream. + * @return An NBT tag. + */ + public static NbtBase fromStream(DataInput source) { + if (methodLoad == null) { + Class base = MinecraftReflection.getNBTBaseClass(); + + // Use the base class + methodLoad = FuzzyReflection.fromClass(base). + getMethodByParameters("load", base, new Class[] { DataInput.class }); + } + + try { + return fromNMS(methodLoad.invoke(null, source)); + } catch (Exception e) { + throw new FieldAccessException("Unable to read NBT from " + source, e); + } + } + + public static NbtBase of(String name, String value) { + return ofType(NbtType.TAG_STRING, name, value); + } + + public static NbtBase of(String name, byte value) { + return ofType(NbtType.TAG_BYTE, name, value); + } + + public static NbtBase of(String name, short value) { + return ofType(NbtType.TAG_SHORT, name, value); + } + + public static NbtBase of(String name, int value) { + return ofType(NbtType.TAG_INT, name, value); + } + + public static NbtBase of(String name, long value) { + return ofType(NbtType.TAG_LONG, name, value); + } + + public static NbtBase of(String name, float value) { + return ofType(NbtType.TAG_FLOAT, name, value); + } + + public static NbtBase of(String name, double value) { + return ofType(NbtType.TAG_DOUBlE, name, value); + } + + public static NbtBase of(String name, byte[] value) { + return ofType(NbtType.TAG_BYTE_ARRAY, name, value); + } + + public static NbtBase of(String name, int[] value) { + return ofType(NbtType.TAG_INT_ARRAY, name, value); + } + + /** + * Construct a new NBT compound wrapper 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 NbtCompound.fromList(name, list); + } + + /** + * Construct a NBT list of out an array of values. + * @param name - name of this list. + * @param elements - elements to add. + * @return The new filled NBT list. + */ + public static NbtList ofList(String name, T... elements) { + return NbtList.fromArray(name, elements); + } + + /** + * Create a new NBT wrapper from a given 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. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static NbtWrapper ofType(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 (methodCreateTag == null) { + Class base = MinecraftReflection.getNBTBaseClass(); + + // Use the base class + methodCreateTag = FuzzyReflection.fromClass(base). + getMethodByParameters("createTag", base, new Class[] { byte.class, String.class }); + } + + try { + Object handle = methodCreateTag.invoke(null, (byte) type.getRawID(), name); + + if (type == NbtType.TAG_COMPOUND) + return (NbtWrapper) new NbtCompound(handle); + else if (type == NbtType.TAG_LIST) + return (NbtWrapper) new NbtList(handle); + else + return new NbtElement(handle); + + } catch (Exception e) { + // Inform the caller + throw new FieldAccessException( + String.format("Cannot create NBT element %s (type: %s)", name, type), + e); + } + } + + /** + * Create a new NBT wrapper from a given 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 ofType(NbtType type, String name, T value) { + NbtWrapper created = ofType(type, name); + + // Update the value + created.setValue(value); + return created; + } + + /** + * Create a new NBT wrapper from a given 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 ofType(Class type, String name, T value) { + return ofType(NbtType.getTypeFromClass(type), name, value); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtList.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtList.java new file mode 100644 index 00000000..c3bd131d --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtList.java @@ -0,0 +1,311 @@ +package com.comphenix.protocol.wrappers.nbt; + +import java.io.DataOutput; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import javax.annotation.Nullable; + +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.collect.Iterables; + +/** + * Represents a list of NBT tags of the same type without names. + *

+ * Use {@link NbtFactory} to load or create an instance. + * + * @author Kristian + * + * @param - the value type of each NBT tag. + */ +public class NbtList implements NbtWrapper>>, Iterable { + /** + * The name of every NBT tag in a list. + */ + public static String EMPTY_NAME = ""; + + // A list container + private NbtElement> container; + + // Saved wrapper list + private ConvertedList> savedList; + + /** + * Construct a new empty NBT list. + * @param name - name of this list. + * @return The new empty NBT list. + */ + public static NbtList fromName(String name) { + return (NbtList) NbtFactory.>>ofType(NbtType.TAG_LIST, name); + } + + /** + * Construct a NBT list of out an array of values.. + * @param name - name of this list. + * @param elements - values to add. + * @return The new filled NBT list. + */ + public static NbtList fromArray(String name, T... elements) { + NbtList result = fromName(name); + + for (T element : elements) { + if (element == null) + throw new IllegalArgumentException("An NBT list cannot contain a null element!"); + result.add(NbtFactory.ofType(element.getClass(), EMPTY_NAME, element)); + } + return result; + } + + /** + * Construct a NBT list of out a list of NBT elements. + * @param name - name of this list. + * @param elements - elements to add. + * @return The new filled NBT list. + */ + public static NbtList fromList(String name, Collection elements) { + NbtList result = fromName(name); + + for (T element : elements) { + if (element == null) + throw new IllegalArgumentException("An NBT list cannot contain a null element!"); + result.add(NbtFactory.ofType(element.getClass(), EMPTY_NAME, element)); + } + return result; + } + + NbtList(Object handle) { + this.container = new NbtElement>(handle); + } + + @Override + public Object getHandle() { + return container.getHandle(); + } + + @Override + public NbtType getType() { + return NbtType.TAG_LIST; + } + + /** + * Get the type of each element. + * @return Element type. + */ + public NbtType getElementType() { + return container.getSubType(); + } + + @Override + public String getName() { + return container.getName(); + } + + @Override + public void setName(String name) { + container.setName(name); + } + + @Override + public List> getValue() { + if (savedList == null) { + savedList = new ConvertedList>(container.getValue()) { + @Override + public boolean add(NbtBase e) { + if (e == null) + throw new IllegalArgumentException("Cannot store NULL elements in list."); + if (!e.getName().equals(EMPTY_NAME)) + throw new IllegalArgumentException("Cannot add a named NBT tag " + e + " to a list."); + if (size() == 0) + container.setSubType(e.getType()); + + return super.add(e); + } + + @Override + public void add(int index, NbtBase element) { + if (element == null) + throw new IllegalArgumentException("Cannot store NULL elements in list."); + if (!element.getName().equals(EMPTY_NAME)) + throw new IllegalArgumentException("Cannot add a the named NBT tag " + element + " to a list."); + if (index == 0) + container.setSubType(element.getType()); + + super.add(index, element); + } + + @Override + public boolean addAll(Collection> c) { + boolean empty = size() == 0; + boolean result = false; + + for (NbtBase element : c) { + add(element); + result = true; + } + + // See if we now added our first object(s) + if (empty && result) { + container.setSubType(get(0).getType()); + } + return result; + } + + @Override + protected Object toInner(NbtBase outer) { + if (outer == null) + return null; + return NbtFactory.fromBase(outer).getHandle(); + } + + @Override + protected NbtBase toOuter(Object inner) { + if (inner == null) + return null; + return NbtFactory.fromNMS(inner); + } + + @Override + public String toString() { + return NbtList.this.toString(); + } + }; + } + return savedList; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public NbtBase>> clone() { + return (NbtBase) container.clone(); + } + + public void add(NbtBase element) { + getValue().add(element); + } + + @SuppressWarnings("unchecked") + public void add(String value) { + add((NbtBase) NbtFactory.of(EMPTY_NAME, value)); + } + + @SuppressWarnings("unchecked") + public void add(byte value) { + add((NbtBase) NbtFactory.of(EMPTY_NAME, value)); + } + + @SuppressWarnings("unchecked") + public void add(short value) { + add((NbtBase) NbtFactory.of(EMPTY_NAME, value)); + } + + @SuppressWarnings("unchecked") + public void add(int value) { + add((NbtBase) NbtFactory.of(EMPTY_NAME, value)); + } + + @SuppressWarnings("unchecked") + public void add(long value) { + add((NbtBase) NbtFactory.of(EMPTY_NAME, value)); + } + + @SuppressWarnings("unchecked") + public void add(double value) { + add((NbtBase) NbtFactory.of(EMPTY_NAME, value)); + } + + @SuppressWarnings("unchecked") + public void add(byte[] value) { + add((NbtBase) NbtFactory.of(EMPTY_NAME, value)); + } + + @SuppressWarnings("unchecked") + public void add(int[] value) { + add((NbtBase) NbtFactory.of(EMPTY_NAME, value)); + } + + public int size() { + return getValue().size(); + } + + public TType getValue(int index) { + return getValue().get(index).getValue(); + } + + /** + * Retrieve each NBT tag in this list. + * @return A view of NBT tag in this list. + */ + public Collection> asCollection() { + return getValue(); + } + + @Override + public void setValue(List> newValue) { + NbtBase lastElement = null; + List list = container.getValue(); + list.clear(); + + // Set each underlying element + for (NbtBase type : newValue) { + if (type != null) { + lastElement = type; + list.add(NbtFactory.fromBase(type).getHandle()); + } else { + list.add(null); + } + } + + // Update the sub type as well + if (lastElement != null) { + container.setSubType(lastElement.getType()); + } + } + + @Override + public void write(DataOutput destination) { + NbtFactory.toStream(container, destination); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof NbtList) { + @SuppressWarnings("unchecked") + NbtList other = (NbtList) obj; + return container.equals(other.container); + } + return false; + } + + @Override + public int hashCode() { + return container.hashCode(); + } + + @Override + public Iterator iterator() { + return Iterables.transform(getValue(), new Function, TType>() { + @Override + public TType apply(@Nullable NbtBase param) { + return param.getValue(); + } + }).iterator(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + + builder.append("{\"name\": \"" + getName() + "\", \"value\": ["); + + if (size() > 0) { + if (getElementType() == NbtType.TAG_STRING) + builder.append("\"" + Joiner.on("\", \"").join(this) + "\""); + else + builder.append(Joiner.on(", ").join(this)); + } + + builder.append("]}"); + return builder.toString(); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtType.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtType.java new file mode 100644 index 00000000..f6f950f2 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtType.java @@ -0,0 +1,141 @@ +package com.comphenix.protocol.wrappers.nbt; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents all the element types + * + * @author Kristian + */ +public enum NbtType { + /** + * Used to mark the end of compound tags. CANNOT be constructed. + */ + TAG_END(0, Void.class), + + /** + * A signed 1 byte integral type. Sometimes used for booleans. + */ + TAG_BYTE(1, byte.class), + + /** + * A signed 2 byte integral type. + */ + TAG_SHORT(2, short.class), + + /** + * A signed 4 byte integral type. + */ + TAG_INT(3, int.class), + + /** + * A signed 8 byte integral type. + */ + TAG_LONG(4, long.class), + + /** + * A signed 4 byte floating point type. + */ + TAG_FLOAT(5, float.class), + + /** + * A signed 8 byte floating point type. + */ + TAG_DOUBlE(6, double.class), + + /** + * An array of bytes. + */ + TAG_BYTE_ARRAY(7, byte[].class), + + /** + * An array of TAG_Int's payloads.. + */ + TAG_INT_ARRAY(11, int[].class), + + /** + * A UTF-8 string + */ + TAG_STRING(8, String.class), + + /** + * A list of tag payloads, without repeated tag IDs or any tag names. + */ + TAG_LIST(9, List.class), + + /** + * A list of fully formed tags, including their IDs, names, and payloads. No two tags may have the same name. + */ + TAG_COMPOUND(10, Map.class); + + private int rawID; + private Class valueType; + + // Used to lookup a specified NBT + private static NbtType[] lookup; + + // Lookup NBT by class + private static Map, NbtType> classLookup; + + static { + NbtType[] values = values(); + lookup = new NbtType[values.length]; + classLookup = new HashMap, NbtType>(); + + // Initialize lookup tables + for (NbtType type : values) { + lookup[type.getRawID()] = type; + classLookup.put(type.getValueType(), type); + } + } + + private NbtType(int rawID, Class valueType) { + this.rawID = rawID; + this.valueType = valueType; + } + + /** + * Retrieves the raw unique integer that identifies the type of the parent NBT element. + * @return Integer that uniquely identifying the type. + */ + public int getRawID() { + return rawID; + } + + /** + * Retrieves the type of the value stored in the NBT element. + * @return Type of the stored value. + */ + public Class getValueType() { + return valueType; + } + + /** + * Retrieve an NBT type from a given raw ID. + * @param rawID - the raw ID to lookup. + * @return The associated NBT value. + */ + public static NbtType getTypeFromID(int rawID) { + if (rawID < 0 || rawID >= lookup.length) + throw new IllegalArgumentException("Unrecognized raw ID " + rawID); + return lookup[rawID]; + } + + /** + * Retrieve an NBT type from the given Java class. + * @param clazz - type of the value the NBT type can contain. + * @return The NBT type. + * @throws IllegalArgumentException If this class type cannot be represented by NBT tags. + */ + public static NbtType getTypeFromClass(Class clazz) { + NbtType result = classLookup.get(clazz); + + // Try to lookup this value + if (result != null) + return result; + else + throw new IllegalArgumentException("No NBT tag can represent a " + clazz); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtWrapper.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtWrapper.java new file mode 100644 index 00000000..61106dc3 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtWrapper.java @@ -0,0 +1,24 @@ +package com.comphenix.protocol.wrappers.nbt; + +import java.io.DataOutput; + +/** + * Indicates that this NBT wraps an underlying net.minecraft.server instance. + * + * @author Kristian + * + * @param - type of the value that is stored. + */ +public interface NbtWrapper extends NbtBase { + /** + * Retrieve the underlying net.minecraft.server instance. + * @return The NMS instance. + */ + public Object getHandle(); + + /** + * Write the current NBT tag to an output stream. + * @param destination - the destination stream. + */ + public void write(DataOutput destination); +} diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/nbt/NbtFactoryTest.java b/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/nbt/NbtFactoryTest.java new file mode 100644 index 00000000..d5bbfbf5 --- /dev/null +++ b/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/nbt/NbtFactoryTest.java @@ -0,0 +1,46 @@ +package com.comphenix.protocol.wrappers.nbt; + +import static org.junit.Assert.*; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; + +import org.junit.BeforeClass; +import org.junit.Test; + +import com.comphenix.protocol.utility.MinecraftReflection; + +public class NbtFactoryTest { + @BeforeClass + public static void initializeBukkit() { + // Initialize reflection + MinecraftReflection.setMinecraftPackage("net.minecraft.server.v1_4_6", "org.bukkit.craftbukkit.v1_4_6"); + } + + @Test + public void testFromStream() { + NbtCompound compound = NbtCompound.fromName("tag"); + + compound.put("name", "Test Testerson"); + compound.put("age", 42); + + compound.put(NbtFactory.ofList("nicknames", "a", "b", "c")); + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + DataOutput test = new DataOutputStream(buffer); + compound.write(test); + + ByteArrayInputStream source = new ByteArrayInputStream(buffer.toByteArray()); + DataInput input = new DataInputStream(source); + + NbtCompound cloned = (NbtCompound) NbtFactory.fromStream(input); + + assertEquals(compound.getString("name"), cloned.getString("name")); + assertEquals(compound.getInteger("age"), cloned.getInteger("age")); + assertEquals(compound.getList("nicknames"), cloned.getList("nicknames")); + } +}