buildcraft/common/buildcraft/core/network/serializers/ClassMapping.java
SpaceToad 9577c53313 Fixed gate extension ids synchronization, for #1895.
Added a new concept of NetworkId, allowing to transfer ids over the network
instead of strings.
RPCs are now all handled the same way (except RPC Pipes, to be completed when
actually used).
2014-06-22 11:49:59 +02:00

645 lines
16 KiB
Java

/**
* Copyright (c) 2011-2014, SpaceToad and the BuildCraft Team
* http://www.mod-buildcraft.com
*
* BuildCraft is distributed under the terms of the Minecraft Mod Public
* License 1.0, or MMPL. Please check the contents of the license located in
* http://www.mod-buildcraft.com/MMPL-1.0.txt
*/
package buildcraft.core.network.serializers;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import io.netty.buffer.ByteBuf;
import net.minecraft.block.Block;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraftforge.fluids.FluidStack;
import buildcraft.api.core.JavaTools;
import buildcraft.api.core.NetworkData;
import buildcraft.core.network.INBTSerializable;
import buildcraft.core.network.NetworkIdRegistry;
/**
* This class implements custom class mapping. There are three advantages in
* using a custom serializer here:
*
* (1) the approach is constructive instead of destructive, that is to say,
* only marked fields will be taken. Granted, this is mostly coding style
* related, but this prevent introduction of useless serialized data by
* mistake.
*
* (2) we can introduce specific serialized types. For example (although not
* yet implemented?) we will be able to implement a tile as a reference to
* this tile through e.g. {x, y, z}, that is know what needs to be serialized,
* know what needs to be referenced, and how to reference it.
*
* (3) again, not yet implemented, but we can in theory have different set
* of serialization depending on the context.
*
* HISTORY NOTE
*
* This was initially developed because the initial network framework only
* allowed for byte, float and int, so more things were needed. To the light
* of current understanding, using only byte would have been good enough.
*
* It seems like the three points above indeed give more value and safety to
* the whole code and make this system still relevant. To be re-evaluated.
*
* QUESTION ON OBJECTS
*
* At the moment, we do not support object creation from this interface, so
* the objects are supposed to be already there and then updated. This may
* not always make sense, in particular in the context of RPC
*
* Non-null arrays of objects are forbidden as well, and they need to be set
* to the same null and non-null elements on both sides.
*
*/
public class ClassMapping extends ClassSerializer {
private static Map<String, ClassSerializer> classes = new TreeMap<String, ClassSerializer>();
private LinkedList<Field> floatFields = new LinkedList<Field>();
private LinkedList<Field> doubleFields = new LinkedList<Field>();
private LinkedList<Field> shortFields = new LinkedList<Field>();
private LinkedList<Field> intFields = new LinkedList<Field>();
private LinkedList<Field> booleanFields = new LinkedList<Field>();
private LinkedList<Field> enumFields = new LinkedList<Field>();
class FieldObject {
public Field field;
public ClassSerializer mapping;
}
private LinkedList<FieldObject> objectFields = new LinkedList<FieldObject>();
enum CptType {
Byte,
Float,
Double,
Short,
Int,
Boolean,
Enum,
Object
}
private CptType cptType;
private ClassSerializer cptMapping;
public ClassMapping() {
}
@SuppressWarnings({ "rawtypes" })
public void analyzeClass(final Class<? extends Object> c) {
try {
if (c.isArray()) {
Class cptClass = c.getComponentType();
if (byte.class.equals(cptClass)) {
cptType = CptType.Byte;
} else if (float.class.equals(cptClass)) {
cptType = CptType.Float;
} else if (double.class.equals(cptClass)) {
cptType = CptType.Double;
} else if (short.class.equals(cptClass)) {
cptType = CptType.Short;
} else if (int.class.equals(cptClass)) {
cptType = CptType.Int;
} else if (boolean.class.equals(cptClass)) {
cptType = CptType.Byte;
} else if (Enum.class.isAssignableFrom(cptClass)) {
cptType = CptType.Enum;
} else {
cptType = CptType.Object;
cptMapping = get (cptClass);
}
} else {
List<Field> fields = JavaTools.getAllFields(c);
for (Field f : fields) {
if (!isSynchronizedField(f)) {
continue;
}
f.setAccessible(true);
Type t = f.getType();
if (t instanceof Class) {
Class fieldClass = (Class) t;
if (short.class.equals(fieldClass)) {
shortFields.add(f);
} else if (int.class.equals(fieldClass)) {
intFields.add(f);
} else if (boolean.class.equals(fieldClass)) {
booleanFields.add(f);
} else if (Enum.class.isAssignableFrom(fieldClass)) {
enumFields.add(f);
} else if (float.class.equals(fieldClass)) {
floatFields.add(f);
} else if (double.class.equals(fieldClass)) {
doubleFields.add(f);
} else {
FieldObject obj = new FieldObject();
obj.mapping = get (fieldClass);
obj.field = f;
objectFields.add(obj);
}
}
}
}
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
}
private boolean isSynchronizedField(Field f) {
NetworkData updateAnnotation = f.getAnnotation(NetworkData.class);
return updateAnnotation != null;
}
/**
* This class will update data in an object from a stream. Public data
* market #NetworkData will get synchronized. The following rules will
* apply:
*
* In the following description, we consider strings as primitive objects.
*
* Market primitives data will be directly updated on the destination
* object after the value of the source object
*
* Market primitive arrays will be re-created in the destination object
* after the primitive array of the source object. This means that array
* references are not preserved by the proccess. If an array is null
* in the source array and not in the destination one, it will be turned to
* null.
*
* Market object will be synchronized - that it we do not create new
* instances in the destination object if they are already there but rather
* recursively synchronize values. If destination is null and not
* source, the destination will get the instance created. If destination is
* not null and source is, the destination will get truned to null.
*
* Market object arrays will be synchronized - not re-created. If
* destination is null and not source, the destination will get the instance
* created. If destination is not null and source is, the destination will
* get turned to null. The same behavior applies to the contents of the
* array. Trying to synchronize two arrays of different size is an error
* and will lead to an exception - so if the array needs to change on the
* destination it needs to be set to null first.
*
* WARNINGS
*
* - only public non-final fields can be serialized
* - non static nested classes are not supported
* - no reference analysis is done, e.g. an object referenced twice will
* be serialized twice
*/
@Override
public void write(ByteBuf data, Object o, SerializationContext context) throws IllegalArgumentException, IllegalAccessException {
if (o == null) {
data.writeBoolean(false);
} else {
data.writeBoolean(true);
if (mappedClass.isArray()) {
writeArray(o, data, context);
} else {
writeClass(o, data, context);
}
}
}
@Override
public Object read(ByteBuf data, Object o, SerializationContext context)
throws IllegalArgumentException, IllegalAccessException,
InstantiationException, ClassNotFoundException {
if (!data.readBoolean()) {
return null;
} else {
if (mappedClass.isArray()) {
return readArray(o, data, context);
} else {
return readClass(o, data, context);
}
}
}
@SuppressWarnings("rawtypes")
void writeClass(Object obj, ByteBuf data, SerializationContext context) throws IllegalArgumentException,
IllegalAccessException {
Class realClass = obj.getClass();
if (realClass.equals(this.mappedClass)) {
data.writeByte(0);
} else {
NetworkIdRegistry.write(data, realClass.getCanonicalName());
ClassMapping delegateMapping = (ClassMapping) get(realClass);
return;
}
for (Field f : shortFields) {
data.writeShort(f.getShort(obj));
}
for (Field f : intFields) {
data.writeInt(f.getInt(obj));
}
for (Field f : booleanFields) {
data.writeBoolean(f.getBoolean(obj));
}
for (Field f : enumFields) {
if (f.get(obj) == null) {
data.writeBoolean(false);
} else {
data.writeBoolean(true);
data.writeByte(((Enum) f.get(obj)).ordinal());
}
}
for (Field f : floatFields) {
data.writeFloat(f.getFloat(obj));
}
for (Field f : doubleFields) {
data.writeDouble(f.getDouble(obj));
}
for (FieldObject f : objectFields) {
Object cpt = f.field.get(obj);
f.mapping.write(data, cpt, context);
}
}
@SuppressWarnings("rawtypes")
Object readClass(Object objI, ByteBuf data, SerializationContext context) throws IllegalArgumentException,
IllegalAccessException, InstantiationException, ClassNotFoundException {
Object obj = objI;
// The data layout for an object is the following:
// [boolean] does the object exist (e.g. non-null)
// {false} exit
// [int] what is the object real class?
// {0} the same as the declared class
// {1-x} the network id of the class
// [bytes] the actual contents
int index = data.readByte();
if (index != 0) {
String className = NetworkIdRegistry.read(data);
Class cls = Class.forName(className);
ClassMapping delegateMapping = (ClassMapping) get(cls);
return delegateMapping.readClass(obj, data, context);
}
if (obj == null) {
obj = mappedClass.newInstance();
}
for (Field f : shortFields) {
f.setShort(obj, data.readShort());
}
for (Field f : intFields) {
f.setInt(obj, data.readInt());
}
for (Field f : booleanFields) {
f.setBoolean(obj, data.readBoolean());
}
for (Field f : enumFields) {
if (data.readBoolean()) {
f.set(obj, ((Class) f.getGenericType()).getEnumConstants()[data.readByte()]);
}
}
for (Field f : floatFields) {
f.setFloat(obj, data.readFloat());
}
for (Field f : doubleFields) {
f.setDouble(obj, data.readDouble());
}
for (FieldObject f : objectFields) {
f.field.set(obj, f.mapping.read(data, f.field.get(obj), context));
}
return obj;
}
private void writeArray(Object obj, ByteBuf data, SerializationContext context) throws IllegalArgumentException,
IllegalAccessException {
switch (cptType) {
case Byte: {
byte[] arr = (byte[]) obj;
data.writeInt (arr.length);
data.writeBytes(arr);
break;
}
case Float: {
float[] arr = (float[]) obj;
data.writeInt (arr.length);
for (float element : arr) {
data.writeFloat(element);
}
break;
}
case Double: {
double[] arr = (double[]) obj;
data.writeInt (arr.length);
for (double element : arr) {
data.writeDouble(element);
}
break;
}
case Short: {
short[] arr = (short[]) obj;
data.writeInt (arr.length);
for (short element : arr) {
data.writeShort(element);
}
break;
}
case Int: {
int[] arr = (int[]) obj;
data.writeInt (arr.length);
for (int element : arr) {
data.writeInt(element);
}
break;
}
case Boolean: {
boolean[] arr = (boolean[]) obj;
data.writeInt (arr.length);
for (boolean element : arr) {
data.writeBoolean(element);
}
break;
}
case Enum: {
Enum[] arr = (Enum[]) obj;
data.writeInt (arr.length);
for (Enum<?> element : arr) {
data.writeBoolean(element != null);
if (element != null) {
data.writeByte(element.ordinal());
}
}
break;
}
case Object: {
Object[] arr = (Object[]) obj;
data.writeInt(arr.length);
for (Object element : arr) {
cptMapping.write(data, element, context);
}
break;
}
}
}
private Object readArray(Object objI, ByteBuf data, SerializationContext context) throws IllegalArgumentException,
IllegalAccessException, InstantiationException, ClassNotFoundException {
Object obj = objI;
Class<? extends Object> cpt = mappedClass.getComponentType();
int size = data.readInt();
switch (cptType) {
case Byte: {
byte[] arr;
if (obj == null) {
arr = new byte[size];
} else {
arr = (byte[]) obj;
}
data.readBytes (arr);
obj = arr;
break;
}
case Float: {
float[] arr;
if (obj == null) {
arr = new float[size];
} else {
arr = (float[]) obj;
}
for (int i = 0; i < arr.length; ++i) {
arr[i] = data.readFloat();
}
obj = arr;
break;
}
case Double: {
double[] arr;
if (obj == null) {
arr = new double[size];
} else {
arr = (double[]) obj;
}
for (int i = 0; i < arr.length; ++i) {
arr[i] = data.readDouble();
}
obj = arr;
break;
}
case Short: {
short[] arr;
if (obj == null) {
arr = new short[size];
} else {
arr = (short[]) obj;
}
for (int i = 0; i < arr.length; ++i) {
arr[i] = data.readShort();
}
obj = arr;
break;
}
case Int: {
int[] arr;
if (obj == null) {
arr = new int[size];
} else {
arr = (int[]) obj;
}
for (int i = 0; i < arr.length; ++i) {
arr[i] = data.readInt();
}
obj = arr;
break;
}
case Boolean: {
boolean[] arr;
if (obj == null) {
arr = new boolean[size];
} else {
arr = (boolean[]) obj;
}
for (int i = 0; i < arr.length; ++i) {
arr[i] = data.readBoolean();
}
obj = arr;
break;
}
case Enum: {
Enum[] arr;
if (obj == null) {
arr = new Enum[size];
} else {
arr = (Enum[]) obj;
}
for (int i = 0; i < arr.length; ++i) {
if (data.readBoolean()) {
arr[i] = (Enum<?>) cpt.getEnumConstants()[data.readByte()];
} else {
arr[i] = null;
}
}
obj = arr;
break;
}
case Object: {
Object[] arr;
if (obj == null) {
arr = (Object[]) Array.newInstance(cpt, size);
} else {
arr = (Object[]) obj;
}
for (int i = 0; i < arr.length; ++i) {
arr[i] = cptMapping.read(data, arr[i], context);
}
obj = arr;
break;
}
}
return obj;
}
private static void registerSerializer (Class<?> clas, ClassSerializer s) {
try {
s.mappedClass = clas;
classes.put(clas.getCanonicalName(), s);
} catch (Throwable t) {
t.printStackTrace();
throw new RuntimeException("Can't register " + clas.getCanonicalName() + " in serializers");
}
}
public static ClassSerializer get (Class<?> clas) {
ClassSerializer mapping;
if (Block.class.isAssignableFrom(clas)) {
mapping = classes.get(Block.class.getCanonicalName());
} else if (Item.class.isAssignableFrom(clas)) {
mapping = classes.get(Item.class.getCanonicalName());
} else if (INBTSerializable.class.isAssignableFrom(clas)) {
mapping = classes.get(INBTSerializable.class.getCanonicalName());
} else if (!classes.containsKey(clas.getCanonicalName())) {
mapping = new ClassMapping ();
registerSerializer(clas, mapping);
((ClassMapping) mapping).analyzeClass(clas);
} else {
mapping = classes.get(clas.getCanonicalName());
}
return mapping;
}
static {
registerSerializer(String.class, new SerializerString());
registerSerializer(HashMap.class, new SerializerHashMap());
registerSerializer(HashSet.class, new SerializerHashSet());
registerSerializer(LinkedList.class, new SerializerLinkedList());
registerSerializer(ArrayList.class, new SerializerArrayList());
registerSerializer(Block.class, new SerializerBlock());
registerSerializer(Item.class, new SerializerItem());
registerSerializer(NBTTagCompound.class, new SerializerNBT());
registerSerializer(ItemStack.class, new SerializerItemStack());
registerSerializer(FluidStack.class, new SerializerFluidStack());
registerSerializer(Integer.class, new SerializerInteger());
registerSerializer(INBTSerializable.class, new SerializerINBTSerializable());
}
}