From 04dd29ad566ec33de3e5198b941e222379adc701 Mon Sep 17 00:00:00 2001 From: Gabriel Tofvesson Date: Wed, 25 Jul 2018 01:07:35 +0200 Subject: [PATCH] Redesigned serialization - Created a dynamic serialization system with add-in support - Created a custom flag system (may need rework) Moved primitive serialization system to its own Serializer module Added primitive array serialization module Added some fun reflection toys :) --- src/Main.java | 55 +++- src/net/tofvesson/networking/Arithmetic.kt | 10 + .../networking/PrimitiveArraySerializer.kt | 283 ++++++++++++++++ .../networking/PrimitiveSerializers.kt | 219 +++++++++++++ src/net/tofvesson/networking/Serializer.kt | 49 +++ src/net/tofvesson/networking/SyncHandler.kt | 305 ++++-------------- src/net/tofvesson/networking/SyncedVar.kt | 70 +++- .../networking/UnsupportedTypeException.kt | 9 + src/net/tofvesson/reflect/Accessible.kt | 34 ++ 9 files changed, 786 insertions(+), 248 deletions(-) create mode 100644 src/net/tofvesson/networking/PrimitiveArraySerializer.kt create mode 100644 src/net/tofvesson/networking/PrimitiveSerializers.kt create mode 100644 src/net/tofvesson/networking/Serializer.kt create mode 100644 src/net/tofvesson/networking/UnsupportedTypeException.kt create mode 100644 src/net/tofvesson/reflect/Accessible.kt diff --git a/src/Main.java b/src/Main.java index abf842f..0e93ec1 100644 --- a/src/Main.java +++ b/src/Main.java @@ -2,7 +2,7 @@ import net.tofvesson.networking.SyncHandler; import net.tofvesson.networking.SyncedVar; public class Main { - @SyncedVar(nonNegative = true) + @SyncedVar("NonNegative") public int syncTest = 5; @SyncedVar @@ -17,35 +17,68 @@ public class Main { @SyncedVar public static boolean testbool1 = true; + @SyncedVar + public static boolean[] test = {true, false}; + public static void main(String[] args){ Main testObject = new Main(); SyncHandler sync = new SyncHandler(); sync.registerSyncObject(testObject); sync.registerSyncObject(Main.class); + // Generate mismatch check byte[] mismatchCheck = sync.generateMismatchCheck(); + + // Generate snapshot of values to serialize byte[] ser = sync.serialize(); - System.out.println("Created and serialized snapshot of field values:\n"+testObject.syncTest+"\n"+staticTest+"\n"+value+"\n"+testObject.testbool+"\n"+testbool1+"\n"); + System.out.println("Created and serialized snapshot of field values:\n\t"+ + testObject.syncTest+"\n\t"+ + staticTest+"\n\t"+ + value+"\n\t"+ + testObject.testbool+"\n\t"+ + testbool1+"\n\t"+ + test[0]+"\n\t"+ + test[1]+"\n" + ); + // Modify all the values testObject.syncTest = 20; staticTest = 32; value = 9.0f; testObject.testbool = true; testbool1 = false; + test = new boolean[3]; + test[0] = false; + test[1] = true; + test[2] = true; - System.out.println("Set a new state of test values:\n"+testObject.syncTest+"\n"+staticTest+"\n"+value+"\n"+testObject.testbool+"\n"+testbool1+"\n"); + System.out.println("Set a new state of test values:\n\t"+ + testObject.syncTest+"\n\t"+ + staticTest+"\n\t"+ + value+"\n\t"+ + testObject.testbool+"\n\t"+ + testbool1+"\n\t"+ + test[0]+"\n\t"+ + test[1]+"\n\t"+ + test[2]+"\n" + ); - /* Swap the registry order - sync.unregisterSyncObject(testObject); - sync.unregisterSyncObject(Main.class); - sync.registerSyncObject(Main.class); - sync.registerSyncObject(testObject); - */ + // Do mismatch check if(!sync.doMismatchCheck(mismatchCheck)) throw new RuntimeException("Target sync mismatch"); + + // Load snapshot values back sync.deserialize(ser); - System.out.println("Deserialized snapshot values:\n"+testObject.syncTest+"\n"+staticTest+"\n"+value+"\n"+testObject.testbool+"\n"+testbool1+"\n"); - System.out.println("Snapshot size: "+ser.length+" bytes"); + System.out.println("Deserialized snapshot values:\n\t"+ + testObject.syncTest+"\n\t"+ + staticTest+"\n\t"+ + value+"\n\t"+ + testObject.testbool+"\n\t"+ + testbool1+"\n\t"+ + test[0]+"\n\t"+ + test[1]+"\n\n" + + "Snapshot size: \"+ser.length+\" bytes" + ); } } diff --git a/src/net/tofvesson/networking/Arithmetic.kt b/src/net/tofvesson/networking/Arithmetic.kt index c88ecca..5196465 100644 --- a/src/net/tofvesson/networking/Arithmetic.kt +++ b/src/net/tofvesson/networking/Arithmetic.kt @@ -1,6 +1,7 @@ package net.tofvesson.networking import java.nio.ByteBuffer +import kotlin.experimental.or fun varIntSize(value: Long): Int = when { @@ -103,6 +104,15 @@ fun readVarInt(buffer: ByteBuffer, offset: Int): Long { return res } +fun writeBit(bit: Boolean, buffer: ByteArray, index: Int){ + buffer[index shr 3] = buffer[index shr 3].or(((if(bit) 1 else 0) shl (index and 7)).toByte()) +} +fun readBit(buffer: ByteArray, index: Int): Boolean = buffer[index shr 3].toInt() and (1 shl (index and 7)) != 0 +fun writeBit(bit: Boolean, buffer: ByteBuffer, index: Int){ + buffer.put(index shr 3, buffer[index shr 3] or (((if(bit) 1 else 0) shl (index and 7)).toByte())) +} +fun readBit(buffer: ByteBuffer, index: Int): Boolean = buffer[index shr 3].toInt() and (1 shl (index and 7)) != 0 + private val converter = ByteBuffer.allocateDirect(8) fun floatToInt(value: Float): Int = diff --git a/src/net/tofvesson/networking/PrimitiveArraySerializer.kt b/src/net/tofvesson/networking/PrimitiveArraySerializer.kt new file mode 100644 index 0000000..98276d2 --- /dev/null +++ b/src/net/tofvesson/networking/PrimitiveArraySerializer.kt @@ -0,0 +1,283 @@ +package net.tofvesson.networking + +import java.lang.reflect.Field +import java.nio.ByteBuffer + +class PrimitiveArraySerializer private constructor(): Serializer(arrayOf( + BooleanArray::class.java, + ByteArray::class.java, + ShortArray::class.java, + IntArray::class.java, + LongArray::class.java, + FloatArray::class.java, + DoubleArray::class.java +)) { + companion object { + const val flagKnownSize = "knownSize" + private val knownSize = SyncFlag.createFlag(flagKnownSize) + val singleton = PrimitiveArraySerializer() + } + + override fun computeSize(field: Field, flags: Array, owner: Any?): Pair { + val arrayLength = java.lang.reflect.Array.getLength(field.get(owner)) + var byteSize = if(flags.contains(knownSize)) 0 else varIntSize(arrayLength.toLong()) + var bitSize = 0 + when (field.type) { + BooleanArray::class.java -> bitSize = arrayLength + ByteArray::class.java -> byteSize += arrayLength + ShortArray::class.java -> + if(flags.contains(SyncFlag.NoCompress)) byteSize += arrayLength * 2 + else + for(value in field.get(owner) as ShortArray) + byteSize += + varIntSize( + if(flags.contains(SyncFlag.NonNegative)) value.toLong() + else zigZagEncode(value.toLong()) + ) + IntArray::class.java -> + if(flags.contains(SyncFlag.NoCompress)) byteSize += arrayLength * 4 + else + for(value in field.get(owner) as IntArray) + byteSize += + varIntSize( + if(flags.contains(SyncFlag.NonNegative)) value.toLong() + else zigZagEncode(value.toLong()) + ) + LongArray::class.java -> + if(flags.contains(SyncFlag.NoCompress)) byteSize += arrayLength * 8 + else + for(value in field.get(owner) as LongArray) + byteSize += + varIntSize( + if(flags.contains(SyncFlag.NonNegative)) value + else zigZagEncode(value) + ) + FloatArray::class.java -> + if(flags.contains(SyncFlag.NoCompress)) byteSize += arrayLength * 4 + else + for (value in field.get(owner) as LongArray) + byteSize += + varIntSize( + if(flags.contains(SyncFlag.FloatEndianSwap)) bitConvert(swapEndian(floatToInt(field.getFloat(owner)))) + else bitConvert(floatToInt(field.getFloat(owner))) + ) + DoubleArray::class.java -> + if(flags.contains(SyncFlag.NoCompress)) byteSize += arrayLength * 8 + else + for (value in field.get(owner) as LongArray) + byteSize += + varIntSize( + if(flags.contains(SyncFlag.FloatEndianSwap)) swapEndian(doubleToLong(field.getDouble(owner))) + else doubleToLong(field.getDouble(owner)) + ) + } + return Pair(byteSize, bitSize) + } + + override fun serialize(field: Field, flags: Array, owner: Any?, byteBuffer: ByteBuffer, offset: Int, bitFieldOffset: Int): Pair { + val arrayLength = java.lang.reflect.Array.getLength(field.get(owner)) + var localByteOffset = offset + var localBitOffset = bitFieldOffset + if(!flags.contains(knownSize)){ + writeVarInt(byteBuffer, offset, arrayLength.toLong()) + localByteOffset += varIntSize(arrayLength.toLong()) + } + when (field.type) { + BooleanArray::class.java -> + for(value in field.get(owner) as BooleanArray) + writeBit(value, byteBuffer, localBitOffset++) + ByteArray::class.java -> + for(value in field.get(owner) as ByteArray) + byteBuffer.put(localByteOffset++, value) + ShortArray::class.java -> + if(flags.contains(SyncFlag.NoCompress)) + for(value in field.get(owner) as ShortArray){ + byteBuffer.putShort(localByteOffset, value) + localByteOffset += 2 + } + else + for(value in field.get(owner) as ShortArray) { + val rawVal = + if(flags.contains(SyncFlag.NonNegative)) value.toLong() + else zigZagEncode(value.toLong()) + writeVarInt(byteBuffer, localByteOffset, rawVal) + localByteOffset += varIntSize(rawVal) + } + IntArray::class.java -> + if(flags.contains(SyncFlag.NoCompress)) + for(value in field.get(owner) as IntArray){ + byteBuffer.putInt(localByteOffset, value) + localByteOffset += 4 + } + else + for(value in field.get(owner) as IntArray) { + val rawVal = + if(flags.contains(SyncFlag.NonNegative)) value.toLong() + else zigZagEncode(value.toLong()) + writeVarInt(byteBuffer, localByteOffset, rawVal) + localByteOffset += varIntSize(rawVal) + } + LongArray::class.java -> + if(flags.contains(SyncFlag.NoCompress)) + for(value in field.get(owner) as LongArray){ + byteBuffer.putLong(localByteOffset, value) + localByteOffset += 8 + } + else + for(value in field.get(owner) as LongArray) { + val rawVal = + if(flags.contains(SyncFlag.NonNegative)) value + else zigZagEncode(value) + writeVarInt( + byteBuffer, + localByteOffset, + rawVal + ) + localByteOffset += varIntSize(rawVal) + } + FloatArray::class.java -> + if(flags.contains(SyncFlag.NoCompress)) + for(value in field.get(owner) as FloatArray){ + byteBuffer.putFloat(localByteOffset, value) + localByteOffset += 4 + } + else + for (value in field.get(owner) as FloatArray) { + val rawVal = + if(flags.contains(SyncFlag.FloatEndianSwap)) bitConvert(swapEndian(floatToInt(field.getFloat(owner)))) + else bitConvert(floatToInt(field.getFloat(owner))) + writeVarInt(byteBuffer, localByteOffset, rawVal) + localByteOffset += varIntSize(rawVal) + } + DoubleArray::class.java -> + if(flags.contains(SyncFlag.NoCompress)) + for(value in field.get(owner) as DoubleArray){ + byteBuffer.putDouble(localByteOffset, value) + localByteOffset += 8 + } + else + for (value in field.get(owner) as DoubleArray){ + val rawVal = + if(flags.contains(SyncFlag.FloatEndianSwap)) swapEndian(doubleToLong(field.getDouble(owner))) + else doubleToLong(field.getDouble(owner)) + writeVarInt(byteBuffer, localByteOffset, rawVal) + localByteOffset += varIntSize(rawVal) + } + } + return Pair(localByteOffset, localBitOffset) + } + + override fun deserialize(field: Field, flags: Array, owner: Any?, byteBuffer: ByteBuffer, offset: Int, bitFieldOffset: Int): Pair { + var localByteOffset = offset + var localBitOffset = bitFieldOffset + val localLength = java.lang.reflect.Array.getLength(field.get(owner)) + val arrayLength = + if(flags.contains(knownSize)) localLength + else{ + val v = readVarInt(byteBuffer, offset) + localByteOffset += varIntSize(v) + v.toInt() + } + + val target = + if(arrayLength!=localLength) java.lang.reflect.Array.newInstance(field.type.componentType, arrayLength) + else field.get(owner) + + when (field.type) { + BooleanArray::class.java -> { + val booleanTarget = target as BooleanArray + for (index in 0 until arrayLength) + booleanTarget[index] = readBit(byteBuffer, localBitOffset++) + } + ByteArray::class.java -> { + val byteTarget = target as ByteArray + for (index in 0 until arrayLength) + byteTarget[index] = byteBuffer[localByteOffset++] + } + ShortArray::class.java -> { + val shortTarget = target as ShortArray + if (flags.contains(SyncFlag.NoCompress)) + for (index in 0 until arrayLength) { + shortTarget[index] = byteBuffer.getShort(localByteOffset) + localByteOffset += 2 + } + else + for (index in 0 until arrayLength) { + val rawValue = + if(flags.contains(SyncFlag.NonNegative)) readVarInt(byteBuffer, offset) + else zigZagDecode(readVarInt(byteBuffer, offset)) + shortTarget[index] = rawValue.toShort() + localByteOffset += varIntSize(rawValue) + } + } + IntArray::class.java -> { + val intTarget = target as IntArray + if (flags.contains(SyncFlag.NoCompress)) + for (index in 0 until arrayLength) { + intTarget[index] = byteBuffer.getInt(localByteOffset) + localByteOffset += 4 + } + else + for (index in 0 until arrayLength) { + val rawValue = + if(flags.contains(SyncFlag.NonNegative)) readVarInt(byteBuffer, offset) + else zigZagDecode(readVarInt(byteBuffer, offset)) + intTarget[index] = rawValue.toInt() + localByteOffset += varIntSize(rawValue) + } + } + LongArray::class.java -> { + val longTarget = target as LongArray + if (flags.contains(SyncFlag.NoCompress)) + for (index in 0 until arrayLength) { + longTarget[index] = byteBuffer.getLong(localByteOffset) + localByteOffset += 8 + } + else + for (index in 0 until arrayLength) { + val rawValue = + if(flags.contains(SyncFlag.NonNegative)) readVarInt(byteBuffer, offset) + else zigZagDecode(readVarInt(byteBuffer, offset)) + longTarget[index] = rawValue + localByteOffset += varIntSize(rawValue) + } + } + FloatArray::class.java -> { + val floatTarget = target as FloatArray + if (flags.contains(SyncFlag.NoCompress)) + for (index in 0 until arrayLength) { + floatTarget[index] = byteBuffer.getFloat(localByteOffset) + localByteOffset += 4 + } + else + for (index in 0 until arrayLength) { + val readVal = readVarInt(byteBuffer, offset) + val rawValue = + if(flags.contains(SyncFlag.FloatEndianSwap)) intToFloat(swapEndian(readVal.toInt())) + else intToFloat(readVal.toInt()) + floatTarget[index] = rawValue + localBitOffset += varIntSize(readVal) + } + } + DoubleArray::class.java -> { + val doubleTarget = target as DoubleArray + if (flags.contains(SyncFlag.NoCompress)) + for (index in 0 until arrayLength) { + doubleTarget[index] = byteBuffer.getDouble(localByteOffset) + localByteOffset += 8 + } + else + for (index in 0 until arrayLength) { + val readVal = readVarInt(byteBuffer, offset) + val rawValue = + if(flags.contains(SyncFlag.FloatEndianSwap)) longToDouble(swapEndian(readVal)) + else longToDouble(readVal) + doubleTarget[index] = rawValue + localBitOffset += varIntSize(readVal) + } + } + } + if(arrayLength!=localLength) field.set(owner, target) + return Pair(localByteOffset, localBitOffset) + } +} \ No newline at end of file diff --git a/src/net/tofvesson/networking/PrimitiveSerializers.kt b/src/net/tofvesson/networking/PrimitiveSerializers.kt new file mode 100644 index 0000000..76eabdd --- /dev/null +++ b/src/net/tofvesson/networking/PrimitiveSerializers.kt @@ -0,0 +1,219 @@ +package net.tofvesson.networking + +import java.lang.reflect.Field +import java.nio.ByteBuffer + +class PrimitiveSerializer private constructor() : Serializer(arrayOf( + Boolean::class.java, + Byte::class.java, + Short::class.java, + Int::class.java, + Long::class.java, + Float::class.java, + Double::class.java +)) { + companion object { val singleton = PrimitiveSerializer() } + + override fun computeSize( + field: Field, + flags: Array, + owner: Any? + ): Pair = + when(field.type){ + Boolean::class.java -> Pair(0, 1) + Byte::class.java -> Pair(1, 0) + Short::class.java -> + if(flags.contains(SyncFlag.NoCompress)) Pair(2, 0) + else Pair(varIntSize( + if(flags.contains(SyncFlag.NonNegative)) field.getShort(owner).toLong() + else zigZagEncode(field.getShort(owner).toLong()) + ), 0) + Int::class.java -> + if(flags.contains(SyncFlag.NoCompress)) Pair(4, 0) + else Pair(varIntSize( + if(flags.contains(SyncFlag.NonNegative)) field.getInt(owner).toLong() + else zigZagEncode(field.getInt(owner).toLong()) + ), 0) + Long::class.java -> + if(flags.contains(SyncFlag.NoCompress)) Pair(8, 0) + else Pair(varIntSize( + if(flags.contains(SyncFlag.NonNegative)) field.getLong(owner) + else zigZagEncode(field.getLong(owner)) + ), 0) + Float::class.java -> + if(flags.contains(SyncFlag.NoCompress)) Pair(4, 0) + else Pair(varIntSize( + if(flags.contains(SyncFlag.FloatEndianSwap)) bitConvert(swapEndian(floatToInt(field.getFloat(owner)))) + else bitConvert(floatToInt(field.getFloat(owner))) + ), 0) + Double::class.java -> + if(flags.contains(SyncFlag.NoCompress)) Pair(8, 0) + else Pair(varIntSize( + if(flags.contains(SyncFlag.FloatEndianSwap)) swapEndian(doubleToLong(field.getDouble(owner))) + else doubleToLong(field.getDouble(owner)) + ), 0) + else -> Pair(0, 0) + } + + override fun serialize( + field: Field, + flags: Array, + owner: Any?, + byteBuffer: ByteBuffer, + offset: Int, + bitFieldOffset: Int + ): Pair = + when(field.type){ + Boolean::class.java -> { + writeBit(field.getBoolean(owner), byteBuffer, bitFieldOffset) + Pair(offset, bitFieldOffset+1) + } + Byte::class.java -> { + byteBuffer.put(offset, field.getByte(owner)) + Pair(offset+1, bitFieldOffset) + } + Short::class.java -> + if(flags.contains(SyncFlag.NoCompress)){ + byteBuffer.putShort(offset, field.getShort(owner)) + Pair(offset+2, bitFieldOffset) + } + else { + val rawValue = + if(flags.contains(SyncFlag.NonNegative)) field.getShort(owner).toLong() + else zigZagEncode(field.getShort(owner).toLong()) + writeVarInt(byteBuffer, offset, rawValue) + Pair(offset+varIntSize(rawValue), bitFieldOffset) + } + Int::class.java -> + if(flags.contains(SyncFlag.NoCompress)){ + byteBuffer.putInt(offset, field.getInt(owner)) + Pair(offset+4, bitFieldOffset) + } + else { + val rawValue = + if(flags.contains(SyncFlag.NonNegative)) field.getInt(owner).toLong() + else zigZagEncode(field.getInt(owner).toLong()) + writeVarInt(byteBuffer, offset, rawValue) + Pair(offset+varIntSize(rawValue), bitFieldOffset) + } + Long::class.java -> + if(flags.contains(SyncFlag.NoCompress)){ + byteBuffer.putLong(offset, field.getLong(owner)) + Pair(offset+8, bitFieldOffset) + } + else { + val rawValue = + if(flags.contains(SyncFlag.NonNegative)) field.getLong(owner) + else zigZagEncode(field.getLong(owner)) + writeVarInt(byteBuffer, offset, rawValue) + Pair(offset+varIntSize(rawValue), bitFieldOffset) + } + Float::class.java -> + if(flags.contains(SyncFlag.NoCompress)){ + byteBuffer.putFloat(offset, field.getFloat(owner)) + Pair(offset+4, bitFieldOffset) + } + else{ + val rawValue = + if(flags.contains(SyncFlag.FloatEndianSwap)) bitConvert(swapEndian(floatToInt(field.getFloat(owner)))) + else bitConvert(floatToInt(field.getFloat(owner))) + writeVarInt(byteBuffer, offset, rawValue) + Pair(offset+varIntSize(rawValue), bitFieldOffset) + } + Double::class.java -> + if(flags.contains(SyncFlag.NoCompress)){ + byteBuffer.putDouble(offset, field.getDouble(owner)) + Pair(offset+8, bitFieldOffset) + } + else{ + val rawValue = + if(flags.contains(SyncFlag.FloatEndianSwap)) swapEndian(doubleToLong(field.getDouble(owner))) + else doubleToLong(field.getDouble(owner)) + writeVarInt(byteBuffer, offset, rawValue) + Pair(offset+varIntSize(rawValue), bitFieldOffset) + } + else -> Pair(offset, bitFieldOffset) + } + + override fun deserialize( + field: Field, + flags: Array, + owner: Any?, + byteBuffer: ByteBuffer, + offset: Int, + bitFieldOffset: Int + ): Pair = + when(field.type){ + Boolean::class.java -> { + field.setBoolean(owner, readBit(byteBuffer, bitFieldOffset)) + Pair(offset, bitFieldOffset+1) + } + Byte::class.java -> { + field.setByte(owner, byteBuffer.get(offset)) + Pair(offset+1, bitFieldOffset) + } + Short::class.java -> + if(flags.contains(SyncFlag.NoCompress)){ + field.setShort(owner, byteBuffer.getShort(offset)) + Pair(offset+2, bitFieldOffset) + } + else { + val rawValue = + if(flags.contains(SyncFlag.NonNegative)) readVarInt(byteBuffer, offset) + else zigZagDecode(readVarInt(byteBuffer, offset)) + field.setShort(owner, rawValue.toShort()) + Pair(offset+varIntSize(rawValue), bitFieldOffset) + } + Int::class.java -> + if(flags.contains(SyncFlag.NoCompress)){ + field.setInt(owner, byteBuffer.getInt(offset)) + Pair(offset+4, bitFieldOffset) + } + else { + val rawValue = + if(flags.contains(SyncFlag.NonNegative)) readVarInt(byteBuffer, offset) + else zigZagDecode(readVarInt(byteBuffer, offset)) + field.setInt(owner, rawValue.toInt()) + Pair(offset+varIntSize(rawValue), bitFieldOffset) + } + Long::class.java -> + if(flags.contains(SyncFlag.NoCompress)){ + field.setLong(owner, byteBuffer.getLong(offset)) + Pair(offset+8, bitFieldOffset) + } + else { + val rawValue = + if(flags.contains(SyncFlag.NonNegative)) readVarInt(byteBuffer, offset) + else zigZagDecode(readVarInt(byteBuffer, offset)) + field.setLong(owner, rawValue) + Pair(offset+varIntSize(rawValue), bitFieldOffset) + } + Float::class.java -> + if(flags.contains(SyncFlag.NoCompress)){ + field.setFloat(owner, byteBuffer.getFloat(offset)) + Pair(offset+4, bitFieldOffset) + } + else{ + val readVal = readVarInt(byteBuffer, offset) + val rawValue = + if(flags.contains(SyncFlag.FloatEndianSwap)) intToFloat(swapEndian(readVal.toInt())) + else intToFloat(readVal.toInt()) + field.setFloat(owner, rawValue) + Pair(offset+varIntSize(readVal), bitFieldOffset) + } + Double::class.java -> + if(flags.contains(SyncFlag.NoCompress)){ + field.setDouble(owner, byteBuffer.getDouble(offset)) + Pair(offset+8, bitFieldOffset) + } + else{ + val readVal = readVarInt(byteBuffer, offset) + val rawValue = + if(flags.contains(SyncFlag.FloatEndianSwap)) longToDouble(swapEndian(readVal)) + else longToDouble(readVal) + field.setDouble(owner, rawValue) + Pair(offset+varIntSize(readVal), bitFieldOffset) + } + else -> Pair(offset, bitFieldOffset) + } +} \ No newline at end of file diff --git a/src/net/tofvesson/networking/Serializer.kt b/src/net/tofvesson/networking/Serializer.kt new file mode 100644 index 0000000..9b1acd7 --- /dev/null +++ b/src/net/tofvesson/networking/Serializer.kt @@ -0,0 +1,49 @@ +package net.tofvesson.networking + +import java.lang.reflect.Field +import java.nio.ByteBuffer +import java.util.* + +abstract class Serializer(registeredTypes: Array>) { + private val registeredTypes: Array> = Arrays.copyOf(registeredTypes, registeredTypes.size) + + /** + * Compute the size in bits that a field will occupy + * @return Size in the byteField (first) and bitField (second) that need to be allocated + */ + abstract fun computeSize( + field: Field, + flags: Array, + owner: Any? + ): Pair + + /** + * Serialize a field to the buffer + * @return The new offset (first) and bitFieldOffset (second) + */ + abstract fun serialize( + field: Field, + flags: Array, + owner: Any?, + byteBuffer: ByteBuffer, + offset: Int, + bitFieldOffset: Int + ): Pair + + /** + * Deserialize a field from the buffer + * @return The new offset (first) and bitFieldOffset (second) + */ + abstract fun deserialize( + field: Field, + flags: Array, + owner: Any?, + byteBuffer: ByteBuffer, + offset: Int, + bitFieldOffset: Int + ): Pair + + fun getRegisteredTypes(): Array> = Arrays.copyOf(registeredTypes, registeredTypes.size) + fun canSerialize(field: Field): Boolean = registeredTypes.contains(field.type) + fun canSerialize(type: Class<*>): Boolean = registeredTypes.contains(type) +} \ No newline at end of file diff --git a/src/net/tofvesson/networking/SyncHandler.kt b/src/net/tofvesson/networking/SyncHandler.kt index 2bd6d55..c96677e 100644 --- a/src/net/tofvesson/networking/SyncHandler.kt +++ b/src/net/tofvesson/networking/SyncHandler.kt @@ -2,7 +2,6 @@ package net.tofvesson.networking import java.lang.reflect.Field import java.nio.ByteBuffer -import kotlin.experimental.or import java.security.NoSuchAlgorithmException import java.security.MessageDigest @@ -10,30 +9,46 @@ import java.security.MessageDigest /** * @param permissiveMismatchCheck This should essentially never be set to true aside from some *very* odd edge cases */ -class SyncHandler(private val permissiveMismatchCheck: Boolean = false) { - private val toSync: ArrayList> = ArrayList() +class SyncHandler(defaultSerializers: Boolean = true, private val permissiveMismatchCheck: Boolean = false) { + private val toSync: ArrayList = ArrayList() + private val serializers: ArrayList = ArrayList() + + init { + if(defaultSerializers) { + serializers.add(PrimitiveSerializer.singleton) + serializers.add(PrimitiveArraySerializer.singleton) + } + } fun registerSyncObject(value: Any){ - if(!toSync.contains(value)) toSync.add(Pair(value, getBooleanFieldCount(value::class.java))) + if(!toSync.contains(value)) toSync.add(value) } fun unregisterSyncObject(value: Any){ - for(i in toSync.indices.reversed()) - if(toSync[i].first == value) - toSync.removeAt(i) + toSync.remove(value) + } + + fun withSerializer(serializer: Serializer): SyncHandler { + if(!serializers.contains(serializer)) serializers.add(serializer) + return this } fun serialize(): ByteArray{ - val headerBits = computeBitHeaderCount() - val headerSize = (headerBits shr 3) + (if((headerBits and 7) != 0) 1 else 0) + var headerSize = 0 + var totalSize = 0 + for(entry in toSync){ + val result = if(entry is Class<*>) computeClassSize(entry) else computeObjectSize(entry) + totalSize += result.first + headerSize += result.second + } + var headerIndex = 0 - var dataIndex = headerSize - var totalSize = headerSize - for((entry, _) in toSync) - totalSize += if(entry is Class<*>) computeClassSize(entry) else computeObjectSize(entry) + var dataIndex = (headerSize shr 3) + (if(headerSize and 7 != 0) 1 else 0) + + totalSize += dataIndex val buffer = ByteArray(totalSize) - for((entry, _) in toSync){ + for(entry in toSync){ val result = if(entry is Class<*>) readClass(entry, buffer, dataIndex, headerIndex) else readObject(entry, buffer, dataIndex, headerIndex) @@ -44,11 +59,12 @@ class SyncHandler(private val permissiveMismatchCheck: Boolean = false) { } fun deserialize(syncData: ByteArray){ - val headerBits = computeBitHeaderCount() - val headerSize = (headerBits shr 3) + (if((headerBits and 7) != 0) 1 else 0) + var headerSize = 0 + for(entry in toSync) + headerSize += (if(entry is Class<*>) computeClassSize(entry) else computeObjectSize(entry)).second var headerIndex = 0 - var dataIndex = headerSize - for((entry, _) in toSync){ + var dataIndex = (headerSize shr 3) + (if(headerSize and 7 != 0) 1 else 0) + for(entry in toSync){ val result = if(entry is Class<*>) writeClass(entry, syncData, dataIndex, headerIndex) else writeObject(entry, syncData, dataIndex, headerIndex) @@ -59,16 +75,15 @@ class SyncHandler(private val permissiveMismatchCheck: Boolean = false) { fun generateMismatchCheck(): ByteArray { val builder = StringBuilder() - for((entry, _) in toSync) + for(entry in toSync) for(field in (entry as? Class<*> ?: entry::class.java).declaredFields){ if((entry is Class<*> && field.modifiers and 8 == 0) || (entry !is Class<*> && field.modifiers and 8 != 0)) continue val annotation = field.getAnnotation(SyncedVar::class.java) - if(annotation!=null) - builder - .append(if(permissiveMismatchCheck) field.type.name else field.toGenericString()) - .append(if(annotation.floatEndianSwap) 1 else 0) - .append(if(annotation.noCompress) 1 else 0) - .append(if(annotation.nonNegative) 1 else 0) + if(annotation!=null) { + builder.append('{').append(if (permissiveMismatchCheck) field.type.name else field.toGenericString()) + for(flag in annotation.value) builder.append(flag) + builder.append('}') + } } return try { @@ -87,11 +102,29 @@ class SyncHandler(private val permissiveMismatchCheck: Boolean = false) { return true } - private fun computeBitHeaderCount(): Int { - var bitCount = 0 - for((_, bits) in toSync) - bitCount += bits - return bitCount + + private fun computeObjectSize(value: Any) = computeTypeSize(value.javaClass, value) + private fun computeClassSize(value: Class<*>) = computeTypeSize(value, null) + private fun computeTypeSize(type: Class<*>, value: Any?): Pair { + var byteSize = 0 + var bitSize = 0 + for(field in type.declaredFields){ + if((value == null && field.modifiers and 8 == 0) || (value != null && field.modifiers and 8 != 0)) continue + val annotation = field.getAnnotation(SyncedVar::class.java) + field.trySetAccessible() + if(annotation!=null){ + val result = getCompatibleSerializer(field.type).computeSize(field, SyncFlag.parse(annotation.value), value) + byteSize += result.first + bitSize += result.second + } + } + return Pair(byteSize, bitSize) + } + private fun getCompatibleSerializer(type: Class<*>): Serializer { + for(serializer in serializers) + if(serializer.canSerialize(type)) + return serializer + throw UnsupportedTypeException("Cannot find a compatible serializer for $type") } private fun readObject(value: Any, buffer: ByteArray, offset: Int, bitOffset: Int) = readType(value.javaClass, value, buffer, offset, bitOffset) @@ -104,8 +137,9 @@ class SyncHandler(private val permissiveMismatchCheck: Boolean = false) { if((value == null && field.modifiers and 8 == 0) || (value != null && field.modifiers and 8 != 0)) continue val annotation = field.getAnnotation(SyncedVar::class.java) if(annotation != null){ - if(field.type!=Boolean::class.java) localOffset += readValue(field, annotation, value, byteBuffer, localOffset) - else writeBit(field.getBoolean(value), buffer, localBitOffset++) + val result = getCompatibleSerializer(field.type).serialize(field, SyncFlag.parse(annotation.value), value, byteBuffer, localOffset, localBitOffset) + localOffset = result.first + localBitOffset = result.second } } return Pair(localOffset, localBitOffset) @@ -121,212 +155,11 @@ class SyncHandler(private val permissiveMismatchCheck: Boolean = false) { if((value == null && field.modifiers and 8 == 0) || (value != null && field.modifiers and 8 != 0)) continue val annotation = field.getAnnotation(SyncedVar::class.java) if(annotation != null){ - if(field.type!=Boolean::class.java) localOffset += writeValue(field, annotation, value, byteBuffer, localOffset) - else field.setBoolean(value, readBit(buffer, localBitOffset++)) + val result = getCompatibleSerializer(field.type).deserialize(field, SyncFlag.parse(annotation.value), value, byteBuffer, localOffset, localBitOffset) + localOffset = result.first + localBitOffset = result.second } } return Pair(localOffset, localBitOffset) } - - companion object { - private fun getBooleanFieldCount(type: Class<*>): Int { - var count = 0 - for(field in type.declaredFields) - if(field.getAnnotation(SyncedVar::class.java)!=null && field.type==Boolean::class.java) - ++count - return count - } - private fun writeBit(bit: Boolean, buffer: ByteArray, index: Int){ - buffer[index shr 3] = buffer[index shr 3].or(((if(bit) 1 else 0) shl (index and 7)).toByte()) - } - private fun readBit(buffer: ByteArray, index: Int): Boolean = buffer[index shr 3].toInt() and (1 shl (index and 7)) != 0 - - private fun computeObjectSize(value: Any) = computeTypeSize(value.javaClass, value) - private fun computeClassSize(value: Class<*>) = computeTypeSize(value, null) - private fun computeTypeSize(type: Class<*>, value: Any?): Int{ - var byteSize = 0 - for(field in type.declaredFields){ - if((value == null && field.modifiers and 8 == 0) || (value != null && field.modifiers and 8 != 0)) continue - val annotation = field.getAnnotation(SyncedVar::class.java) - field.trySetAccessible() - if(annotation!=null) byteSize += computeDataSize(field, annotation, value) - } - return byteSize - } - private fun computeDataSize(field: Field, annotation: SyncedVar, value: Any?): Int = - when(field.type){ - Byte::class.java -> 1 - Short::class.java -> - if(annotation.noCompress) 2 - else varIntSize( - if(annotation.nonNegative) field.getShort(value).toLong() - else zigZagEncode(field.getShort(value).toLong()) - ) - Int::class.java -> - if(annotation.noCompress) 4 - else varIntSize( - if(annotation.nonNegative) field.getInt(value).toLong() - else zigZagEncode(field.getInt(value).toLong()) - ) - Long::class.java -> - if(annotation.noCompress) 8 - else varIntSize( - if(annotation.nonNegative) field.getLong(value) - else zigZagEncode(field.getLong(value)) - ) - Float::class.java -> - if(annotation.noCompress) 4 - else varIntSize( - if(annotation.floatEndianSwap) bitConvert(swapEndian(floatToInt(field.getFloat(value)))) - else bitConvert(floatToInt(field.getFloat(value))) - ) - Double::class.java -> - if(annotation.noCompress) 8 - else varIntSize( - if(annotation.floatEndianSwap) swapEndian(doubleToLong(field.getDouble(value))) - else doubleToLong(field.getDouble(value)) - ) - else -> 0 - } - - private fun readValue(field: Field, annotation: SyncedVar, value: Any?, buffer: ByteBuffer, offset: Int): Int = - when(field.type){ - Byte::class.java ->{ - buffer.put(offset, field.getByte(value)) - 1 - } - Short::class.java -> - if(annotation.noCompress){ - buffer.putShort(offset, field.getShort(value)) - 2 - } - else { - val rawValue = - if(annotation.nonNegative) field.getShort(value).toLong() - else zigZagEncode(field.getShort(value).toLong()) - writeVarInt(buffer, offset, rawValue) - varIntSize(rawValue) - } - Int::class.java -> - if(annotation.noCompress){ - buffer.putInt(offset, field.getInt(value)) - 4 - } - else { - val rawValue = - if(annotation.nonNegative) field.getInt(value).toLong() - else zigZagEncode(field.getInt(value).toLong()) - writeVarInt(buffer, offset, rawValue) - varIntSize(rawValue) - } - Long::class.java -> - if(annotation.noCompress){ - buffer.putLong(offset, field.getLong(value)) - 8 - } - else { - val rawValue = - if(annotation.nonNegative) field.getLong(value) - else zigZagEncode(field.getLong(value)) - writeVarInt(buffer, offset, rawValue) - varIntSize(rawValue) - } - Float::class.java -> - if(annotation.noCompress){ - buffer.putFloat(offset, field.getFloat(value)) - 4 - } - else{ - val rawValue = - if(annotation.floatEndianSwap) bitConvert(swapEndian(floatToInt(field.getFloat(value)))) - else bitConvert(floatToInt(field.getFloat(value))) - writeVarInt(buffer, offset, rawValue) - varIntSize(rawValue) - } - Double::class.java -> - if(annotation.noCompress){ - buffer.putDouble(offset, field.getDouble(value)) - 8 - } - else{ - val rawValue = - if(annotation.floatEndianSwap) swapEndian(doubleToLong(field.getDouble(value))) - else doubleToLong(field.getDouble(value)) - writeVarInt(buffer, offset, rawValue) - varIntSize(rawValue) - } - else -> 0 - } - - private fun writeValue(field: Field, annotation: SyncedVar, value: Any?, buffer: ByteBuffer, offset: Int): Int = - when(field.type){ - Byte::class.java ->{ - field.setByte(value, buffer.get(offset)) - 1 - } - Short::class.java -> - if(annotation.noCompress){ - field.setShort(value, buffer.getShort(offset)) - 2 - } - else { - val rawValue = - if(annotation.nonNegative) readVarInt(buffer, offset) - else zigZagDecode(readVarInt(buffer, offset)) - field.setShort(value, rawValue.toShort()) - varIntSize(rawValue) - } - Int::class.java -> - if(annotation.noCompress){ - field.setInt(value, buffer.getInt(offset)) - 4 - } - else { - val rawValue = - if(annotation.nonNegative) readVarInt(buffer, offset) - else zigZagDecode(readVarInt(buffer, offset)) - field.setInt(value, rawValue.toInt()) - varIntSize(rawValue) - } - Long::class.java -> - if(annotation.noCompress){ - field.setLong(value, buffer.getLong(offset)) - 8 - } - else { - val rawValue = - if(annotation.nonNegative) readVarInt(buffer, offset) - else zigZagDecode(readVarInt(buffer, offset)) - field.setLong(value, rawValue) - varIntSize(rawValue) - } - Float::class.java -> - if(annotation.noCompress){ - field.setFloat(value, buffer.getFloat(offset)) - 4 - } - else{ - val readVal = readVarInt(buffer, offset) - val rawValue = - if(annotation.floatEndianSwap) intToFloat(swapEndian(readVal.toInt())) - else intToFloat(readVal.toInt()) - field.setFloat(value, rawValue) - varIntSize(readVal) - } - Double::class.java -> - if(annotation.noCompress){ - field.setDouble(value, buffer.getDouble(offset)) - 8 - } - else{ - val readVal = readVarInt(buffer, offset) - val rawValue = - if(annotation.floatEndianSwap) longToDouble(swapEndian(readVal)) - else longToDouble(readVal) - field.setDouble(value, rawValue) - varIntSize(readVal) - } - else -> 0 - } - } } \ No newline at end of file diff --git a/src/net/tofvesson/networking/SyncedVar.kt b/src/net/tofvesson/networking/SyncedVar.kt index 4278e19..2647f91 100644 --- a/src/net/tofvesson/networking/SyncedVar.kt +++ b/src/net/tofvesson/networking/SyncedVar.kt @@ -1,5 +1,7 @@ package net.tofvesson.networking +import net.tofvesson.reflect.* + /** * An annotation denoting that a field should be automatically serialized to bytes. * @param noCompress specifies whether or not the SyncedVar value should be losslessly compressed during serialization @@ -10,4 +12,70 @@ package net.tofvesson.networking * Swapping endianness may improve compression rates. This parameter is ignored if noCompress is set to true. */ @Target(allowedTargets = [(AnnotationTarget.FIELD)]) -annotation class SyncedVar(val noCompress: Boolean = false, val nonNegative: Boolean = false, val floatEndianSwap: Boolean = true) \ No newline at end of file +annotation class SyncedVar(vararg val value: String = []) + +enum class SyncFlag { + NoCompress, + NonNegative, + FloatEndianSwap; + + companion object { + fun getByName(name: String): SyncFlag? { + for(flag in SyncFlag.values()) + if(flag.name == name) + return flag + return null + } + + fun parse(flagSet: Array): Array = Array(flagSet.size) { getByName(flagSet[it])!! } + + fun createFlag(name: String): SyncFlag { + // Do duplication check + if(getByName(name)!=null) throw IllegalArgumentException("Flag \"$name\" is already registered!") + + // Get unsafe + val unsafe = getUnsafe() + + // Create new enum + val newFlag: SyncFlag = unsafe.allocateInstance(SyncFlag::class.java) as SyncFlag + + // Set valid ordinal + val ordinalField = Enum::class.java.getDeclaredField("ordinal") + ordinalField.forceAccessible = true + ordinalField.setInt(newFlag, getUnallocatedOrdinal()) + + // Set valid name + val nameField = Enum::class.java.getDeclaredField("name") + nameField.forceAccessible = true + nameField.set(newFlag, name) + + // Add the new flag to the array of values + val oldFlags = values() + val flagArray = Array(oldFlags.size+1){ + if(it < oldFlags.size) oldFlags[it] + else newFlag + } + val flags = SyncFlag::class.java.getDeclaredField("\$VALUES") + flags.setStaticFinalValue(flagArray) + + return newFlag + } + + private fun getUnallocatedOrdinal(): Int { + var ordinal = 0 + val flags = SyncFlag.values().toMutableList() + val remove = ArrayList() + while(flags.count()!=0) { + for (flag in flags) + if (flag.ordinal == ordinal) { + remove.add(flag) + ++ordinal + if (ordinal == 0) throw IndexOutOfBoundsException("Full 32-bit range or enum ordinals occupied!") + } + flags.removeAll(remove) + remove.clear() + } + return ordinal + } + } +} \ No newline at end of file diff --git a/src/net/tofvesson/networking/UnsupportedTypeException.kt b/src/net/tofvesson/networking/UnsupportedTypeException.kt new file mode 100644 index 0000000..a85a256 --- /dev/null +++ b/src/net/tofvesson/networking/UnsupportedTypeException.kt @@ -0,0 +1,9 @@ +package net.tofvesson.networking + +class UnsupportedTypeException: RuntimeException { + constructor() : super() + constructor(message: String?) : super(message) + constructor(message: String?, cause: Throwable?) : super(message, cause) + constructor(cause: Throwable?) : super(cause) + constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(message, cause, enableSuppression, writableStackTrace) +} \ No newline at end of file diff --git a/src/net/tofvesson/reflect/Accessible.kt b/src/net/tofvesson/reflect/Accessible.kt new file mode 100644 index 0000000..0756f87 --- /dev/null +++ b/src/net/tofvesson/reflect/Accessible.kt @@ -0,0 +1,34 @@ +package net.tofvesson.reflect + +import sun.misc.Unsafe +import java.lang.reflect.AccessibleObject +import java.lang.reflect.Field + +var AccessibleObject.forceAccessible: Boolean + get() = this.isAccessible + set(value) { + val fieldOverride = AccessibleObject::class.java.getDeclaredField("override") + val unsafe = getUnsafe() + val overrideOffset = unsafe.objectFieldOffset(fieldOverride) + unsafe.getAndSetObject(this, overrideOffset, value) + } + +fun getUnsafe(): Unsafe{ + val theUnsafe = Unsafe::class.java.getDeclaredField("theUnsafe") + theUnsafe.trySetAccessible() + return theUnsafe.get(null) as Unsafe +} + +fun Field.setStaticFinalValue(value: Any?){ + val factory = Class.forName("jdk.internal.reflect.UnsafeFieldAccessorFactory").getDeclaredMethod("newFieldAccessor", Field::class.java, Boolean::class.java) + val isReadonly = Class.forName("jdk.internal.reflect.UnsafeQualifiedStaticFieldAccessorImpl").getDeclaredField("isReadOnly") + isReadonly.forceAccessible = true + factory.forceAccessible = true + val overrideAccessor = Field::class.java.getDeclaredField("overrideFieldAccessor") + overrideAccessor.forceAccessible = true + val accessor = factory.invoke(null, this, true) + isReadonly.setBoolean(accessor, false) + overrideAccessor.set(this, accessor) + this.forceAccessible = true + this.set(null, value) +} \ No newline at end of file