mirror of
https://github.com/dmulloy2/ProtocolLib.git
synced 2024-10-31 16:00:52 +01:00
Added the ability to serialize and deserialize NBT to many formats.
This commit is contained in:
parent
3426306805
commit
dbc28c0035
@ -29,6 +29,13 @@ import com.comphenix.protocol.wrappers.nbt.NbtType;
|
||||
* @param <TType> - type of the value that is stored.
|
||||
*/
|
||||
public interface NbtBase<TType> {
|
||||
/**
|
||||
* Accepts a NBT visitor.
|
||||
* @param visitor - the hierarchical NBT visitor.
|
||||
* @return TRUE if the parent should continue processing children at the current level, FALSE otherwise.
|
||||
*/
|
||||
public abstract boolean accept(NbtVisitor visitor);
|
||||
|
||||
/**
|
||||
* Retrieve the type of this NBT element.
|
||||
* @return The type of this NBT element.
|
||||
|
@ -10,7 +10,7 @@ import java.util.Set;
|
||||
* <p>
|
||||
* Use {@link NbtFactory} to load or create an instance.
|
||||
* <p>
|
||||
* The {@link NbtBase#getValue()} method returns a {@link java.util.Map} that will correctly return the content
|
||||
* The {@link NbtBase#getValue()} method returns a {@link java.util.Map} that will return the full content
|
||||
* of this NBT compound, but may throw an {@link UnsupportedOperationException} for any of the write operations.
|
||||
*
|
||||
* @author Kristian
|
||||
@ -73,6 +73,14 @@ public interface NbtCompound extends NbtBase<Map<String, NbtBase<?>>>, Iterable<
|
||||
* @return This current compound, for chaining.
|
||||
*/
|
||||
public abstract NbtCompound put(String key, String value);
|
||||
|
||||
/**
|
||||
* Inserts an entry after cloning it and renaming it to "key".
|
||||
* @param key - the name of the entry.
|
||||
* @param entry - the entry to insert.
|
||||
* @return This current compound, for chaining.
|
||||
*/
|
||||
public abstract NbtCompound put(String key, NbtBase<?> entry);
|
||||
|
||||
/**
|
||||
* Retrieve the byte value of an entry identified by a given key.
|
||||
@ -286,7 +294,7 @@ public interface NbtCompound extends NbtBase<Map<String, NbtBase<?>>>, Iterable<
|
||||
* @param list - the list value.
|
||||
* @return This current compound, for chaining.
|
||||
*/
|
||||
public abstract <T> NbtCompound put(WrappedList<T> list);
|
||||
public abstract <T> NbtCompound put(NbtList<T> list);
|
||||
|
||||
/**
|
||||
* Associate a new NBT list with the given key.
|
||||
|
@ -17,8 +17,6 @@
|
||||
|
||||
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;
|
||||
@ -40,11 +38,7 @@ import com.comphenix.protocol.wrappers.BukkitConverters;
|
||||
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;
|
||||
|
||||
|
||||
// Item stack trickery
|
||||
private static StructureModifier<Object> itemStackModifier;
|
||||
|
||||
@ -102,7 +96,7 @@ public class NbtFactory {
|
||||
|
||||
} else if (base.getType() == NbtType.TAG_LIST) {
|
||||
// As above
|
||||
WrappedList<T> copy = WrappedList.fromName(base.getName());
|
||||
NbtList<T> copy = WrappedList.fromName(base.getName());
|
||||
|
||||
copy.setValue((List<NbtBase<T>>) base.getValue());
|
||||
return (NbtWrapper<T>) copy;
|
||||
@ -166,49 +160,7 @@ public class NbtFactory {
|
||||
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 <TType> void toStream(NbtWrapper<TType> 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 NbtWrapper<?> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a NBT tag of type string.
|
||||
* @param name - name of the tag.
|
||||
@ -276,7 +228,7 @@ public class NbtFactory {
|
||||
* @return The constructed NBT tag.
|
||||
*/
|
||||
public static NbtBase<Double> of(String name, double value) {
|
||||
return ofWrapper(NbtType.TAG_DOUBlE, name, value);
|
||||
return ofWrapper(NbtType.TAG_DOUBLE, name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -23,11 +23,27 @@ public interface NbtList<TType> extends NbtBase<List<NbtBase<TType>>>, Iterable<
|
||||
public static String EMPTY_NAME = "";
|
||||
|
||||
/**
|
||||
* Get the type of each element.
|
||||
* Get the type of each element.
|
||||
* <p>
|
||||
* This will be {@link NbtType#TAG_END TAG_END} if the NBT list has just been created.
|
||||
* @return Element type.
|
||||
*/
|
||||
public abstract NbtType getElementType();
|
||||
|
||||
/**
|
||||
* Set the type of each element.
|
||||
* @param type - type of each element.
|
||||
*/
|
||||
public abstract void setElementType(NbtType type);
|
||||
|
||||
/**
|
||||
* Add a value to a typed list by attempting to convert it to the nearest value.
|
||||
* <p>
|
||||
* Note that the list must be typed by setting {@link #setElementType(NbtType)} before calling this function.
|
||||
* @param value - the value to add.
|
||||
*/
|
||||
public abstract void addClosest(Object value);
|
||||
|
||||
/**
|
||||
* Add a NBT list or NBT compound to the list.
|
||||
* @param element
|
||||
|
@ -62,7 +62,7 @@ public enum NbtType {
|
||||
/**
|
||||
* A signed 8 byte floating point type.
|
||||
*/
|
||||
TAG_DOUBlE(6, double.class),
|
||||
TAG_DOUBLE(6, double.class),
|
||||
|
||||
/**
|
||||
* An array of bytes.
|
||||
@ -113,13 +113,25 @@ public enum NbtType {
|
||||
classLookup.put(Primitives.wrap(type.getValueType()), type);
|
||||
}
|
||||
}
|
||||
|
||||
// Additional lookup
|
||||
classLookup.put(NbtList.class, TAG_LIST);
|
||||
classLookup.put(NbtCompound.class, TAG_COMPOUND);
|
||||
}
|
||||
|
||||
private NbtType(int rawID, Class<?> valueType) {
|
||||
this.rawID = rawID;
|
||||
this.valueType = valueType;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the given NBT can store multiple children NBT tags.
|
||||
* @return TRUE if this is a composite NBT tag, FALSE otherwise.
|
||||
*/
|
||||
public boolean isComposite() {
|
||||
return this == TAG_COMPOUND || this == TAG_LIST;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the raw unique integer that identifies the type of the parent NBT element.
|
||||
* @return Integer that uniquely identifying the type.
|
||||
@ -157,9 +169,16 @@ public enum NbtType {
|
||||
NbtType result = classLookup.get(clazz);
|
||||
|
||||
// Try to lookup this value
|
||||
if (result != null)
|
||||
if (result != null) {
|
||||
return result;
|
||||
else
|
||||
} else {
|
||||
// Look for interfaces
|
||||
for (Class<?> implemented : clazz.getInterfaces()) {
|
||||
if (classLookup.containsKey(implemented))
|
||||
return classLookup.get(implemented);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("No NBT tag can represent a " + clazz);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,43 @@
|
||||
package com.comphenix.protocol.wrappers.nbt;
|
||||
|
||||
/**
|
||||
* A visitor that can enumerate a NBT tree structure.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public interface NbtVisitor {
|
||||
/**
|
||||
* Visit a leaf node, which is a NBT tag with a primitive or String value.
|
||||
* @param node - the visited leaf node.
|
||||
* @return TRUE to continue visiting children at this level, FALSE otherwise.
|
||||
*/
|
||||
public boolean visit(NbtBase<?> node);
|
||||
|
||||
/**
|
||||
* Begin visiting a list node that contains multiple child nodes of the same type.
|
||||
* @param list - the NBT tag to process.
|
||||
* @return TRUE to visit the child nodes of this list, FALSE otherwise.
|
||||
*/
|
||||
public boolean visitEnter(NbtList<?> list);
|
||||
|
||||
/**
|
||||
* Begin visiting a compound node that contains multiple child nodes of different types.
|
||||
* @param compound - the NBT tag to process.
|
||||
* @return TRUE to visit the child nodes of this compound, FALSE otherwise.
|
||||
*/
|
||||
public boolean visitEnter(NbtCompound compound);
|
||||
|
||||
/**
|
||||
* Stop visiting a list node.
|
||||
* @param list - the list we're done visiting.
|
||||
* @return TRUE for the parent to visit any subsequent sibling nodes, FALSE otherwise.
|
||||
*/
|
||||
public boolean visitLeave(NbtList<?> list);
|
||||
|
||||
/**
|
||||
* Stop visiting a compound node.
|
||||
* @param compound - the compound we're done visting.
|
||||
* @return TRUE for the parent to visit any subsequent sibling nodes, FALSE otherwise
|
||||
*/
|
||||
public boolean visitLeave(NbtCompound compound);
|
||||
}
|
@ -23,6 +23,8 @@ import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.comphenix.protocol.wrappers.nbt.io.NbtBinarySerializer;
|
||||
|
||||
/**
|
||||
* A concrete implementation of an NbtCompound that wraps an underlying NMS Compound.
|
||||
*
|
||||
@ -66,6 +68,19 @@ class WrappedCompound implements NbtWrapper<Map<String, NbtBase<?>>>, Iterable<N
|
||||
public WrappedCompound(Object handle) {
|
||||
this.container = new WrappedElement<Map<String,Object>>(handle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(NbtVisitor visitor) {
|
||||
// Enter this node?
|
||||
if (visitor.visitEnter(this)) {
|
||||
for (NbtBase<?> node : this) {
|
||||
if (!node.accept(visitor))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return visitor.visitLeave(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getHandle() {
|
||||
@ -420,7 +435,7 @@ class WrappedCompound implements NbtWrapper<Map<String, NbtBase<?>>>, Iterable<N
|
||||
*/
|
||||
@Override
|
||||
public double getDoubleOrDefault(String key) {
|
||||
return (Double) getValueOrDefault(key, NbtType.TAG_DOUBlE).getValue();
|
||||
return (Double) getValueOrDefault(key, NbtType.TAG_DOUBLE).getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -543,11 +558,20 @@ class WrappedCompound implements NbtWrapper<Map<String, NbtBase<?>>>, Iterable<N
|
||||
* @return This current compound, for chaining.
|
||||
*/
|
||||
@Override
|
||||
public <T> NbtCompound put(WrappedList<T> list) {
|
||||
public <T> NbtCompound put(NbtList<T> list) {
|
||||
getValue().put(list.getName(), list);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NbtCompound put(String key, NbtBase<?> entry) {
|
||||
// Don't modify the original NBT
|
||||
NbtBase<?> clone = entry.deepClone();
|
||||
|
||||
clone.setName(key);
|
||||
return put(clone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Associate a new NBT list with the given key.
|
||||
* @param key - the key and name of the new NBT list.
|
||||
@ -561,7 +585,7 @@ class WrappedCompound implements NbtWrapper<Map<String, NbtBase<?>>>, Iterable<N
|
||||
|
||||
@Override
|
||||
public void write(DataOutput destination) {
|
||||
NbtFactory.toStream(container, destination);
|
||||
NbtBinarySerializer.DEFAULT.serialize(container, destination);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -24,6 +24,7 @@ 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.comphenix.protocol.wrappers.nbt.io.NbtBinarySerializer;
|
||||
import com.google.common.base.Objects;
|
||||
|
||||
/**
|
||||
@ -106,6 +107,11 @@ class WrappedElement<TType> implements NbtWrapper<TType> {
|
||||
return modifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(NbtVisitor visitor) {
|
||||
return visitor.visit(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the underlying NBT tag object.
|
||||
* @return The underlying Minecraft tag object.
|
||||
@ -173,7 +179,8 @@ class WrappedElement<TType> implements NbtWrapper<TType> {
|
||||
|
||||
@Override
|
||||
public void write(DataOutput destination) {
|
||||
NbtFactory.toStream(this, destination);
|
||||
// No need to cache this object
|
||||
NbtBinarySerializer.DEFAULT.serialize(this, destination);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -24,6 +24,7 @@ import java.util.List;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import com.comphenix.protocol.wrappers.nbt.io.NbtBinarySerializer;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.Iterables;
|
||||
@ -41,13 +42,17 @@ class WrappedList<TType> implements NbtWrapper<List<NbtBase<TType>>>, Iterable<T
|
||||
// Saved wrapper list
|
||||
private ConvertedList<Object, NbtBase<TType>> savedList;
|
||||
|
||||
// Element type
|
||||
private NbtType elementType = NbtType.TAG_END;
|
||||
|
||||
/**
|
||||
* Construct a new empty NBT list.
|
||||
* @param name - name of this list.
|
||||
* @return The new empty NBT list.
|
||||
*/
|
||||
public static <T> WrappedList<T> fromName(String name) {
|
||||
return (WrappedList<T>) NbtFactory.<List<NbtBase<T>>>ofWrapper(NbtType.TAG_LIST, name);
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> NbtList<T> fromName(String name) {
|
||||
return (NbtList<T>) NbtFactory.<List<NbtBase<T>>>ofWrapper(NbtType.TAG_LIST, name);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -56,13 +61,18 @@ class WrappedList<TType> implements NbtWrapper<List<NbtBase<TType>>>, Iterable<T
|
||||
* @param elements - values to add.
|
||||
* @return The new filled NBT list.
|
||||
*/
|
||||
public static <T> WrappedList<T> fromArray(String name, T... elements) {
|
||||
WrappedList<T> result = fromName(name);
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public static <T> NbtList<T> fromArray(String name, T... elements) {
|
||||
NbtList<T> result = fromName(name);
|
||||
|
||||
for (T element : elements) {
|
||||
if (element == null)
|
||||
throw new IllegalArgumentException("An NBT list cannot contain a null element!");
|
||||
result.add(NbtFactory.ofWrapper(element.getClass(), EMPTY_NAME, element));
|
||||
|
||||
if (element instanceof NbtBase)
|
||||
result.add((NbtBase) element);
|
||||
else
|
||||
result.add(NbtFactory.ofWrapper(element.getClass(), EMPTY_NAME, element));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@ -73,21 +83,44 @@ class WrappedList<TType> implements NbtWrapper<List<NbtBase<TType>>>, Iterable<T
|
||||
* @param elements - elements to add.
|
||||
* @return The new filled NBT list.
|
||||
*/
|
||||
public static <T> WrappedList<T> fromList(String name, Collection<? extends T> elements) {
|
||||
WrappedList<T> result = fromName(name);
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public static <T> NbtList<T> fromList(String name, Collection<? extends T> elements) {
|
||||
NbtList<T> result = fromName(name);
|
||||
|
||||
for (T element : elements) {
|
||||
if (element == null)
|
||||
throw new IllegalArgumentException("An NBT list cannot contain a null element!");
|
||||
result.add(NbtFactory.ofWrapper(element.getClass(), EMPTY_NAME, element));
|
||||
|
||||
if (element instanceof NbtBase)
|
||||
result.add((NbtBase) element);
|
||||
else
|
||||
result.add(NbtFactory.ofWrapper(element.getClass(), EMPTY_NAME, element));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a list from an NMS instance.
|
||||
* @param handle - NMS instance.
|
||||
*/
|
||||
public WrappedList(Object handle) {
|
||||
this.container = new WrappedElement<List<Object>>(handle);
|
||||
this.elementType = container.getSubType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(NbtVisitor visitor) {
|
||||
// Enter this node?
|
||||
if (visitor.visitEnter(this)) {
|
||||
for (NbtBase<TType> node : getValue()) {
|
||||
if (!node.accept(visitor))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return visitor.visitLeave(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getHandle() {
|
||||
return container.getHandle();
|
||||
@ -98,15 +131,17 @@ class WrappedList<TType> implements NbtWrapper<List<NbtBase<TType>>>, Iterable<T
|
||||
return NbtType.TAG_LIST;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of each element.
|
||||
* @return Element type.
|
||||
*/
|
||||
@Override
|
||||
public NbtType getElementType() {
|
||||
return container.getSubType();
|
||||
return elementType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setElementType(NbtType type) {
|
||||
this.elementType = type;
|
||||
container.setSubType(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return container.getName();
|
||||
@ -129,7 +164,7 @@ class WrappedList<TType> implements NbtWrapper<List<NbtBase<TType>>>, Iterable<T
|
||||
throw new IllegalArgumentException("Cannot add a the named NBT tag " + element + " to a list.");
|
||||
|
||||
// Check element type
|
||||
if (size() > 0) {
|
||||
if (getElementType() != NbtType.TAG_END) {
|
||||
if (!element.getType().equals(getElementType())) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot add " + element + " of " + element.getType() + " to a list of type " + getElementType());
|
||||
@ -153,18 +188,12 @@ class WrappedList<TType> implements NbtWrapper<List<NbtBase<TType>>>, Iterable<T
|
||||
|
||||
@Override
|
||||
public boolean addAll(Collection<? extends NbtBase<TType>> c) {
|
||||
boolean empty = size() == 0;
|
||||
boolean result = false;
|
||||
|
||||
for (NbtBase<TType> 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;
|
||||
}
|
||||
|
||||
@ -197,6 +226,38 @@ class WrappedList<TType> implements NbtWrapper<List<NbtBase<TType>>>, Iterable<T
|
||||
return (NbtBase) container.deepClone();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void addClosest(Object value) {
|
||||
if (getElementType() == NbtType.TAG_END)
|
||||
throw new IllegalStateException("This list has not been typed yet.");
|
||||
|
||||
if (value instanceof Number) {
|
||||
Number number = (Number) value;
|
||||
|
||||
// Convert the number
|
||||
switch (getElementType()) {
|
||||
case TAG_BYTE: add(number.byteValue()); break;
|
||||
case TAG_SHORT: add(number.shortValue()); break;
|
||||
case TAG_INT: add(number.intValue()); break;
|
||||
case TAG_LONG: add(number.longValue()); break;
|
||||
case TAG_FLOAT: add(number.floatValue()); break;
|
||||
case TAG_DOUBLE: add(number.doubleValue()); break;
|
||||
case TAG_STRING: add(number.toString()); break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Cannot convert " + value + " to " + getType());
|
||||
}
|
||||
|
||||
} else if (value instanceof NbtBase) {
|
||||
// Add the element itself
|
||||
add((NbtBase<TType>) value);
|
||||
|
||||
} else {
|
||||
// Just add it
|
||||
add((NbtBase<TType>) NbtFactory.ofWrapper(getElementType(), EMPTY_NAME, value));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(NbtBase<TType> element) {
|
||||
getValue().add(element);
|
||||
@ -293,7 +354,7 @@ class WrappedList<TType> implements NbtWrapper<List<NbtBase<TType>>>, Iterable<T
|
||||
|
||||
@Override
|
||||
public void write(DataOutput destination) {
|
||||
NbtFactory.toStream(container, destination);
|
||||
NbtBinarySerializer.DEFAULT.serialize(container, destination);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,88 @@
|
||||
package com.comphenix.protocol.wrappers.nbt.io;
|
||||
|
||||
import java.io.DataInput;
|
||||
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.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.wrappers.nbt.NbtBase;
|
||||
import com.comphenix.protocol.wrappers.nbt.NbtCompound;
|
||||
import com.comphenix.protocol.wrappers.nbt.NbtFactory;
|
||||
import com.comphenix.protocol.wrappers.nbt.NbtList;
|
||||
import com.comphenix.protocol.wrappers.nbt.NbtWrapper;
|
||||
|
||||
public class NbtBinarySerializer {
|
||||
// Used to read and write NBT
|
||||
private static Method methodWrite;
|
||||
private static Method methodLoad;
|
||||
|
||||
/**
|
||||
* Retrieve a default instance of the NBT binary serializer.
|
||||
*/
|
||||
public static final NbtBinarySerializer DEFAULT = new NbtBinarySerializer();
|
||||
|
||||
/**
|
||||
* Write the content of a wrapped NBT tag to a stream.
|
||||
* @param value - the NBT tag to write.
|
||||
* @param destination - the destination stream.
|
||||
*/
|
||||
public <TType> void serialize(NbtBase<TType> 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, NbtFactory.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 <TType> NbtWrapper<TType> deserialize(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 NbtFactory.fromNMS(methodLoad.invoke(null, source));
|
||||
} catch (Exception e) {
|
||||
throw new FieldAccessException("Unable to read NBT from " + source, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an NBT compound from a stream.
|
||||
* @param source - the input stream.
|
||||
* @return An NBT compound.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public NbtCompound deserializeCompound(DataInput source) {
|
||||
// I always seem to override generics ...
|
||||
return (NbtCompound) (NbtBase) deserialize(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an NBT list from a stream.
|
||||
* @param source - the input stream.
|
||||
* @return An NBT list.
|
||||
*/
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
public <T> NbtList<T> deserializeList(DataInput source) {
|
||||
return (NbtList<T>) (NbtBase) deserialize(source);
|
||||
}
|
||||
}
|
@ -0,0 +1,301 @@
|
||||
package com.comphenix.protocol.wrappers.nbt.io;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
import com.comphenix.protocol.wrappers.nbt.NbtBase;
|
||||
import com.comphenix.protocol.wrappers.nbt.NbtCompound;
|
||||
import com.comphenix.protocol.wrappers.nbt.NbtFactory;
|
||||
import com.comphenix.protocol.wrappers.nbt.NbtList;
|
||||
import com.comphenix.protocol.wrappers.nbt.NbtType;
|
||||
import com.comphenix.protocol.wrappers.nbt.NbtVisitor;
|
||||
import com.comphenix.protocol.wrappers.nbt.NbtWrapper;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.primitives.Ints;
|
||||
|
||||
/**
|
||||
* Serialize and deserialize NBT information from a configuration section.
|
||||
* <p>
|
||||
* Note that data types may be internally preserved by modifying the serialized name. This may
|
||||
* be visible to the end-user.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class NbtConfigurationSerializer {
|
||||
/**
|
||||
* The default delimiter that is used to store the data type in YAML.
|
||||
*/
|
||||
public static final String TYPE_DELIMITER = "$";
|
||||
|
||||
/**
|
||||
* A standard YAML serializer.
|
||||
*/
|
||||
public static final NbtConfigurationSerializer DEFAULT = new NbtConfigurationSerializer();
|
||||
|
||||
private String dataTypeDelimiter;
|
||||
|
||||
/**
|
||||
* Construct a serializer using {@link #TYPE_DELIMITER} as the default delimiter.
|
||||
*/
|
||||
public NbtConfigurationSerializer() {
|
||||
this.dataTypeDelimiter = TYPE_DELIMITER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a serializer using the given value as a delimiter.
|
||||
* @param dataTypeDelimiter - the local data type delimiter.
|
||||
*/
|
||||
public NbtConfigurationSerializer(String dataTypeDelimiter) {
|
||||
this.dataTypeDelimiter = dataTypeDelimiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current data type delimiter.
|
||||
* @return The current data type delimiter.
|
||||
*/
|
||||
public String getDataTypeDelimiter() {
|
||||
return dataTypeDelimiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the content of a NBT tag to a configuration section.
|
||||
* @param value - the NBT tag to write.
|
||||
* @param destination - the destination section.
|
||||
*/
|
||||
public <TType> void serialize(NbtBase<TType> value, final ConfigurationSection destination) {
|
||||
value.accept(new NbtVisitor() {
|
||||
private ConfigurationSection current = destination;
|
||||
|
||||
// The current list we're working on
|
||||
private List<Object> currentList;
|
||||
|
||||
// Store the index of a configuration section that works like a list
|
||||
private Map<ConfigurationSection, Integer> workingIndex = Maps.newHashMap();
|
||||
|
||||
@Override
|
||||
public boolean visitEnter(NbtCompound compound) {
|
||||
current = current.createSection(compound.getName());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visitEnter(NbtList<?> list) {
|
||||
Integer listIndex = getNextIndex();
|
||||
String name = getEncodedName(list, listIndex);
|
||||
|
||||
if (list.getElementType().isComposite()) {
|
||||
// Use a configuration section to store this list
|
||||
current = current.createSection(name);
|
||||
workingIndex.put(current, 0);
|
||||
} else {
|
||||
currentList = Lists.newArrayList();
|
||||
current.set(name, currentList);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visitLeave(NbtCompound compound) {
|
||||
current = current.getParent();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visitLeave(NbtList<?> list) {
|
||||
// Write the list to the configuration section
|
||||
if (currentList != null) {
|
||||
// Save and reset the temporary list
|
||||
currentList = null;
|
||||
} else {
|
||||
// Go up a level
|
||||
workingIndex.remove(current);
|
||||
current = current.getParent();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(NbtBase<?> node) {
|
||||
// Are we working on a list?
|
||||
if (currentList == null) {
|
||||
Integer listIndex = getNextIndex();
|
||||
String name = getEncodedName(node, listIndex);
|
||||
|
||||
// Save member
|
||||
current.set(name, node.getValue());
|
||||
|
||||
} else {
|
||||
currentList.add(node.getValue());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Integer getNextIndex() {
|
||||
Integer listIndex = workingIndex.get(current);
|
||||
|
||||
if (listIndex != null)
|
||||
return workingIndex.put(current, listIndex + 1);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
// We need to store the data type somehow
|
||||
private String getEncodedName(NbtBase<?> node, Integer index) {
|
||||
if (index != null)
|
||||
return index + dataTypeDelimiter + node.getType().getRawID();
|
||||
else
|
||||
return node.getName() + dataTypeDelimiter + node.getType().getRawID();
|
||||
}
|
||||
|
||||
private String getEncodedName(NbtList<?> node, Integer index) {
|
||||
if (index != null)
|
||||
return index + dataTypeDelimiter + node.getElementType().getRawID();
|
||||
else
|
||||
return node.getName() + dataTypeDelimiter + node.getElementType().getRawID();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a NBT tag from a root configuration.
|
||||
* @param root - configuration that contains the NBT tag.
|
||||
* @param nodeName - name of the NBT tag.
|
||||
* @return The read NBT tag.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <TType> NbtWrapper<TType> deserialize(ConfigurationSection root, String nodeName) {
|
||||
return (NbtWrapper<TType>) readNode(root, nodeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a NBT compound from a root configuration.
|
||||
* @param root - configuration that contains the NBT compound.
|
||||
* @param nodeName - name of the NBT compound.
|
||||
* @return The read NBT compound.
|
||||
*/
|
||||
public NbtCompound deserializeCompound(YamlConfiguration root, String nodeName) {
|
||||
return (NbtCompound) readNode(root, nodeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a NBT compound from a root configuration.
|
||||
* @param root - configuration that contains the NBT compound.
|
||||
* @param nodeName - name of the NBT compound.
|
||||
* @return The read NBT compound.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> NbtList<T> deserializeList(YamlConfiguration root, String nodeName) {
|
||||
return (NbtList<T>) readNode(root, nodeName);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private NbtWrapper<?> readNode(ConfigurationSection parent, String name) {
|
||||
String[] decoded = getDecodedName(name);
|
||||
Object node = parent.get(name);
|
||||
NbtType type = NbtType.TAG_END;
|
||||
|
||||
// It's possible that the caller isn't aware of the encoded name itself
|
||||
if (node == null) {
|
||||
for (String key : parent.getKeys(false)) {
|
||||
decoded = getDecodedName(key);
|
||||
|
||||
// Great
|
||||
if (decoded[0].equals(name)) {
|
||||
node = parent.get(decoded[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Inform the caller of the problem
|
||||
if (node == null) {
|
||||
throw new IllegalArgumentException("Unable to find node " + name + " in " + parent);
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to decode a NBT type
|
||||
if (decoded.length > 1) {
|
||||
type = NbtType.getTypeFromID(Integer.parseInt(decoded[1]));
|
||||
}
|
||||
|
||||
// Is this a compound?
|
||||
if (node instanceof ConfigurationSection) {
|
||||
// Is this a list of a map?
|
||||
if (type != NbtType.TAG_END) {
|
||||
NbtList<Object> list = NbtFactory.ofList(decoded[0]);
|
||||
ConfigurationSection section = (ConfigurationSection) node;
|
||||
List<String> sorted = sortSet(section.getKeys(false));
|
||||
|
||||
// Read everything in order
|
||||
for (String key : sorted) {
|
||||
NbtBase<Object> base = (NbtBase<Object>) readNode(section, key.toString());
|
||||
base.setName(NbtList.EMPTY_NAME);
|
||||
list.getValue().add(base);
|
||||
}
|
||||
return (NbtWrapper<?>) list;
|
||||
|
||||
} else {
|
||||
NbtCompound compound = NbtFactory.ofCompound(decoded[0]);
|
||||
ConfigurationSection section = (ConfigurationSection) node;
|
||||
|
||||
// As above
|
||||
for (String key : section.getKeys(false))
|
||||
compound.put(readNode(section, key));
|
||||
return (NbtWrapper<?>) compound;
|
||||
}
|
||||
|
||||
} else {
|
||||
// We need to know
|
||||
if (type == NbtType.TAG_END) {
|
||||
throw new IllegalArgumentException("Cannot find encoded type of " + decoded[0] + " in " + name);
|
||||
}
|
||||
|
||||
if (node instanceof List) {
|
||||
NbtList<Object> list = NbtFactory.ofList(decoded[0]);
|
||||
list.setElementType(type);
|
||||
|
||||
for (Object value : (List<Object>) node) {
|
||||
list.addClosest(value);
|
||||
}
|
||||
|
||||
// Add the list
|
||||
return (NbtWrapper<?>) list;
|
||||
|
||||
} else {
|
||||
// Normal node
|
||||
return NbtFactory.ofWrapper(type, decoded[0], node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> sortSet(Set<String> unsorted) {
|
||||
// Convert to integers
|
||||
List<String> sorted = new ArrayList<String>(unsorted);
|
||||
|
||||
Collections.sort(sorted, new Comparator<String>() {
|
||||
@Override
|
||||
public int compare(String o1, String o2) {
|
||||
// Parse the name
|
||||
int index1 = Integer.parseInt(getDecodedName(o1)[0]);
|
||||
int index2 = Integer.parseInt(getDecodedName(o2)[0]);
|
||||
return Ints.compare(index1, index2);
|
||||
}
|
||||
});
|
||||
return sorted;
|
||||
}
|
||||
|
||||
private String[] getDecodedName(String nodeName) {
|
||||
int delimiter = nodeName.lastIndexOf('$');
|
||||
|
||||
if (delimiter > 0)
|
||||
return new String[] { nodeName.substring(0, delimiter), nodeName.substring(delimiter + 1) };
|
||||
else
|
||||
return new String[] { nodeName };
|
||||
}
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
package com.comphenix.protocol.wrappers.nbt.io;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
|
||||
import com.comphenix.protocol.wrappers.nbt.NbtBase;
|
||||
import com.comphenix.protocol.wrappers.nbt.NbtCompound;
|
||||
import com.comphenix.protocol.wrappers.nbt.NbtList;
|
||||
import com.comphenix.protocol.wrappers.nbt.NbtWrapper;
|
||||
|
||||
/**
|
||||
* Serializes NBT to a base N (default 32) encoded string and back.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public class NbtTextSerializer {
|
||||
/**
|
||||
* The default radix to use while converting to text.
|
||||
*/
|
||||
public static final int STANDARD_BASE = 32;
|
||||
|
||||
/**
|
||||
* A default instance of this serializer.
|
||||
*/
|
||||
public static final NbtTextSerializer DEFAULT = new NbtTextSerializer();
|
||||
|
||||
private NbtBinarySerializer binarySerializer;
|
||||
private int baseRadix;
|
||||
|
||||
public NbtTextSerializer() {
|
||||
this(new NbtBinarySerializer(), STANDARD_BASE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a serializer with a custom binary serializer and base radix.
|
||||
* @param binary - binary serializer.
|
||||
* @param baseRadix - base radix in the range 2 - 32.
|
||||
*/
|
||||
public NbtTextSerializer(NbtBinarySerializer binary, int baseRadix) {
|
||||
this.binarySerializer = binary;
|
||||
this.baseRadix = baseRadix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the binary serializer that is used.
|
||||
* @return The binary serializer.
|
||||
*/
|
||||
public NbtBinarySerializer getBinarySerializer() {
|
||||
return binarySerializer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the base radix.
|
||||
* @return The base radix.
|
||||
*/
|
||||
public int getBaseRadix() {
|
||||
return baseRadix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a NBT tag to a String.
|
||||
* @param value - the NBT tag to serialize.
|
||||
* @return The NBT tag in base N form.
|
||||
*/
|
||||
public <TType> String serialize(NbtBase<TType> value) {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
DataOutputStream dataOutput = new DataOutputStream(outputStream);
|
||||
|
||||
binarySerializer.serialize(value, dataOutput);
|
||||
|
||||
// Serialize that array
|
||||
return new BigInteger(1, outputStream.toByteArray()).toString(baseRadix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a NBT tag from a base N encoded string.
|
||||
* @param input - the base N string.
|
||||
* @return The NBT tag contained in the string.
|
||||
* @throws IOException If we are unable to parse the input.
|
||||
*/
|
||||
public <TType> NbtWrapper<TType> deserialize(String input) throws IOException {
|
||||
try {
|
||||
BigInteger baseN = new BigInteger(input, baseRadix);
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(baseN.toByteArray());
|
||||
|
||||
return binarySerializer.deserialize(new DataInputStream(inputStream));
|
||||
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException("Input is not valid base " + baseRadix + ".", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a NBT compound from a base N encoded string.
|
||||
* @param input - the base N string.
|
||||
* @return The NBT tag contained in the string.
|
||||
* @throws IOException If we are unable to parse the input.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public NbtCompound deserializeCompound(String input) throws IOException {
|
||||
// I always seem to override generics ...
|
||||
return (NbtCompound) (NbtBase) deserialize(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a NBT list from a base N encoded string.
|
||||
* @param input - the base N string.
|
||||
* @return The NBT tag contained in the string.
|
||||
* @throws IOException If we are unable to parse the input.
|
||||
*/
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
public <T> NbtList<T> deserializeList(String input) throws IOException {
|
||||
return (NbtList<T>) (NbtBase) deserialize(input);
|
||||
}
|
||||
}
|
@ -90,6 +90,11 @@ public class NbtCompoundTest {
|
||||
@Override
|
||||
public NbtBase<TValue> deepClone() {
|
||||
return new NbtCustomTag<TValue>(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(NbtVisitor visitor) {
|
||||
return visitor.visit(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.wrappers.nbt.io.NbtBinarySerializer;
|
||||
|
||||
public class NbtFactoryTest {
|
||||
@BeforeClass
|
||||
@ -54,7 +55,7 @@ public class NbtFactoryTest {
|
||||
ByteArrayInputStream source = new ByteArrayInputStream(buffer.toByteArray());
|
||||
DataInput input = new DataInputStream(source);
|
||||
|
||||
NbtCompound cloned = (NbtCompound) NbtFactory.fromStream(input);
|
||||
NbtCompound cloned = NbtBinarySerializer.DEFAULT.deserializeCompound(input);
|
||||
|
||||
assertEquals(compound.getString("name"), cloned.getString("name"));
|
||||
assertEquals(compound.getInteger("age"), cloned.getInteger("age"));
|
||||
|
@ -0,0 +1,37 @@
|
||||
package com.comphenix.protocol.wrappers.nbt.io;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.wrappers.nbt.NbtCompound;
|
||||
import com.comphenix.protocol.wrappers.nbt.NbtFactory;
|
||||
|
||||
public class NbtConfigurationSerializerTest {
|
||||
@BeforeClass
|
||||
public static void initializeBukkit() {
|
||||
// Initialize reflection
|
||||
MinecraftReflection.setMinecraftPackage("net.minecraft.server.v1_4_6", "org.bukkit.craftbukkit.v1_4_6");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testSerialization() {
|
||||
NbtCompound compound = NbtFactory.ofCompound("hello");
|
||||
compound.put("age", (short) 30);
|
||||
compound.put("name", "test");
|
||||
compound.put(NbtFactory.ofList("telephone", "12345678", "81549300"));
|
||||
|
||||
compound.put(NbtFactory.ofList("lists", NbtFactory.ofList("", "a", "a", "b", "c")));
|
||||
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
NbtConfigurationSerializer.DEFAULT.serialize(compound, yaml);
|
||||
|
||||
NbtCompound result = NbtConfigurationSerializer.DEFAULT.deserializeCompound(yaml, "hello");
|
||||
|
||||
assertEquals(compound, result);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user