Added the ability to serialize and deserialize NBT to many formats.

This commit is contained in:
Kristian S. Stangeland 2013-01-09 04:50:04 +01:00
parent 3426306805
commit dbc28c0035
15 changed files with 772 additions and 84 deletions

View File

@ -29,6 +29,13 @@ import com.comphenix.protocol.wrappers.nbt.NbtType;
* @param <TType> - type of the value that is stored. * @param <TType> - type of the value that is stored.
*/ */
public interface NbtBase<TType> { 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. * Retrieve the type of this NBT element.
* @return The type of this NBT element. * @return The type of this NBT element.

View File

@ -10,7 +10,7 @@ import java.util.Set;
* <p> * <p>
* Use {@link NbtFactory} to load or create an instance. * Use {@link NbtFactory} to load or create an instance.
* <p> * <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. * of this NBT compound, but may throw an {@link UnsupportedOperationException} for any of the write operations.
* *
* @author Kristian * @author Kristian
@ -74,6 +74,14 @@ public interface NbtCompound extends NbtBase<Map<String, NbtBase<?>>>, Iterable<
*/ */
public abstract NbtCompound put(String key, String value); 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. * Retrieve the byte value of an entry identified by a given key.
* @param key - the key of the entry. * @param key - the key of the entry.
@ -286,7 +294,7 @@ public interface NbtCompound extends NbtBase<Map<String, NbtBase<?>>>, Iterable<
* @param list - the list value. * @param list - the list value.
* @return This current compound, for chaining. * @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. * Associate a new NBT list with the given key.

View File

@ -17,8 +17,6 @@
package com.comphenix.protocol.wrappers.nbt; package com.comphenix.protocol.wrappers.nbt;
import java.io.DataInput;
import java.io.DataOutput;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@ -41,10 +39,6 @@ public class NbtFactory {
// Used to create the underlying tag // Used to create the underlying tag
private static Method methodCreateTag; private static Method methodCreateTag;
// Used to read and write NBT
private static Method methodWrite;
private static Method methodLoad;
// Item stack trickery // Item stack trickery
private static StructureModifier<Object> itemStackModifier; private static StructureModifier<Object> itemStackModifier;
@ -102,7 +96,7 @@ public class NbtFactory {
} else if (base.getType() == NbtType.TAG_LIST) { } else if (base.getType() == NbtType.TAG_LIST) {
// As above // As above
WrappedList<T> copy = WrappedList.fromName(base.getName()); NbtList<T> copy = WrappedList.fromName(base.getName());
copy.setValue((List<NbtBase<T>>) base.getValue()); copy.setValue((List<NbtBase<T>>) base.getValue());
return (NbtWrapper<T>) copy; return (NbtWrapper<T>) copy;
@ -167,48 +161,6 @@ public class NbtFactory {
return partial; 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. * Constructs a NBT tag of type string.
* @param name - name of the tag. * @param name - name of the tag.
@ -276,7 +228,7 @@ public class NbtFactory {
* @return The constructed NBT tag. * @return The constructed NBT tag.
*/ */
public static NbtBase<Double> of(String name, double value) { public static NbtBase<Double> of(String name, double value) {
return ofWrapper(NbtType.TAG_DOUBlE, name, value); return ofWrapper(NbtType.TAG_DOUBLE, name, value);
} }
/** /**

View File

@ -24,10 +24,26 @@ public interface NbtList<TType> extends NbtBase<List<NbtBase<TType>>>, Iterable<
/** /**
* 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. * @return Element type.
*/ */
public abstract NbtType getElementType(); 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. * Add a NBT list or NBT compound to the list.
* @param element * @param element

View File

@ -62,7 +62,7 @@ public enum NbtType {
/** /**
* A signed 8 byte floating point type. * A signed 8 byte floating point type.
*/ */
TAG_DOUBlE(6, double.class), TAG_DOUBLE(6, double.class),
/** /**
* An array of bytes. * An array of bytes.
@ -113,6 +113,10 @@ public enum NbtType {
classLookup.put(Primitives.wrap(type.getValueType()), type); 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) { private NbtType(int rawID, Class<?> valueType) {
@ -120,6 +124,14 @@ public enum NbtType {
this.valueType = valueType; 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. * Retrieves the raw unique integer that identifies the type of the parent NBT element.
* @return Integer that uniquely identifying the type. * @return Integer that uniquely identifying the type.
@ -157,9 +169,16 @@ public enum NbtType {
NbtType result = classLookup.get(clazz); NbtType result = classLookup.get(clazz);
// Try to lookup this value // Try to lookup this value
if (result != null) if (result != null) {
return result; 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); throw new IllegalArgumentException("No NBT tag can represent a " + clazz);
} }
} }
}

View File

@ -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);
}

View File

@ -23,6 +23,8 @@ import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import com.comphenix.protocol.wrappers.nbt.io.NbtBinarySerializer;
/** /**
* A concrete implementation of an NbtCompound that wraps an underlying NMS Compound. * A concrete implementation of an NbtCompound that wraps an underlying NMS Compound.
* *
@ -67,6 +69,19 @@ class WrappedCompound implements NbtWrapper<Map<String, NbtBase<?>>>, Iterable<N
this.container = new WrappedElement<Map<String,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 @Override
public Object getHandle() { public Object getHandle() {
return container.getHandle(); return container.getHandle();
@ -420,7 +435,7 @@ class WrappedCompound implements NbtWrapper<Map<String, NbtBase<?>>>, Iterable<N
*/ */
@Override @Override
public double getDoubleOrDefault(String key) { 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. * @return This current compound, for chaining.
*/ */
@Override @Override
public <T> NbtCompound put(WrappedList<T> list) { public <T> NbtCompound put(NbtList<T> list) {
getValue().put(list.getName(), list); getValue().put(list.getName(), list);
return this; 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. * Associate a new NBT list with the given key.
* @param key - the key and name of the new NBT list. * @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 @Override
public void write(DataOutput destination) { public void write(DataOutput destination) {
NbtFactory.toStream(container, destination); NbtBinarySerializer.DEFAULT.serialize(container, destination);
} }
@Override @Override

View File

@ -24,6 +24,7 @@ import com.comphenix.protocol.reflect.FieldAccessException;
import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.reflect.StructureModifier; import com.comphenix.protocol.reflect.StructureModifier;
import com.comphenix.protocol.utility.MinecraftReflection; import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.wrappers.nbt.io.NbtBinarySerializer;
import com.google.common.base.Objects; import com.google.common.base.Objects;
/** /**
@ -106,6 +107,11 @@ class WrappedElement<TType> implements NbtWrapper<TType> {
return modifier; return modifier;
} }
@Override
public boolean accept(NbtVisitor visitor) {
return visitor.visit(this);
}
/** /**
* Retrieve the underlying NBT tag object. * Retrieve the underlying NBT tag object.
* @return The underlying Minecraft tag object. * @return The underlying Minecraft tag object.
@ -173,7 +179,8 @@ class WrappedElement<TType> implements NbtWrapper<TType> {
@Override @Override
public void write(DataOutput destination) { public void write(DataOutput destination) {
NbtFactory.toStream(this, destination); // No need to cache this object
NbtBinarySerializer.DEFAULT.serialize(this, destination);
} }
@Override @Override

View File

@ -24,6 +24,7 @@ import java.util.List;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import com.comphenix.protocol.wrappers.nbt.io.NbtBinarySerializer;
import com.google.common.base.Function; import com.google.common.base.Function;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
@ -41,13 +42,17 @@ class WrappedList<TType> implements NbtWrapper<List<NbtBase<TType>>>, Iterable<T
// Saved wrapper list // Saved wrapper list
private ConvertedList<Object, NbtBase<TType>> savedList; private ConvertedList<Object, NbtBase<TType>> savedList;
// Element type
private NbtType elementType = NbtType.TAG_END;
/** /**
* Construct a new empty NBT list. * Construct a new empty NBT list.
* @param name - name of this list. * @param name - name of this list.
* @return The new empty NBT list. * @return The new empty NBT list.
*/ */
public static <T> WrappedList<T> fromName(String name) { @SuppressWarnings("unchecked")
return (WrappedList<T>) NbtFactory.<List<NbtBase<T>>>ofWrapper(NbtType.TAG_LIST, name); public static <T> NbtList<T> fromName(String name) {
return (NbtList<T>) NbtFactory.<List<NbtBase<T>>>ofWrapper(NbtType.TAG_LIST, name);
} }
/** /**
@ -56,12 +61,17 @@ class WrappedList<TType> implements NbtWrapper<List<NbtBase<TType>>>, Iterable<T
* @param elements - values to add. * @param elements - values to add.
* @return The new filled NBT list. * @return The new filled NBT list.
*/ */
public static <T> WrappedList<T> fromArray(String name, T... elements) { @SuppressWarnings({"unchecked", "rawtypes"})
WrappedList<T> result = fromName(name); public static <T> NbtList<T> fromArray(String name, T... elements) {
NbtList<T> result = fromName(name);
for (T element : elements) { for (T element : elements) {
if (element == null) if (element == null)
throw new IllegalArgumentException("An NBT list cannot contain a null element!"); throw new IllegalArgumentException("An NBT list cannot contain a null element!");
if (element instanceof NbtBase)
result.add((NbtBase) element);
else
result.add(NbtFactory.ofWrapper(element.getClass(), EMPTY_NAME, element)); result.add(NbtFactory.ofWrapper(element.getClass(), EMPTY_NAME, element));
} }
return result; return result;
@ -73,19 +83,42 @@ class WrappedList<TType> implements NbtWrapper<List<NbtBase<TType>>>, Iterable<T
* @param elements - elements to add. * @param elements - elements to add.
* @return The new filled NBT list. * @return The new filled NBT list.
*/ */
public static <T> WrappedList<T> fromList(String name, Collection<? extends T> elements) { @SuppressWarnings({"unchecked", "rawtypes"})
WrappedList<T> result = fromName(name); public static <T> NbtList<T> fromList(String name, Collection<? extends T> elements) {
NbtList<T> result = fromName(name);
for (T element : elements) { for (T element : elements) {
if (element == null) if (element == null)
throw new IllegalArgumentException("An NBT list cannot contain a null element!"); throw new IllegalArgumentException("An NBT list cannot contain a null element!");
if (element instanceof NbtBase)
result.add((NbtBase) element);
else
result.add(NbtFactory.ofWrapper(element.getClass(), EMPTY_NAME, element)); result.add(NbtFactory.ofWrapper(element.getClass(), EMPTY_NAME, element));
} }
return result; return result;
} }
/**
* Construct a list from an NMS instance.
* @param handle - NMS instance.
*/
public WrappedList(Object handle) { public WrappedList(Object handle) {
this.container = new WrappedElement<List<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 @Override
@ -98,13 +131,15 @@ class WrappedList<TType> implements NbtWrapper<List<NbtBase<TType>>>, Iterable<T
return NbtType.TAG_LIST; return NbtType.TAG_LIST;
} }
/**
* Get the type of each element.
* @return Element type.
*/
@Override @Override
public NbtType getElementType() { public NbtType getElementType() {
return container.getSubType(); return elementType;
}
@Override
public void setElementType(NbtType type) {
this.elementType = type;
container.setSubType(type);
} }
@Override @Override
@ -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."); throw new IllegalArgumentException("Cannot add a the named NBT tag " + element + " to a list.");
// Check element type // Check element type
if (size() > 0) { if (getElementType() != NbtType.TAG_END) {
if (!element.getType().equals(getElementType())) { if (!element.getType().equals(getElementType())) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Cannot add " + element + " of " + element.getType() + " to a list of type " + getElementType()); "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 @Override
public boolean addAll(Collection<? extends NbtBase<TType>> c) { public boolean addAll(Collection<? extends NbtBase<TType>> c) {
boolean empty = size() == 0;
boolean result = false; boolean result = false;
for (NbtBase<TType> element : c) { for (NbtBase<TType> element : c) {
add(element); add(element);
result = true; result = true;
} }
// See if we now added our first object(s)
if (empty && result) {
container.setSubType(get(0).getType());
}
return result; return result;
} }
@ -197,6 +226,38 @@ class WrappedList<TType> implements NbtWrapper<List<NbtBase<TType>>>, Iterable<T
return (NbtBase) container.deepClone(); 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 @Override
public void add(NbtBase<TType> element) { public void add(NbtBase<TType> element) {
getValue().add(element); getValue().add(element);
@ -293,7 +354,7 @@ class WrappedList<TType> implements NbtWrapper<List<NbtBase<TType>>>, Iterable<T
@Override @Override
public void write(DataOutput destination) { public void write(DataOutput destination) {
NbtFactory.toStream(container, destination); NbtBinarySerializer.DEFAULT.serialize(container, destination);
} }
@Override @Override

View File

@ -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);
}
}

View File

@ -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 };
}
}

View File

@ -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);
}
}

View File

@ -91,5 +91,10 @@ public class NbtCompoundTest {
public NbtBase<TValue> deepClone() { public NbtBase<TValue> deepClone() {
return new NbtCustomTag<TValue>(name, value); return new NbtCustomTag<TValue>(name, value);
} }
@Override
public boolean accept(NbtVisitor visitor) {
return visitor.visit(this);
}
} }
} }

View File

@ -30,6 +30,7 @@ import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import com.comphenix.protocol.utility.MinecraftReflection; import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.wrappers.nbt.io.NbtBinarySerializer;
public class NbtFactoryTest { public class NbtFactoryTest {
@BeforeClass @BeforeClass
@ -54,7 +55,7 @@ public class NbtFactoryTest {
ByteArrayInputStream source = new ByteArrayInputStream(buffer.toByteArray()); ByteArrayInputStream source = new ByteArrayInputStream(buffer.toByteArray());
DataInput input = new DataInputStream(source); 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.getString("name"), cloned.getString("name"));
assertEquals(compound.getInteger("age"), cloned.getInteger("age")); assertEquals(compound.getInteger("age"), cloned.getInteger("age"));

View File

@ -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);
}
}