ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/io/NbtConfigurationSerializer....

342 lines
9.9 KiB
Java

package com.comphenix.protocol.wrappers.nbt.io;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.HashMap;
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.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 <TType> Type
* @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 = new HashMap<>();
@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 = new ArrayList<>();
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, fromNodeValue(node));
} else {
currentList.add(fromNodeValue(node));
}
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 <TType> Type
* @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 <T> Type
* @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(toNodeValue(value, type));
}
// Add the list
return (NbtWrapper<?>) list;
} else {
// Normal node
return NbtFactory.ofWrapper(type, decoded[0], toNodeValue(node, type));
}
}
}
private List<String> sortSet(Set<String> unsorted) {
// Convert to integers
final List<String> sorted = new ArrayList<>(unsorted);
// Parse the name and sort.
sorted.sort((o1, o2) -> Ints.compare(Integer.parseInt(getDecodedName(o1)[0]), Integer.parseInt(getDecodedName(o2)[0])));
return sorted;
}
// Ensure that int arrays are converted to byte arrays
private Object fromNodeValue(NbtBase<?> base) {
if (base.getType() == NbtType.TAG_INT_ARRAY)
return toByteArray((int[]) base.getValue());
else
return base.getValue();
}
// Convert them back
public Object toNodeValue(Object value, NbtType type) {
if (type == NbtType.TAG_INT_ARRAY)
return toIntegerArray((byte[]) value);
else
return value;
}
/**
* Convert an integer array to an equivalent byte array.
* @param data - the integer array with the data.
* @return An equivalent byte array.
*/
private static byte[] toByteArray(int[] data) {
ByteBuffer byteBuffer = ByteBuffer.allocate(data.length * 4);
IntBuffer intBuffer = byteBuffer.asIntBuffer();
intBuffer.put(data);
return byteBuffer.array();
}
/**
* Convert a byte array to the equivalent integer array.
* <p>
* Note that the number of byte elements are only perserved if the byte size is a multiple of four.
* @param data - the byte array to convert.
* @return The equivalent integer array.
*/
private static int[] toIntegerArray(byte[] data) {
IntBuffer source = ByteBuffer.wrap(data).asIntBuffer();
IntBuffer copy = IntBuffer.allocate(source.capacity());
copy.put(source);
return copy.array();
}
private static 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 };
}
}