From 87ec95544dad9797c2299c34563186e34d0badc4 Mon Sep 17 00:00:00 2001 From: Gabriel Tofvesson Date: Mon, 23 Jul 2018 23:38:16 +0200 Subject: [PATCH] Initial commit --- SyncedVar.iml | 12 + src/Main.java | 34 +++ src/net/tofvesson/networking/Arithmetic.kt | 151 ++++++++++ src/net/tofvesson/networking/SyncHandler.kt | 312 ++++++++++++++++++++ src/net/tofvesson/networking/SyncedVar.kt | 13 + 5 files changed, 522 insertions(+) create mode 100644 SyncedVar.iml create mode 100644 src/Main.java create mode 100644 src/net/tofvesson/networking/Arithmetic.kt create mode 100644 src/net/tofvesson/networking/SyncHandler.kt create mode 100644 src/net/tofvesson/networking/SyncedVar.kt diff --git a/SyncedVar.iml b/SyncedVar.iml new file mode 100644 index 0000000..245d342 --- /dev/null +++ b/SyncedVar.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Main.java b/src/Main.java new file mode 100644 index 0000000..7b7a8da --- /dev/null +++ b/src/Main.java @@ -0,0 +1,34 @@ +import net.tofvesson.networking.SyncHandler; +import net.tofvesson.networking.SyncedVar; + +public class Main { + @SyncedVar(nonNegative = true) + public int syncTest = 5; + + @SyncedVar + public static long staticTest = 90; + + @SyncedVar + public static float value = 1337f; + + public static void main(String[] args){ + Main testObject = new Main(); + SyncHandler sync = new SyncHandler(); + sync.registerSyncObject(testObject); + //sync.registerSyncObject(Main.class); + + byte[] ser = sync.serialize(); + + System.out.println("Created and serialized snapshot of field values:\n"+testObject.syncTest+"\n"+staticTest+"\n"+value+"\n"); + + testObject.syncTest = 20; + staticTest = 32; + value = 9.0f; + + System.out.println("Set a new state of test values:\n"+testObject.syncTest+"\n"+staticTest+"\n"+value+"\n"); + + sync.deserialize(ser); + + System.out.println("Deserialized snapshot values:\n"+testObject.syncTest+"\n"+staticTest+"\n"+value+"\n"); + } +} diff --git a/src/net/tofvesson/networking/Arithmetic.kt b/src/net/tofvesson/networking/Arithmetic.kt new file mode 100644 index 0000000..c88ecca --- /dev/null +++ b/src/net/tofvesson/networking/Arithmetic.kt @@ -0,0 +1,151 @@ +package net.tofvesson.networking + +import java.nio.ByteBuffer + +fun varIntSize(value: Long): Int = + when { + value <= 240 -> 1 + value <= 2287 -> 2 + value <= 67823 -> 3 + value <= 16777215 -> 4 + value <= 4294967295 -> 5 + value <= 1099511627775 -> 6 + value <= 281474976710655 -> 7 + value <= 72057594037927935 -> 8 + else -> 9 + } + +fun writeVarInt(buffer: ByteArray, offset: Int, value: Long){ + when(value) { + in 0..240 -> buffer[offset] = value.toByte() + in 241..2287 -> { + buffer[offset] = (((value - 240) shr 8) + 241).toByte() + buffer[offset+1] = (value - 240).toByte() + } + in 2288..67823 -> { + buffer[offset] = 249.toByte() + buffer[offset+1] = ((value - 2288) shr 8).toByte() + buffer[offset+2] = (value - 2288).toByte() + } + else -> { + var header = 255 + var match = 0x00FF_FFFF_FFFF_FFFFL + while (value in 67824..match) { + --header + match = match shr 8 + } + buffer[offset] = header.toByte() + val max = header - 247 + for (i in 0..(max-1)) buffer[offset+i+1] = (value shr (i shl 3)).toByte() + } + } +} + +fun writeVarInt(buffer: ByteBuffer, offset: Int, value: Long){ + when (value) { + in 0..240 -> buffer.put(offset, value.toByte()) + in 0..2287 -> { + buffer.put(offset, (((value - 240) shr 8) + 241).toByte()) + buffer.put(offset+1, (value - 240).toByte()) + } + in 0..67823 -> { + buffer.put(offset, 249.toByte()) + buffer.put(offset+1, ((value - 2288) shr 8).toByte()) + buffer.put(offset+2, (value - 2288).toByte()) + } + else -> { + var header = 255 + var match = 0x00FF_FFFF_FFFF_FFFFL + while (value in 67824..match) { + --header + match = match shr 8 + } + buffer.put(offset, header.toByte()) + val max = header - 247 + for (i in 0..(max-1)) buffer.put(offset+i+1, (value shr (i shl 3)).toByte()) + } + } +} + +fun writeInt(buffer: ByteArray, offset: Int, value: Long, bytes: Int){ + for(i in 0..(bytes-1)) + buffer[offset+i] = (value shr (8*i)).toByte() +} + +fun writeInt(buffer: ByteBuffer, offset: Int, value: Long, bytes: Int){ + for(i in 0..(bytes-1)) + buffer.put(offset+i, (value shr (8*i)).toByte()) +} + +fun readVarInt(buffer: ByteArray, offset: Int): Long { + var off = offset + val header: Long = buffer[off++].toLong() and 0xFF + if (header <= 240L) return header + if (header <= 248L) return 240L + ((header - 241L).shl(8)) + (buffer[off].toLong() and 0xFF) + if (header == 249L) return 2288 + ((buffer[off++].toLong() and 0xFF).shl(8)) + (buffer[off].toLong() and 0xFF) + var res = (buffer[off++].toLong() and 0xFF).or(((buffer[off++].toLong() and 0xFF).shl(8)).or((buffer[off++].toLong() and 0xFF).shl(16))) + var cmp = 2 + val hdr = header - 247 + while (hdr > ++cmp) res = res.or((buffer[off++].toLong() and 0xFF).shl(cmp.shl(3))) + return res +} + +fun readVarInt(buffer: ByteBuffer, offset: Int): Long { + var off = offset + val header: Long = (buffer[off++].toLong() and 0xFF) + if (header <= 240L) return header + if (header <= 248L) return 240L + ((header - 241L).shl(8)) + (buffer[off].toLong() and 0xFF) + if (header == 249L) return 2288 + ((buffer[off++].toLong() and 0xFF).shl(8)) + (buffer[off].toLong() and 0xFF) + var res = (buffer[off++].toLong() and 0xFF).or(((buffer[off++].toLong() and 0xFF).shl(8)).or((buffer[off++].toLong() and 0xFF).shl(16))) + var cmp = 2 + val hdr = header - 247 + while (hdr > ++cmp) res = res.or((buffer[off++].toLong() and 0xFF).shl(cmp.shl(3))) + return res +} + +private val converter = ByteBuffer.allocateDirect(8) + +fun floatToInt(value: Float): Int = + synchronized(converter){ + converter.putFloat(0, value) + return@synchronized converter.getInt(0) + } + +fun doubleToLong(value: Double): Long = + synchronized(converter){ + converter.putDouble(0, value) + return@synchronized converter.getLong(0) + } + +fun intToFloat(value: Int): Float = + synchronized(converter){ + converter.putInt(0, value) + return@synchronized converter.getFloat(0) + } + +fun longToDouble(value: Long): Double = + synchronized(converter){ + converter.putLong(0, value) + return@synchronized converter.getDouble(0) + } + +fun swapEndian(value: Short) = ((value.toInt() shl 8) or ((value.toInt() shr 8) and 255)).toShort() +fun swapEndian(value: Int) = + ((value shr 24) and 0xFF) or + ((value shr 8) and 0xFF00) or + ((value shl 24) and -16777216) or + ((value shl 8) and 0xFF0000) +fun swapEndian(value: Long) = + ((value shr 56) and 0xFFL) or + ((value shr 40) and 0xFF00L) or + ((value shr 24) and 0xFF0000L) or + ((value shr 8) and 0xFF000000L) or + ((value shl 56) and -72057594037927936L) or + ((value shl 40) and 0xFF000000000000L) or + ((value shl 24) and 0xFF0000000000L) or + ((value shl 8) and 0xFF00000000L) + +fun bitConvert(value: Int): Long = value.toLong() and 0xFFFFFFFFL + +fun zigZagEncode(value: Long): Long = (value shl 1) xor (value shr 63) +fun zigZagDecode(value: Long): Long = (value shr 1) xor ((value shl 63) shr 63) \ No newline at end of file diff --git a/src/net/tofvesson/networking/SyncHandler.kt b/src/net/tofvesson/networking/SyncHandler.kt new file mode 100644 index 0000000..aee04cf --- /dev/null +++ b/src/net/tofvesson/networking/SyncHandler.kt @@ -0,0 +1,312 @@ +package net.tofvesson.networking + +import java.lang.reflect.Field +import java.nio.ByteBuffer +import kotlin.experimental.and + +class SyncHandler { + private val toSync: ArrayList> = ArrayList() + + fun registerSyncObject(value: Any){ + if(!toSync.contains(value)) toSync.add(Pair(value, getBooleanFieldCount(value::class.java))) + } + + fun unregisterSyncObject(value: Any){ + toSync.remove(value) + } + + fun serialize(): ByteArray{ + val headerBits = computeBitHeaderCount() + val headerSize = (headerBits shr 3) + (if((headerBits and 7) != 0) 1 else 0) + var headerIndex = 0 + var dataIndex = headerSize + var totalSize = headerSize + for((entry, _) in toSync) + totalSize += if(entry is Class<*>) computeClassSize(entry) else computeObjectSize(entry) + + val buffer = ByteArray(totalSize) + for((entry, _) in toSync){ + val result = + if(entry is Class<*>) readClass(entry, buffer, dataIndex, headerIndex) + else readObject(entry, buffer, dataIndex, headerIndex) + dataIndex = result.first + headerIndex = result.second + } + return buffer + } + + fun deserialize(syncData: ByteArray){ + val headerBits = computeBitHeaderCount() + val headerSize = (headerBits shr 3) + (if((headerBits and 7) != 0) 1 else 0) + var headerIndex = 0 + var dataIndex = headerSize + var totalSize = headerSize + for((entry, _) in toSync){ + val result = + if(entry is Class<*>) writeClass(entry, syncData, dataIndex, headerIndex) + else writeObject(entry, syncData, dataIndex, headerIndex) + dataIndex = result.first + headerIndex = result.second + } + } + + fun generateMismatchCheck(): ByteArray { + val bitCount = computeBitHeaderCount() + val outBuffer = ByteArray(varIntSize(bitCount.toLong()) + varIntSize(toSync.size.toLong())) + writeVarInt(outBuffer, 0, bitCount.toLong()) + writeVarInt(outBuffer, varIntSize(bitCount.toLong()), toSync.size.toLong()) + return outBuffer + } + + fun doMismatchCheck(check: ByteArray): Boolean { + val mismatchCheck = generateMismatchCheck() + if(mismatchCheck.size != check.size) return false + for(index in mismatchCheck.indices) + if(mismatchCheck[index]!=check[index]) + return false + return true + } + + private fun computeBitHeaderCount(): Int { + var bitCount = 0 + for((_, bits) in toSync) + bitCount += bits + return bitCount + } + + private fun readObject(value: Any, buffer: ByteArray, offset: Int, bitOffset: Int) = readType(value.javaClass, value, buffer, offset, bitOffset) + private fun readClass(value: Class<*>, buffer: ByteArray, offset: Int, bitOffset: Int) = readType(value, null, buffer, offset, bitOffset) + private fun readType(type: Class<*>, value: Any?, buffer: ByteArray, offset: Int, bitOffset: Int): Pair { + val byteBuffer = ByteBuffer.wrap(buffer) + var localOffset = offset + var localBitOffset = bitOffset + 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) + if(annotation != null){ + if(field.type!=Boolean::class.java) localOffset += readValue(field, annotation, value, byteBuffer, localOffset) + else writeBit(field.getBoolean(value), buffer, localBitOffset++) + } + } + return Pair(localOffset, localBitOffset) + } + + private fun writeObject(value: Any, buffer: ByteArray, offset: Int, bitOffset: Int) = writeType(value.javaClass, value, buffer, offset, bitOffset) + private fun writeClass(value: Class<*>, buffer: ByteArray, offset: Int, bitOffset: Int) = writeType(value, null, buffer, offset, bitOffset) + private fun writeType(type: Class<*>, value: Any?, buffer: ByteArray, offset: Int, bitOffset: Int): Pair { + val byteBuffer = ByteBuffer.wrap(buffer) + var localOffset = offset + var localBitOffset = bitOffset + 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) + if(annotation != null){ + if(field.type!=Boolean::class.java) localOffset += writeValue(field, annotation, value, byteBuffer, localOffset) + else field.setBoolean(value, readBit(buffer, localBitOffset++)) + } + } + 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] and (1 shl (index and 7)).toByte() + } + private fun readBit(buffer: ByteArray, index: Int): Boolean = buffer[index shr 8].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 new file mode 100644 index 0000000..4278e19 --- /dev/null +++ b/src/net/tofvesson/networking/SyncedVar.kt @@ -0,0 +1,13 @@ +package net.tofvesson.networking + +/** + * 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 + * @param nonNegative An advanced compression flag that may decrease serialization size at the cost of never having + * negative values. NOTE: If the field is not a short, int or long or noCompress is set to + * true, this parameter is ignored. This will cause errors if used in conjunction with a negative integer value. + * @param floatEndianSwap Whether or not floating point values should have their endianness swapped before compression. + * 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