NetworkedElement, Json RPC and ClientContextUpdate fixes

This commit is contained in:
DBotThePony 2024-03-19 15:22:05 +07:00
parent d37bad79c6
commit 66aa99acc2
Signed by: DBot
GPG Key ID: DCC23B5715498507
31 changed files with 1290 additions and 90 deletions

View File

@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
kotlinVersion=1.9.10 kotlinVersion=1.9.10
kotlinCoroutinesVersion=1.8.0 kotlinCoroutinesVersion=1.8.0
kommonsVersion=2.9.20 kommonsVersion=2.9.21
ffiVersion=2.2.13 ffiVersion=2.2.13
lwjglVersion=3.3.0 lwjglVersion=3.3.0

View File

@ -10,6 +10,7 @@ import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig
import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig
import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig
import ru.dbotthepony.kstarbound.defs.world.DungeonWorldsConfig import ru.dbotthepony.kstarbound.defs.world.DungeonWorldsConfig
import ru.dbotthepony.kstarbound.defs.world.SkyGlobalConfig
import ru.dbotthepony.kstarbound.defs.world.WorldTemplateConfig import ru.dbotthepony.kstarbound.defs.world.WorldTemplateConfig
import ru.dbotthepony.kstarbound.json.mapAdapter import ru.dbotthepony.kstarbound.json.mapAdapter
import ru.dbotthepony.kstarbound.json.pairSetAdapter import ru.dbotthepony.kstarbound.json.pairSetAdapter
@ -53,6 +54,9 @@ object GlobalDefaults {
var bushDamage by Delegates.notNull<TileDamageConfig>() var bushDamage by Delegates.notNull<TileDamageConfig>()
private set private set
var sky by Delegates.notNull<SkyGlobalConfig>()
private set
private object EmptyTask : ForkJoinTask<Unit>() { private object EmptyTask : ForkJoinTask<Unit>() {
private fun readResolve(): Any = EmptyTask private fun readResolve(): Any = EmptyTask
override fun getRawResult() { override fun getRawResult() {
@ -99,12 +103,14 @@ object GlobalDefaults {
tasks.add(load("/terrestrial_worlds.config", ::terrestrialWorlds)) tasks.add(load("/terrestrial_worlds.config", ::terrestrialWorlds))
tasks.add(load("/asteroids_worlds.config", ::asteroidWorlds)) tasks.add(load("/asteroids_worlds.config", ::asteroidWorlds))
tasks.add(load("/world_template.config", ::worldTemplate)) tasks.add(load("/world_template.config", ::worldTemplate))
tasks.add(load("/dungeon_worlds.config", ::dungeonWorlds, Starbound.gson.mapAdapter())) tasks.add(load("/sky.config", ::sky))
tasks.add(load("/plants/grassDamage.config", ::grassDamage)) tasks.add(load("/plants/grassDamage.config", ::grassDamage))
tasks.add(load("/plants/treeDamage.config", ::treeDamage)) tasks.add(load("/plants/treeDamage.config", ::treeDamage))
tasks.add(load("/plants/bushDamage.config", ::bushDamage)) tasks.add(load("/plants/bushDamage.config", ::bushDamage))
tasks.add(load("/dungeon_worlds.config", ::dungeonWorlds, Starbound.gson.mapAdapter()))
return tasks return tasks
} }
} }

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.defs.item.api package ru.dbotthepony.kstarbound.defs.item.api
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.AssetPath import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.defs.IThingWithDescription import ru.dbotthepony.kstarbound.defs.IThingWithDescription
@ -37,7 +38,7 @@ interface IItemDefinition : IThingWithDescription {
/** /**
* Иконка в инвентаре, относительный и абсолютный пути * Иконка в инвентаре, относительный и абсолютный пути
*/ */
val inventoryIcon: List<IInventoryIcon>? val inventoryIcon: Either<out IInventoryIcon, out List<IInventoryIcon>>?
/** /**
* Теги предмета * Теги предмета

View File

@ -1,11 +1,13 @@
package ru.dbotthepony.kstarbound.defs.item.impl package ru.dbotthepony.kstarbound.defs.item.impl
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.AssetPath import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.defs.IThingWithDescription import ru.dbotthepony.kstarbound.defs.IThingWithDescription
import ru.dbotthepony.kstarbound.defs.ThingDescription import ru.dbotthepony.kstarbound.defs.ThingDescription
import ru.dbotthepony.kstarbound.defs.item.IInventoryIcon import ru.dbotthepony.kstarbound.defs.item.IInventoryIcon
import ru.dbotthepony.kstarbound.defs.item.InventoryIcon
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.defs.item.ItemRarity import ru.dbotthepony.kstarbound.defs.item.ItemRarity
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@ -20,7 +22,7 @@ data class ItemDefinition(
override val price: Long = 0, override val price: Long = 0,
override val rarity: ItemRarity = ItemRarity.COMMON, override val rarity: ItemRarity = ItemRarity.COMMON,
override val category: String? = null, override val category: String? = null,
override val inventoryIcon: ImmutableList<IInventoryIcon>? = null, override val inventoryIcon: Either<InventoryIcon, ImmutableList<IInventoryIcon>>? = null,
override val itemTags: ImmutableList<String> = ImmutableList.of(), override val itemTags: ImmutableList<String> = ImmutableList.of(),
override val learnBlueprintsOnPickup: ImmutableList<Registry.Ref<IItemDefinition>> = ImmutableList.of(), override val learnBlueprintsOnPickup: ImmutableList<Registry.Ref<IItemDefinition>> = ImmutableList.of(),
override val maxStack: Long = 9999L, override val maxStack: Long = 9999L,

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.defs.tile
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.defs.IThingWithDescription import ru.dbotthepony.kstarbound.defs.IThingWithDescription
@ -16,8 +17,8 @@ data class TileDefinition(
val materialName: String, val materialName: String,
val particleColor: RGBAColor? = null, val particleColor: RGBAColor? = null,
val itemDrop: String? = null, val itemDrop: String? = null,
val footstepSound: ImmutableList<String> = ImmutableList.of(), val footstepSound: Either<ImmutableList<String>, String> = Either.left(ImmutableList.of()),
val miningSounds: ImmutableList<String> = ImmutableList.of(), val miningSounds: Either<ImmutableList<String>, String> = Either.left(ImmutableList.of()),
val blocksLiquidFlow: Boolean = true, val blocksLiquidFlow: Boolean = true,
val soil: Boolean = false, val soil: Boolean = false,

View File

@ -1,9 +1,12 @@
package ru.dbotthepony.kstarbound.defs.world package ru.dbotthepony.kstarbound.defs.world
import com.google.common.collect.ImmutableList
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.io.readColor import ru.dbotthepony.kstarbound.io.readColor
import ru.dbotthepony.kstarbound.io.writeColor import ru.dbotthepony.kstarbound.io.writeColor
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
@ -20,14 +23,22 @@ enum class SkyType {
ATMOSPHERELESS, ATMOSPHERELESS,
ORBITAL, ORBITAL,
WARP, WARP,
SPACE SPACE;
companion object {
val CODEC = StreamCodec.Enum(SkyType::class.java)
}
} }
enum class FlyingType { enum class FlyingType {
NONE, NONE,
DISEMBARKING, DISEMBARKING,
WARP, WARP,
ARRIVING ARRIVING;
companion object {
val CODEC = StreamCodec.Enum(FlyingType::class.java)
}
} }
enum class WarpPhase(val stupidassbitch: Int) { enum class WarpPhase(val stupidassbitch: Int) {
@ -119,16 +130,17 @@ data class SkyColoring(
data class SkyWorldHorizon(val center: Vector2d, val scale: Double, val rotation: Double, val layers: List<Pair<String, String>>) data class SkyWorldHorizon(val center: Vector2d, val scale: Double, val rotation: Double, val layers: List<Pair<String, String>>)
class SkyParameters() { @JsonFactory
var skyType = SkyType.BARREN data class SkyParameters(
var seed = 0L var skyType: SkyType = SkyType.BARREN,
var dayLength: Double? = null var seed: Long = 0L,
var horizonClouds = false var dayLength: Double? = null,
var skyColoring: Either<SkyColoring, RGBAColor> = Either.right(RGBAColor.BLACK) var horizonClouds: Boolean = false,
var spaceLevel: Double? = null var skyColoring: Either<SkyColoring, RGBAColor> = Either.left(SkyColoring()),
var surfaceLevel: Double? = null var spaceLevel: Double? = null,
var nearbyPlanet: Pair<List<Pair<String, Double>>, Vector2d>? = null var surfaceLevel: Double? = null,
var nearbyPlanet: Pair<List<Pair<String, Double>>, Vector2d>? = null,
) {
companion object { companion object {
suspend fun create(coordinate: UniversePos, universe: Universe): SkyParameters { suspend fun create(coordinate: UniversePos, universe: Universe): SkyParameters {
if (coordinate.isSystem) if (coordinate.isSystem)
@ -163,3 +175,13 @@ class SkyParameters() {
} }
} }
} }
data class SkyGlobalConfig(
val stars: Stars,
) {
data class Stars(
val frames: Int,
val list: ImmutableList<AssetPath>,
val hyperlist: ImmutableList<AssetPath>,
)
}

View File

@ -215,13 +215,13 @@ private fun materialMiningSound(context: ExecutionContext, arguments: ArgumentIt
val tile = lookup(Registries.tiles, arguments.nextAny()) val tile = lookup(Registries.tiles, arguments.nextAny())
val mod = lookup(Registries.tiles, arguments.nextOptionalAny(null)) val mod = lookup(Registries.tiles, arguments.nextOptionalAny(null))
if (mod != null && mod.value.miningSounds.isNotEmpty()) { if (mod != null && mod.value.miningSounds.map({ it.isNotEmpty() }, { true })) {
context.returnBuffer.setTo(mod.value.miningSounds.random()) context.returnBuffer.setTo(mod.value.miningSounds.map({ it.random() }, { it }))
return return
} }
if (tile != null && tile.value.miningSounds.isNotEmpty()) { if (tile != null && tile.value.miningSounds.map({ it.isNotEmpty() }, { true })) {
context.returnBuffer.setTo(tile.value.miningSounds.random()) context.returnBuffer.setTo(tile.value.miningSounds.map({ it.random() }, { it }))
return return
} }
@ -233,13 +233,13 @@ private fun materialFootstepSound(context: ExecutionContext, arguments: Argument
val tile = lookup(Registries.tiles, arguments.nextAny()) val tile = lookup(Registries.tiles, arguments.nextAny())
val mod = lookup(Registries.tiles, arguments.nextOptionalAny(null)) val mod = lookup(Registries.tiles, arguments.nextOptionalAny(null))
if (mod != null && mod.value.footstepSound.isNotEmpty()) { if (mod != null && mod.value.footstepSound.map({ it.isNotEmpty() }, { true })) {
context.returnBuffer.setTo(mod.value.footstepSound.random()) context.returnBuffer.setTo(mod.value.footstepSound.map({ it.random() }, { it }))
return return
} }
if (tile != null && tile.value.footstepSound.isNotEmpty()) { if (tile != null && tile.value.footstepSound.map({ it.isNotEmpty() }, { true })) {
context.returnBuffer.setTo(tile.value.footstepSound.random()) context.returnBuffer.setTo(tile.value.footstepSound.map({ it.random() }, { it }))
return return
} }

View File

@ -0,0 +1,31 @@
package ru.dbotthepony.kstarbound.math
import ru.dbotthepony.kommons.math.linearInterpolation
import kotlin.math.PI
import kotlin.math.sin
fun interface Interpolator {
fun interpolate(t: Double, a: Double, b: Double): Double
object Sin : Interpolator {
override fun interpolate(t: Double, a: Double, b: Double): Double {
return linearInterpolation((sin(t * PI - PI / 2.0) + 1.0) / 2.0, a, b)
}
}
object Linear : Interpolator {
override fun interpolate(t: Double, a: Double, b: Double): Double {
// custom to allow extrapolation
return a + (b - a) * t
}
}
object NearestMiddle : Interpolator {
override fun interpolate(t: Double, a: Double, b: Double): Double {
if (t >= 0.5)
return b
else
return a
}
}
}

View File

@ -12,16 +12,6 @@ data class PeriodicFunction(
val periodVariance: Double = 0.0, val periodVariance: Double = 0.0,
val magnitudeVariance: Double = 0.0 val magnitudeVariance: Double = 0.0
) { ) {
fun interface Interpolator {
fun interpolate(t: Double, a: Double, b: Double): Double
}
object Sin : Interpolator {
override fun interpolate(t: Double, a: Double, b: Double): Double {
return linearInterpolation((sin(t * PI - PI / 2.0) + 1.0) / 2.0, a, b)
}
}
private var timer = 0.0 private var timer = 0.0
private var timerMax = 1.0 private var timerMax = 1.0
@ -60,6 +50,6 @@ data class PeriodicFunction(
} }
fun sinValue(): Double { fun sinValue(): Double {
return value(Sin) return value(Interpolator.Sin)
} }
} }

View File

@ -75,8 +75,16 @@ enum class ConnectionType {
MEMORY; MEMORY;
} }
fun interface IPacketReader<T : IPacket> { fun interface IPacketReaderDetailed<T : IPacket> {
fun read(stream: DataInputStream, isLegacy: Boolean, side: ConnectionSide): T
}
fun interface IPacketReader<T : IPacket> : IPacketReaderDetailed<T> {
fun read(stream: DataInputStream, isLegacy: Boolean): T fun read(stream: DataInputStream, isLegacy: Boolean): T
override fun read(stream: DataInputStream, isLegacy: Boolean, side: ConnectionSide): T {
return read(stream, isLegacy)
}
} }
interface IPacket { interface IPacket {

View File

@ -73,6 +73,8 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
isLegacy = false isLegacy = false
isConnected = true isConnected = true
inGame()
} }
protected open fun onChannelClosed() { protected open fun onChannelClosed() {

View File

@ -25,13 +25,15 @@ import kotlin.concurrent.withLock
class JsonRPC { class JsonRPC {
enum class Command { enum class Command {
REQUEST, RESPONSE, FAIL; REQUEST, RESPONSE, FAIL;
val jsonName = name.lowercase()
} }
data class Entry(val command: Command, val id: Int, val handler: KOptional<String>, val arguments: KOptional<JsonElement>) { data class Entry(val command: Command, val id: Int, val handler: KOptional<String>, val arguments: KOptional<JsonElement>) {
fun write(stream: DataOutputStream, isLegacy: Boolean) { fun write(stream: DataOutputStream, isLegacy: Boolean) {
if (isLegacy) { if (isLegacy) {
stream.writeJsonElement(JsonObject().also { stream.writeJsonElement(JsonObject().also {
it["command"] = command.name.lowercase() it["command"] = command.jsonName
it["id"] = id it["id"] = id
handler.ifPresent { v -> it["handler"] = v } handler.ifPresent { v -> it["handler"] = v }
arguments.ifPresent { v -> if (command == Command.RESPONSE) it["result"] = v else it["arguments"] = v } arguments.ifPresent { v -> if (command == Command.RESPONSE) it["result"] = v else it["arguments"] = v }
@ -52,11 +54,11 @@ class JsonRPC {
fun legacy(stream: DataInputStream): Entry { fun legacy(stream: DataInputStream): Entry {
val data = stream.readJsonElement() val data = stream.readJsonElement()
check(data is JsonObject) { "Expected JsonObject, got ${data::class}" } check(data is JsonObject) { "Expected JsonObject, got ${data::class}" }
val command = data["command"]?.asString?.uppercase() ?: throw JsonSyntaxException("Missing 'command' in RPC data") val command = data["command"]?.asString?.lowercase() ?: throw JsonSyntaxException("Missing 'command' in RPC data")
val id = data["id"]?.asInt ?: throw JsonSyntaxException("Missing 'id' in RPC data") val id = data["id"]?.asInt ?: throw JsonSyntaxException("Missing 'id' in RPC data")
val handler = KOptional.ofNullable(data["handler"]?.asString) val handler = KOptional.ofNullable(data["handler"]?.asString)
val arguments = KOptional.ofNullable(data["arguments"]) val arguments = KOptional.ofNullable(data["arguments"])
return Entry(Command.entries.firstOrNull { it.name == command } ?: throw JsonSyntaxException("Invalid 'command': $command"), id, handler, arguments) return Entry(Command.entries.firstOrNull { it.jsonName == command } ?: throw JsonSyntaxException("Invalid 'command': $command"), id, handler, arguments)
} }
fun read(stream: DataInputStream, isLegacy: Boolean): Entry { fun read(stream: DataInputStream, isLegacy: Boolean): Entry {
@ -111,8 +113,13 @@ class JsonRPC {
try { try {
when (entry.command) { when (entry.command) {
Command.REQUEST -> { Command.REQUEST -> {
val handler = handlers[entry.handler.value] ?: throw IllegalArgumentException("No such handler ${entry.handler.value}") val handler = handlers[entry.handler.value]
pendingWrite.add(Entry(Command.RESPONSE, entry.id, KOptional(), KOptional(handler(entry.arguments.value))))
if (handler == null) {
pendingWrite.add(Entry(Command.FAIL, entry.id, KOptional(), KOptional()))
} else {
pendingWrite.add(Entry(Command.RESPONSE, entry.id, KOptional(), KOptional(handler(entry.arguments.value))))
}
} }
Command.RESPONSE -> { Command.RESPONSE -> {

View File

@ -50,12 +50,12 @@ class PacketRegistry(val isLegacy: Boolean) {
private val missingNames = Int2ObjectArrayMap<String>() private val missingNames = Int2ObjectArrayMap<String>()
private val clazz2Type = Reference2ObjectOpenHashMap<KClass<*>, Type<*>>() private val clazz2Type = Reference2ObjectOpenHashMap<KClass<*>, Type<*>>()
private data class Type<T : IPacket>(val id: Int, val type: KClass<T>, val factory: IPacketReader<T>, val direction: PacketDirection) private data class Type<T : IPacket>(val id: Int, val type: KClass<T>, val factory: IPacketReaderDetailed<T>, val direction: PacketDirection)
val size: Int val size: Int
get() = packets.size get() = packets.size
private fun <T : IPacket> add(type: KClass<T>, reader: IPacketReader<T>, direction: PacketDirection = PacketDirection.get(type)): PacketRegistry { private fun <T : IPacket> add(type: KClass<T>, reader: IPacketReaderDetailed<T>, direction: PacketDirection = PacketDirection.get(type)): PacketRegistry {
if (packets.size >= 255) if (packets.size >= 255)
throw IndexOutOfBoundsException("Unable to add any more packet types! 255 is the max") throw IndexOutOfBoundsException("Unable to add any more packet types! 255 is the max")
@ -72,8 +72,12 @@ class PacketRegistry(val isLegacy: Boolean) {
return add(T::class, reader, direction) return add(T::class, reader, direction)
} }
private inline fun <reified T : IPacket> add(reader: IPacketReaderDetailed<T>, direction: PacketDirection = PacketDirection.get(T::class)): PacketRegistry {
return add(T::class, reader, direction)
}
private inline fun <reified T : IPacket> add(value: T, direction: PacketDirection = PacketDirection.get(T::class)): PacketRegistry { private inline fun <reified T : IPacket> add(value: T, direction: PacketDirection = PacketDirection.get(T::class)): PacketRegistry {
return add(T::class, { _, _ -> value }, direction) return add(T::class, { _, _, _ -> value }, direction)
} }
private fun skip(amount: Int = 1) { private fun skip(amount: Int = 1) {
@ -154,12 +158,16 @@ class PacketRegistry(val isLegacy: Boolean) {
// legacy protocol allows to stitch multiple packets of same type together without // legacy protocol allows to stitch multiple packets of same type together without
// separate headers for each // separate headers for each
while (stream.available() > 0) { // Due to nature of netty pipeline, we can't do the same on native protocol;
// so don't do that when on native protocol
while (stream.available() > 0 || !isLegacy) {
try { try {
ctx.fireChannelRead(readingType!!.factory.read(DataInputStream(stream), isLegacy)) ctx.fireChannelRead(readingType!!.factory.read(DataInputStream(stream), isLegacy, side))
} catch (err: Throwable) { } catch (err: Throwable) {
LOGGER.error("Error while reading incoming packet from network (type ${readingType!!.id}; ${readingType!!.type})", err) LOGGER.error("Error while reading incoming packet from network (type ${readingType!!.id}; ${readingType!!.type})", err)
} }
if (!isLegacy) break
} }
stream.close() stream.close()
@ -219,8 +227,8 @@ class PacketRegistry(val isLegacy: Boolean) {
val stream = FastByteArrayOutputStream() val stream = FastByteArrayOutputStream()
(msg as IPacket).write(DataOutputStream(stream), isLegacy) (msg as IPacket).write(DataOutputStream(stream), isLegacy)
if (isLegacy) if (isLegacy && stream.length == 0)
check(stream.length > 0) { "Packet $msg didn't write any data to network, this is not allowed by legacy protocol" } throw IllegalStateException("Packet $msg didn't write any data to network, this is not allowed by legacy protocol")
if (stream.length >= 512) { if (stream.length >= 512) {
// compress // compress
@ -228,6 +236,7 @@ class PacketRegistry(val isLegacy: Boolean) {
val buffers = ByteArrayList(1024) val buffers = ByteArrayList(1024)
val buffer = ByteArray(1024) val buffer = ByteArray(1024)
deflater.setInput(stream.array, 0, stream.length) deflater.setInput(stream.array, 0, stream.length)
deflater.finish()
while (!deflater.needsInput()) { while (!deflater.needsInput()) {
val deflated = deflater.deflate(buffer) val deflated = deflater.deflate(buffer)
@ -369,7 +378,7 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.skip("SetDungeonBreathable") LEGACY.skip("SetDungeonBreathable")
LEGACY.skip("SetPlayerStart") LEGACY.skip("SetPlayerStart")
LEGACY.skip("FindUniqueEntityResponse") LEGACY.skip("FindUniqueEntityResponse")
LEGACY.add(PongPacket) LEGACY.add(PongPacket::read)
// Packets sent world client -> world server // Packets sent world client -> world server
LEGACY.skip("ModifyTileList") LEGACY.skip("ModifyTileList")
@ -382,7 +391,7 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.skip("WorldClientStateUpdate") LEGACY.skip("WorldClientStateUpdate")
LEGACY.skip("FindUniqueEntity") LEGACY.skip("FindUniqueEntity")
LEGACY.skip("WorldStartAcknowledge") LEGACY.skip("WorldStartAcknowledge")
LEGACY.add(PingPacket) LEGACY.add(PingPacket::read)
// Packets sent bidirectionally between world client and world server // Packets sent bidirectionally between world client and world server
LEGACY.skip("EntityCreate") LEGACY.skip("EntityCreate")

View File

@ -17,6 +17,8 @@ import ru.dbotthepony.kommons.io.writeMap
import ru.dbotthepony.kommons.io.writeVarInt import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.client.ClientConnection import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.network.ConnectionSide
import ru.dbotthepony.kstarbound.network.IClientPacket import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.IServerPacket import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.network.JsonRPC import ru.dbotthepony.kstarbound.network.JsonRPC
@ -34,23 +36,31 @@ class ClientContextUpdatePacket(
) : IClientPacket, IServerPacket { ) : IClientPacket, IServerPacket {
override fun write(stream: DataOutputStream, isLegacy: Boolean) { override fun write(stream: DataOutputStream, isLegacy: Boolean) {
if (isLegacy) { if (isLegacy) {
// this is stupid if (!shipChunks.isPresent && !networkedVars.isPresent) {
run { // client to server
val wrap = FastByteArrayOutputStream() stream.writeCollection(rpcEntries) { it.write(this, true) }
DataOutputStream(wrap).writeCollection(rpcEntries) { it.write(this, true) } } else {
stream.writeVarInt(wrap.length) // server to client
stream.write(wrap.array, 0, wrap.length) // this is so dumb
} val wrap2 = FastByteArrayOutputStream()
shipChunks.ifPresent { run {
val wrap = FastByteArrayOutputStream() val wrap = FastByteArrayOutputStream()
DataOutputStream(wrap).writeMap(it, { it.write(this) }, { writeKOptional(it) { writeByteArray(it) } }) DataOutputStream(wrap).writeCollection(rpcEntries) { it.write(this, true) }
stream.writeByteArray(wrap.array, 0, wrap.length) wrap2.writeByteArray(wrap.array, 0, wrap.length)
} }
networkedVars.ifPresent { shipChunks.ifPresent {
stream.writeVarInt(it.size) val wrap = FastByteArrayOutputStream()
stream.write(it.elements(), 0, it.size) DataOutputStream(wrap).writeMap(it, { it.write(this) }, { writeKOptional(it) { writeByteArray(it) } })
wrap2.writeByteArray(wrap.array, 0, wrap.length)
}
networkedVars.ifPresent {
wrap2.writeByteArray(it.elements(), 0, it.size)
}
stream.writeByteArray(wrap2.array, 0, wrap2.length)
} }
} else { } else {
stream.writeCollection(rpcEntries) { it.write(this, false) } stream.writeCollection(rpcEntries) { it.write(this, false) }
@ -75,16 +85,25 @@ class ClientContextUpdatePacket(
} }
companion object { companion object {
fun read(stream: DataInputStream, isLegacy: Boolean): ClientContextUpdatePacket { fun read(stream: DataInputStream, isLegacy: Boolean, side: ConnectionSide): ClientContextUpdatePacket {
if (isLegacy) { if (isLegacy) {
// beyond stupid // beyond stupid
val rpc = stream.readByteArray() if (side == ConnectionSide.SERVER) {
return ClientContextUpdatePacket(
DataInputStream(FastByteArrayInputStream(stream.readByteArray())).readCollection { JsonRPC.Entry.legacy(this) },
KOptional(),
KOptional()
)
} else {
val wrap = DataInputStream(FastByteArrayInputStream(stream.readByteArray()))
val rpc = wrap.readByteArray()
return ClientContextUpdatePacket( return ClientContextUpdatePacket(
DataInputStream(FastByteArrayInputStream(rpc)).readCollection { JsonRPC.Entry.legacy(this) }, DataInputStream(FastByteArrayInputStream(rpc)).readCollection { JsonRPC.Entry.legacy(this) },
if (stream.available() > 0) KOptional(stream.readMap({ readByteKey() }, { readKOptional { readByteArray() } })) else KOptional(), if (wrap.available() > 0) KOptional(wrap.readMap({ readByteKey() }, { readKOptional { readByteArray() } })) else KOptional(),
if (stream.available() > 0) KOptional(ByteArrayList.wrap(stream.readByteArray())) else KOptional(), if (wrap.available() > 0) KOptional(ByteArrayList.wrap(wrap.readByteArray())) else KOptional(),
) )
}
} else { } else {
return ClientContextUpdatePacket( return ClientContextUpdatePacket(
stream.readCollection { JsonRPC.Entry.native(this) }, stream.readCollection { JsonRPC.Entry.native(this) },

View File

@ -4,6 +4,7 @@ import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.network.IClientPacket import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.IServerPacket import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.server.ServerConnection
import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
object PongPacket : IClientPacket { object PongPacket : IClientPacket {
@ -14,6 +15,11 @@ object PongPacket : IClientPacket {
override fun play(connection: ClientConnection) { override fun play(connection: ClientConnection) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
fun read(stream: DataInputStream, isLegacy: Boolean): PongPacket {
if (isLegacy) stream.readBoolean()
return PongPacket
}
} }
object PingPacket : IServerPacket { object PingPacket : IServerPacket {
@ -22,6 +28,11 @@ object PingPacket : IServerPacket {
} }
override fun play(connection: ServerConnection) { override fun play(connection: ServerConnection) {
connection.send(PongPacket) connection.sendAndFlush(PongPacket)
}
fun read(stream: DataInputStream, isLegacy: Boolean): PingPacket {
if (isLegacy) stream.readBoolean()
return PingPacket
} }
} }

View File

@ -24,10 +24,16 @@ import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
class WorldStartPacket( class WorldStartPacket(
val templateData: JsonElement, val skyData: ByteArray, val weatherData: ByteArray, val templateData: JsonElement,
val playerStart: Vector2d, val playerRespawn: Vector2d, val respawnInWorld: Boolean, val skyData: ByteArray,
val dungeonGravity: Map<Int, Vector2d>, val dungeonBreathable: Map<Int, Boolean>, val weatherData: ByteArray,
val protectedDungeonIDs: Set<Int>, val worldProperties: JsonElement, val connectionID: Int, val playerStart: Vector2d,
val playerRespawn: Vector2d,
val respawnInWorld: Boolean,
val worldProperties: JsonElement,
val dungeonGravity: Map<Int, Vector2d>,
val dungeonBreathable: Map<Int, Boolean>,
val protectedDungeonIDs: Set<Int>, val connectionID: Int,
val localInterpolationMode: Boolean, val localInterpolationMode: Boolean,
) : IClientPacket { ) : IClientPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this( constructor(stream: DataInputStream, isLegacy: Boolean) : this(
@ -37,10 +43,10 @@ class WorldStartPacket(
if (isLegacy) stream.readVector2f().toDoubleVector() else stream.readVector2d(), if (isLegacy) stream.readVector2f().toDoubleVector() else stream.readVector2d(),
if (isLegacy) stream.readVector2f().toDoubleVector() else stream.readVector2d(), if (isLegacy) stream.readVector2f().toDoubleVector() else stream.readVector2d(),
stream.readBoolean(), stream.readBoolean(),
stream.readJsonElement(),
if (isLegacy) stream.readMap({ readUnsignedShort() }, { Vector2d(0.0, readFloat().toDouble()) }, ::Int2ObjectOpenHashMap) else stream.readMap({ readInt() }, { readVector2d() }, { Int2ObjectAVLTreeMap() }), if (isLegacy) stream.readMap({ readUnsignedShort() }, { Vector2d(0.0, readFloat().toDouble()) }, ::Int2ObjectOpenHashMap) else stream.readMap({ readInt() }, { readVector2d() }, { Int2ObjectAVLTreeMap() }),
if (isLegacy) stream.readMap({ readUnsignedShort() }, { readBoolean() }, ::Int2ObjectOpenHashMap) else stream.readMap({ readInt() }, { readBoolean() }, { Int2BooleanAVLTreeMap() }), if (isLegacy) stream.readMap({ readUnsignedShort() }, { readBoolean() }, ::Int2ObjectOpenHashMap) else stream.readMap({ readInt() }, { readBoolean() }, { Int2BooleanAVLTreeMap() }),
if (isLegacy) stream.readCollection({ readUnsignedShort() }, { IntAVLTreeSet() }) else stream.readCollection({ readInt() }, { IntAVLTreeSet() }), if (isLegacy) stream.readCollection({ readUnsignedShort() }, { IntAVLTreeSet() }) else stream.readCollection({ readInt() }, { IntAVLTreeSet() }),
stream.readJsonElement(),
stream.readUnsignedShort(), stream.readUnsignedShort(),
stream.readBoolean() stream.readBoolean()
) )
@ -54,6 +60,7 @@ class WorldStartPacket(
stream.writeStruct2f(playerStart.toFloatVector()) stream.writeStruct2f(playerStart.toFloatVector())
stream.writeStruct2f(playerRespawn.toFloatVector()) stream.writeStruct2f(playerRespawn.toFloatVector())
stream.writeBoolean(respawnInWorld) stream.writeBoolean(respawnInWorld)
stream.writeJsonElement(worldProperties)
stream.writeMap(dungeonGravity, { writeShort(it) }, { writeFloat(it.y.toFloat()) }) stream.writeMap(dungeonGravity, { writeShort(it) }, { writeFloat(it.y.toFloat()) })
stream.writeMap(dungeonBreathable, { writeShort(it) }, { writeBoolean(it) }) stream.writeMap(dungeonBreathable, { writeShort(it) }, { writeBoolean(it) })
stream.writeCollection(protectedDungeonIDs) { writeShort(it) } stream.writeCollection(protectedDungeonIDs) { writeShort(it) }
@ -61,12 +68,12 @@ class WorldStartPacket(
stream.writeStruct2d(playerStart) stream.writeStruct2d(playerStart)
stream.writeStruct2d(playerRespawn) stream.writeStruct2d(playerRespawn)
stream.writeBoolean(respawnInWorld) stream.writeBoolean(respawnInWorld)
stream.writeJsonElement(worldProperties)
stream.writeMap(dungeonGravity, { writeInt(it) }, { writeStruct2d(it) }) stream.writeMap(dungeonGravity, { writeInt(it) }, { writeStruct2d(it) })
stream.writeMap(dungeonBreathable, { writeInt(it) }, { writeBoolean(it) }) stream.writeMap(dungeonBreathable, { writeInt(it) }, { writeBoolean(it) })
stream.writeCollection(protectedDungeonIDs) { writeInt(it) } stream.writeCollection(protectedDungeonIDs) { writeInt(it) }
} }
stream.writeJsonElement(worldProperties)
stream.writeShort(connectionID) stream.writeShort(connectionID)
stream.writeBoolean(localInterpolationMode) stream.writeBoolean(localInterpolationMode)
} }

View File

@ -0,0 +1,123 @@
package ru.dbotthepony.kstarbound.network.syncher
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.util.Listenable
import ru.dbotthepony.kommons.util.ListenableDelegate
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.*
import java.util.function.Consumer
open class BasicNetworkedElement<TYPE, LEGACY>(private var value: TYPE, val codec: StreamCodec<TYPE>, val legacyCodec: StreamCodec<LEGACY>, val toLegacy: (TYPE) -> LEGACY, val fromLegacy: (LEGACY) -> TYPE) : NetworkedElement(), ListenableDelegate<TYPE> {
protected val valueListeners = Listenable.Impl<TYPE>()
protected val queue = LinkedList<Pair<Double, TYPE>>()
protected var isInterpolating = false
protected var currentTime = 0.0
override fun accept(t: TYPE) {
if (t != value) {
value = t
queue.clear()
bumpVersion()
valueListeners.accept(t)
}
}
override fun addListener(listener: Consumer<TYPE>): Listenable.L {
return valueListeners.addListener(listener)
}
override fun get(): TYPE {
return value
}
override fun readInitial(data: DataInputStream, isLegacy: Boolean) {
val old = value
value = if (isLegacy) fromLegacy(legacyCodec.read(data)) else codec.read(data)
queue.clear()
bumpVersion()
if (value != old) {
valueListeners.accept(value)
}
}
override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) {
if (isLegacy) {
if (queue.isNotEmpty()) {
legacyCodec.write(data, toLegacy(queue.last.second))
} else {
legacyCodec.write(data, toLegacy(value))
}
} else {
if (queue.isNotEmpty()) {
codec.write(data, queue.last.second)
} else {
codec.write(data, value)
}
}
}
override fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean) {
val read = if (isLegacy) fromLegacy(legacyCodec.read(data)) else codec.read(data)
bumpVersion()
if (isInterpolating) {
// Only append an incoming delta to our pending value list if the incoming
// step is forward in time of every other pending value. In any other
// case, this is an error or the step tracking is wildly off, so just clear
// any other incoming values.
val actualDelay = interpolationDelay + currentTime
if (interpolationDelay > 0.0 && (queue.isEmpty() || queue.last.first <= actualDelay)) {
queue.add(actualDelay to read)
} else {
value = read
queue.clear()
valueListeners.accept(read)
}
} else {
value = read
valueListeners.accept(read)
}
}
override fun writeDelta(data: DataOutputStream, remoteVersion: Long, isLegacy: Boolean) {
writeInitial(data, isLegacy)
}
override fun readBlankDelta(interpolationDelay: Double) {
// TODO: original engine doesn't override this, is this intentional?
// tickInterpolation(interpolationDelay)
}
override fun enableInterpolation(extrapolation: Double) {
if (!isInterpolating) {
isInterpolating = true
queue.clear()
}
}
override fun disableInterpolation() {
if (isInterpolating) {
isInterpolating = false
if (queue.isNotEmpty()) {
value = queue.last.second
valueListeners.accept(value)
}
queue.clear()
}
}
override fun tickInterpolation(delta: Double) {
require(delta >= 0.0) { "Negative interpolation delta: $delta" }
currentTime += delta
while (queue.isNotEmpty() && queue.first.first <= currentTime) {
value = queue.removeFirst().second
valueListeners.accept(value)
}
}
}

View File

@ -0,0 +1,45 @@
package ru.dbotthepony.kstarbound.network.syncher
import java.io.DataInputStream
import java.util.function.Consumer
class EventCounterElement : BasicNetworkedElement<Long, Long>(0L, UnsignedVarLongCodec, UnsignedVarLongCodec, { it }, { it }) {
var pulled = 0L
private set
var ignoreOccurrencesOnLoad = false
fun trigger() {
accept(get() + 1L)
}
fun pullOccurred(): Boolean {
return pullOccurrences() != 0L
}
fun pullOccurrences(): Long {
val value = get()
val delta = value - pulled
require(delta >= 0L) { "Event counter turned back in time" }
pulled = value
return delta
}
fun ignoreOccurrences() {
pulled = get()
}
override fun readInitial(data: DataInputStream, isLegacy: Boolean) {
super.readInitial(data, isLegacy)
if (ignoreOccurrencesOnLoad)
pulled = get()
}
init {
valueListeners.addListener(Consumer {
if (it < pulled)
pulled = it
})
}
}

View File

@ -0,0 +1,64 @@
package ru.dbotthepony.kstarbound.network.syncher
import com.google.gson.TypeAdapter
import ru.dbotthepony.kommons.io.BooleanValueCodec
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.VarIntValueCodec
import ru.dbotthepony.kommons.io.VarLongValueCodec
import ru.dbotthepony.kommons.io.Vector2dCodec
import ru.dbotthepony.kommons.io.Vector2fCodec
import ru.dbotthepony.kommons.io.readByteArray
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.readVarLong
import ru.dbotthepony.kommons.io.writeByteArray
import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kommons.io.writeVarLong
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.world.SkyType
import java.io.DataInputStream
import java.io.DataOutputStream
fun <TYPE> BasicNetworkedElement(value: TYPE, codec: StreamCodec<TYPE>): BasicNetworkedElement<TYPE, TYPE> {
return BasicNetworkedElement(value, codec, codec, { it }, { it })
}
val UnsignedVarLongCodec = StreamCodec.Impl(DataInputStream::readVarLong, DataOutputStream::writeVarLong)
val UnsignedVarIntCodec = StreamCodec.Impl(DataInputStream::readVarInt, DataOutputStream::writeVarInt)
val ByteArrayCodec = StreamCodec.Impl(DataInputStream::readByteArray, DataOutputStream::writeByteArray)
// networking size_t...
// god help us all
val SizeTCodec = StreamCodec.Impl({ stream -> stream.readVarLong().let { if (it == 0L) -1L else it - 1L } }, { stream, value -> if (value in 0L..<Long.MAX_VALUE) stream.writeVarLong(value + 1L) else stream.writeVarLong(0L) })
fun networkedFloat(value: Double = 0.0) = FloatingNetworkedElement.float(value)
fun networkedDouble(value: Double = 0.0) = FloatingNetworkedElement.double(value)
fun networkedFixedPoint(base: Double, value: Double = 0.0) = FloatingNetworkedElement.fixed(base, value)
fun networkedSignedInt(value: Int = 0) = BasicNetworkedElement(value, VarIntValueCodec)
fun networkedUnsignedInt(value: Int = 0) = BasicNetworkedElement(value, UnsignedVarIntCodec)
fun networkedSignedLong(value: Long = 0L) = BasicNetworkedElement(value, VarLongValueCodec)
fun networkedUnsignedLong(value: Long = 0L) = BasicNetworkedElement(value, UnsignedVarLongCodec)
fun networkedBoolean(value: Boolean = false) = BasicNetworkedElement(value, BooleanValueCodec)
fun networkedPointer(value: Long = 0L) = BasicNetworkedElement(value, SizeTCodec)
fun networkedVec2f(value: Vector2d = Vector2d.ZERO) = BasicNetworkedElement(value, Vector2dCodec, Vector2fCodec, { it.toFloatVector() }, { it.toDoubleVector() })
fun networkedBytes(value: ByteArray = ByteArray(0)) = BasicNetworkedElement(value, ByteArrayCodec)
fun <E : Enum<E>> networkedEnum(value: E) = BasicNetworkedElement(value, StreamCodec.Enum(value::class.java))
inline fun <reified T> networkedJson(value: T, adapter: TypeAdapter<T> = Starbound.gson.getAdapter(T::class.java), legacyIsArray: Boolean = true): BasicNetworkedElement<T, *> {
if (legacyIsArray) {
return BasicNetworkedElement(value, JsonCodec(adapter), JsonCodec(adapter, true), { it }, { it })
} else {
return BasicNetworkedElement(value, JsonCodec(adapter))
}
}
/**
* properly networks an enum on native protocol;
* networks a signed variable length integer on legacy protocol.
* this is way too dumb beyond my comprehension
*/
fun <E : Enum<E>> networkedEnumStupid(value: E): BasicNetworkedElement<E, Int> {
val codec = StreamCodec.Enum(value::class.java)
return BasicNetworkedElement(value, codec, VarIntValueCodec, { it.ordinal.shl(1) }, { codec.values[it.ushr(1)] })
}

View File

@ -0,0 +1,257 @@
package ru.dbotthepony.kstarbound.network.syncher
import ru.dbotthepony.kommons.io.readSignedVarLong
import ru.dbotthepony.kommons.io.readVarLong
import ru.dbotthepony.kommons.io.writeSignedVarLong
import ru.dbotthepony.kommons.io.writeVarLong
import ru.dbotthepony.kommons.util.Listenable
import ru.dbotthepony.kommons.util.ListenableDelegate
import ru.dbotthepony.kstarbound.math.Interpolator
import ru.dbotthepony.kstarbound.util.FloatSupplier
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.function.Consumer
import java.util.function.DoubleSupplier
import kotlin.math.roundToLong
// works solely with doubles, but networks as either float, double or fixed point
class FloatingNetworkedElement(private var value: Double = 0.0, val ops: Ops, val legacyOps: Ops = ops, val interpolator: Interpolator = Interpolator.NearestMiddle) : NetworkedElement(), ListenableDelegate<Double>, DoubleSupplier {
interface Ops {
fun write(data: DataOutputStream, value: Double)
fun read(data: DataInputStream): Double
fun areDifferent(a: Double, b: Double): Boolean {
return a != b
}
}
object DoubleOps : Ops {
override fun write(data: DataOutputStream, value: Double) {
data.writeDouble(value)
}
override fun read(data: DataInputStream): Double {
return data.readDouble()
}
}
object FloatOps : Ops {
override fun write(data: DataOutputStream, value: Double) {
data.writeFloat(value.toFloat())
}
override fun read(data: DataInputStream): Double {
return data.readFloat().toDouble()
}
override fun areDifferent(a: Double, b: Double): Boolean {
return a.toFloat() != b.toFloat()
}
}
data class FixedPointOps(val base: Double) : Ops {
override fun write(data: DataOutputStream, value: Double) {
data.writeSignedVarLong((value / base).roundToLong())
}
override fun read(data: DataInputStream): Double {
return data.readSignedVarLong() * base
}
override fun areDifferent(a: Double, b: Double): Boolean {
return (a / base).roundToLong() != (b / base).roundToLong()
}
}
private val queue = ArrayDeque<Pair<Double, Double>>()
var currentTime = 0.0
private set
var isInterpolating = false
private set
var extrapolation = 0.0
private set
private val valueListeners = Listenable.Impl<Double>()
override fun accept(t: Double) {
if (t != value) {
val old = value
value = t
if (ops.areDifferent(old, t))
bumpVersion()
if (isInterpolating) {
queue.clear()
queue.add(currentTime to t)
}
valueListeners.accept(t)
}
}
override fun addListener(listener: Consumer<Double>): Listenable.L {
return valueListeners.addListener(listener)
}
override fun get(): Double {
return value
}
override fun getAsDouble(): Double {
return value
}
fun getAsFloat(): Float {
return value.toFloat()
}
val float = FloatSupplier { getAsFloat() }
override fun readInitial(data: DataInputStream, isLegacy: Boolean) {
if (isLegacy) {
value = legacyOps.read(data)
} else {
value = ops.read(data)
}
queue.clear()
}
override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) {
if (queue.isNotEmpty())
(if (isLegacy) legacyOps else ops).write(data, queue.last().second)
else
(if (isLegacy) legacyOps else ops).write(data, value)
}
override fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean) {
val read = if (isLegacy) legacyOps.read(data) else ops.read(data)
if (isInterpolating) {
val realDelay = interpolationDelay + currentTime
if (queue.last().first > realDelay)
queue.clear()
queue.add(realDelay to read)
value = interpolated()
valueListeners.accept(value)
} else {
value = read
valueListeners.accept(read)
}
}
override fun writeDelta(data: DataOutputStream, remoteVersion: Long, isLegacy: Boolean) {
writeInitial(data, isLegacy)
}
override fun readBlankDelta(interpolationDelay: Double) {
if (isInterpolating) {
val actual = interpolationDelay + currentTime
val (last, lastPoint) = queue.last()
if (actual < last)
queue.clear()
queue.add(actual to lastPoint)
val old = value
value = interpolated()
if (old != value) {
valueListeners.accept(value)
}
}
}
override fun enableInterpolation(extrapolation: Double) {
if (!isInterpolating) {
isInterpolating = true
queue.clear()
queue.add(currentTime to value)
}
this.extrapolation = extrapolation
}
override fun disableInterpolation() {
if (isInterpolating) {
isInterpolating = false
if (queue.isNotEmpty()) {
value = queue.last().second
}
queue.clear()
}
}
override fun tickInterpolation(delta: Double) {
currentTime += delta
if (isInterpolating) {
while (queue.size > 2 && queue[1].first <= currentTime) {
queue.removeFirst()
}
val old = value
value = interpolated()
if (value != old) {
valueListeners.accept(value)
}
}
}
fun interpolated(time: Double = 0.0): Double {
check(isInterpolating) { "Not interpolating" }
check(queue.size >= 2) { "Interpolation queue is degenerate (only ${queue.size} points)" }
val actualTime = time + currentTime
if (actualTime < queue.first().first) {
// extrapolate into past?
val (time0, value0) = queue[0]
val (time1, value1) = queue[1]
return interpolator.interpolate(((actualTime - time0) / (time1 - time0)).coerceAtLeast(-extrapolation), value0, value1)
} else if (actualTime > queue.last().first) {
// extrapolate into future
val (time0, value0) = queue[queue.size - 2]
val (time1, value1) = queue[queue.size - 1]
return interpolator.interpolate(((actualTime - time1) / (time1 - time0)).coerceAtMost(extrapolation + 1.0), value0, value1)
} else {
// normal interpolation
for (i in 0 until queue.size - 1) {
val (time0, value0) = queue[i]
val (time1, value1) = queue[i + 1]
if (actualTime in time0 .. time1) {
return interpolator.interpolate((actualTime - time0) / (time1 - time0), value0, value1)
}
}
throw RuntimeException("unreachable code")
}
}
companion object {
/**
* Uses floats when asked to network for legacy protocol
*/
fun float(value: Double = 0.0): FloatingNetworkedElement {
return FloatingNetworkedElement(value, DoubleOps, FloatOps)
}
fun double(value: Double = 0.0): FloatingNetworkedElement {
return FloatingNetworkedElement(value, DoubleOps)
}
fun fixed(base: Double, value: Double = 0.0): FloatingNetworkedElement {
return FloatingNetworkedElement(value, FixedPointOps(base))
}
}
}

View File

@ -0,0 +1,154 @@
package ru.dbotthepony.kstarbound.network.syncher
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.writeVarInt
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.function.Consumer
import java.util.function.LongSupplier
class GroupElement() : NetworkedElement() {
constructor(element: NetworkedElement, vararg rest: NetworkedElement) : this() {
add(element)
rest.forEach { add(it) }
}
// element -> propagateInterpolation
private val elements = ArrayList<Pair<NetworkedElement, Boolean>>()
var isInterpolating = false
private set
var extrapolation = 0.0
private set
override fun specifyVersioner(versionCounter: LongSupplier) {
super.specifyVersioner(versionCounter)
elements.forEach { it.first.specifyVersioner(versionCounter) }
}
override fun enableInterpolation(extrapolation: Double) {
isInterpolating = true
this.extrapolation = extrapolation
elements.forEach { it.first.enableInterpolation(extrapolation) }
}
override fun disableInterpolation() {
isInterpolating = false
extrapolation = 0.0
elements.forEach { it.first.disableInterpolation() }
}
override fun tickInterpolation(delta: Double) {
elements.forEach { it.first.tickInterpolation(delta) }
}
fun add(element: NetworkedElement, propagateInterpolation: Boolean = true): GroupElement {
require(elements.none { it.first == element }) { "Already has element $element in $this" }
elements.add(element to propagateInterpolation)
if (propagateInterpolation) {
if (isInterpolating)
element.enableInterpolation(extrapolation)
else
element.disableInterpolation()
}
if (versionCounter != null) {
element.specifyVersioner(versionCounter!!)
}
element.listeners.addListener(Consumer {
this.version = this.version.coerceAtLeast(it)
})
this.version = this.version.coerceAtLeast(element.version)
return this
}
override fun readInitial(data: DataInputStream, isLegacy: Boolean) {
elements.forEach { it.first.readInitial(data, isLegacy) }
}
override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) {
elements.forEach { it.first.writeInitial(data, isLegacy) }
}
override fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean) {
check(elements.isNotEmpty()) { "No networked elements in this group" }
if (elements.size == 1) {
elements[0].first.readDelta(data, interpolationDelay, isLegacy)
} else {
var nextIndex = data.readVarInt()
// here goes more funk - when interpolating,
// elements missing from network message are interpolated
if (isInterpolating) {
// when interpolating, we can't just sparsingly read elements,
// since elements which are absent from delta must be updated too
// Things get ugly
for ((i, element) in elements.withIndex()) {
if (nextIndex == 0 || i < nextIndex - 1) {
element.first.readBlankDelta(interpolationDelay)
} else if (i == nextIndex - 1) {
element.first.readDelta(data, interpolationDelay, isLegacy)
nextIndex = data.readVarInt()
} else {
throw IllegalStateException("Networked element group indexes were written out of order")
}
}
} else {
while (nextIndex != 0) {
val element = elements.getOrNull(nextIndex - 1) ?: throw NoSuchElementException("Unknown networked element with index ${nextIndex - 1}!")
element.first.readDelta(data, interpolationDelay, isLegacy)
nextIndex = data.readVarInt()
}
}
}
}
override fun readBlankDelta(interpolationDelay: Double) {
if (isInterpolating) {
elements.forEach { it.first.readBlankDelta(interpolationDelay) }
}
}
override fun writeDelta(data: DataOutputStream, remoteVersion: Long, isLegacy: Boolean) {
check(elements.isNotEmpty()) { "No networked elements in this group" }
// here is where it gets funky, data structure
// is different for when there is only one element in group
// or multiple
if (elements.size == 1) {
// one element - pass through
elements[0].first.writeDelta(data, remoteVersion, isLegacy)
} else {
// otherwise, sequentially scan elements for updates
// and if element needs updating, write element's index as variable length integer;
// then write element itself
for ((i, element) in elements.withIndex()) {
if (element.first.hasChangedSince(remoteVersion)) {
data.writeVarInt(i + 1)
element.first.writeDelta(data, remoteVersion, isLegacy)
}
}
data.writeVarInt(0)
}
}
override fun hasChangedSince(version: Long): Boolean {
if (elements.isEmpty())
return false
return super.hasChangedSince(version)
}
override fun bumpVersion() {
elements.forEach { it.first.bumpVersion() }
}
}

View File

@ -0,0 +1,36 @@
package ru.dbotthepony.kstarbound.network.syncher
import com.google.gson.TypeAdapter
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.readByteArray
import ru.dbotthepony.kommons.io.writeByteArray
import ru.dbotthepony.kstarbound.json.BinaryJsonReader
import ru.dbotthepony.kstarbound.json.writeJsonElement
import java.io.DataInputStream
import java.io.DataOutputStream
class JsonCodec<V>(val adapter: TypeAdapter<V>, val wrapIntoArray: Boolean = false) : StreamCodec<V> {
override fun copy(value: V): V {
return value
}
override fun read(stream: DataInputStream): V {
if (wrapIntoArray) {
return adapter.read(BinaryJsonReader(FastByteArrayInputStream(stream.readByteArray())))
} else {
return adapter.read(BinaryJsonReader(stream))
}
}
override fun write(stream: DataOutputStream, value: V) {
if (wrapIntoArray) {
val output = FastByteArrayOutputStream()
DataOutputStream(output).writeJsonElement(adapter.toJsonTree(value))
stream.writeByteArray(output.array, 0, output.length)
} else {
stream.writeJsonElement(adapter.toJsonTree(value))
}
}
}

View File

@ -0,0 +1,81 @@
package ru.dbotthepony.kstarbound.network.syncher
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import ru.dbotthepony.kommons.io.writeByteArray
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.util.Objects
import java.util.function.LongSupplier
class MasterElement<E : NetworkedElement>(val upstream: E) : LongSupplier {
var version: Long = 0L
private set
override fun getAsLong(): Long {
return version
}
init {
upstream.specifyVersioner(this)
}
fun write(remoteVersion: Long = 0L, isLegacy: Boolean = false): Pair<ByteArrayList, Long> {
if (remoteVersion < 0L)
throw IllegalArgumentException("remote version is negative")
else if (remoteVersion == 0L) {
val output = FastByteArrayOutputStream()
val stream = DataOutputStream(output)
stream.write(1)
upstream.writeInitial(stream, isLegacy)
return ByteArrayList.wrap(output.array, output.length) to ++version
} else {
val output = FastByteArrayOutputStream()
val stream = DataOutputStream(output)
if (upstream.hasChangedSince(remoteVersion)) {
stream.write(0)
upstream.writeDelta(stream, remoteVersion, isLegacy)
return ByteArrayList.wrap(output.array, output.length) to ++version
}
return ByteArrayList() to version
}
}
fun write(stream: OutputStream, remoteVersion: Long = 0L, isLegacy: Boolean = false): Long {
val (data, version) = write(remoteVersion, isLegacy)
stream.writeByteArray(data.elements(), 0, data.size)
return version
}
fun read(data: InputStream, interpolationTime: Double = 0.0, isLegacy: Boolean = false) {
if (data.available() == 0) {
upstream.readBlankDelta(interpolationTime)
} else {
val stream = DataInputStream(data)
if (stream.readBoolean()) {
upstream.readInitial(stream, isLegacy)
} else {
upstream.readDelta(stream, interpolationTime, isLegacy)
}
}
}
fun read(data: ByteArray, interpolationTime: Double = 0.0, isLegacy: Boolean = false) {
return read(FastByteArrayInputStream(data), interpolationTime, isLegacy)
}
fun read(data: ByteArray, offset: Int, length: Int, interpolationTime: Double = 0.0, isLegacy: Boolean = false) {
Objects.checkFromIndexSize(offset, length, data.size)
return read(FastByteArrayInputStream(data, offset, length), interpolationTime, isLegacy)
}
fun read(data: ByteArrayList, interpolationTime: Double = 0.0, isLegacy: Boolean = false) {
return read(data.elements(), 0, data.size, interpolationTime, isLegacy)
}
}

View File

@ -0,0 +1,97 @@
package ru.dbotthepony.kstarbound.network.syncher
import ru.dbotthepony.kommons.util.Listenable
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.function.LongSupplier
// While I already have created my own syncher as part of Kommons (for minecraft mods),
// Starbound networking is done a bit differently.
// Most notably, networked variables employ interpolation and extrapolation,
// have different network data format when networking for first time and require
// strict field order when receiving them.
// Also, due to structure of networked delta data, most "lazy" optimizations can not
// be applied, such as dirty lists. If interpolation is enabled, all elements must either network their
// delta, or network "nothing changed, interpolate"
// But on second looks, versioning vs event has its own advantages, namely,
// IF data is updated way more frequent than it is sent to clients, then
// versioning approach is faster than event approach (because on each data update
// we don't need to notify all remote networkers about this)
// infrequent updates: Event
// very frequent updates: Polling with versioning
abstract class NetworkedElement {
// Full store / load of the entire element.
abstract fun readInitial(data: DataInputStream, isLegacy: Boolean)
abstract fun writeInitial(data: DataOutputStream, isLegacy: Boolean)
/**
* Read a delta written by writeNetDelta. 'interpolationTime' is the time in
* the future that data from this delta should be delayed and smoothed into,
* if interpolation is enabled.
*/
abstract fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean)
/**
* Write all the state changes that have happened since (and including)
* fromVersion. The normal way to use this would be to call writeDelta with
* the version at the time of the *last* call to writeDelta, + 1. If
* fromVersion is 0, this will always write the full state. Should return
* true if a delta was needed and was written to DataStream, false otherwise.
*/
abstract fun writeDelta(data: DataOutputStream, remoteVersion: Long, isLegacy: Boolean)
/**
* When extrapolating, it is important to notify when a delta WOULD have been
* received even if no deltas are produced, so no extrapolation takes place.
*/
abstract fun readBlankDelta(interpolationDelay: Double = 0.0)
/**
* Enables interpolation mode. If interpolation mode is enabled, then
* NetElements will delay presenting incoming delta data for the
* 'interpolationTime' parameter given in readNetDelta, and smooth between
* received values. When interpolation is enabled, tickNetInterpolation must
* be periodically called to smooth values forward in time. If
* extrapolationHint is given, this may be used as a hint for the amount of
* time to extrapolate forward if no deltas are received.
*/
abstract fun enableInterpolation(extrapolation: Double = 0.0)
abstract fun disableInterpolation()
abstract fun tickInterpolation(delta: Double)
// A network of NetElements will have a shared monotinically increasing
// NetElementVersion. When elements are updated, they will mark the version
// number at the time they are updated so that a delta can be constructed
// that contains only changes since any past version.
var version: Long = 0L
protected set(value) {
if (field != value) {
require(value > field) { "Downgrading element version from $field to $value" }
field = value
listeners.accept(value)
}
}
open fun hasChangedSince(version: Long): Boolean {
return this.version >= version
}
val listeners = Listenable.Impl<Long>()
var versionCounter: LongSupplier? = null
protected set
open fun specifyVersioner(versionCounter: LongSupplier) {
this.versionCounter = versionCounter
}
/**
* Marks this networked element dirty
*/
open fun bumpVersion() {
version = versionCounter?.asLong ?: version
}
}

View File

@ -37,6 +37,8 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
lateinit var shipWorld: ServerWorld lateinit var shipWorld: ServerWorld
private set private set
var skyVersion = 0L
init { init {
connectionID = server.nextConnectionID.incrementAndGet() connectionID = server.nextConnectionID.incrementAndGet()
} }
@ -86,6 +88,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
override fun setupNative() { override fun setupNative() {
super.setupNative() super.setupNative()
shipChunkSource = IChunkSource.Void
} }
fun receiveShipChunks(chunks: Map<ByteKey, KOptional<ByteArray>>) { fun receiveShipChunks(chunks: Map<ByteKey, KOptional<ByteArray>>) {
@ -219,13 +222,15 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
} }
override fun inGame() { override fun inGame() {
// server.playerInGame(this) if (!isLegacy) {
server.playerInGame(this)
LOGGER.info("Initializing ship world for $this") } else {
shipWorld = ServerWorld(server, WorldGeometry(Vector2i(2048, 2048), false, false)) LOGGER.info("Initializing ship world for $this")
shipWorld.addChunkSource(shipChunkSource) shipWorld = ServerWorld(server, WorldGeometry(Vector2i(2048, 2048), false, false))
shipWorld.thread.start() shipWorld.addChunkSource(shipChunkSource)
shipWorld.acceptPlayer(this) shipWorld.thread.start()
shipWorld.acceptPlayer(this)
}
} }
companion object { companion object {

View File

@ -49,9 +49,12 @@ class ServerWorld(
player.world = this player.world = this
if (player.isLegacy) { if (player.isLegacy) {
val (skyData, skyVersion) = sky.networkedGroup.write(isLegacy = true)
player.skyVersion = skyVersion
player.sendAndFlush(WorldStartPacket( player.sendAndFlush(WorldStartPacket(
templateData = WorldTemplate(geometry).toJson(true), templateData = WorldTemplate(geometry).toJson(true),
skyData = ByteArray(0), skyData = skyData.toByteArray(),
weatherData = ByteArray(0), weatherData = ByteArray(0),
playerStart = playerSpawnPosition, playerStart = playerSpawnPosition,
playerRespawn = playerSpawnPosition, playerRespawn = playerSpawnPosition,

View File

@ -0,0 +1,12 @@
package ru.dbotthepony.kstarbound.util
import java.util.function.Supplier
fun interface FloatSupplier : Supplier<Float> {
@Deprecated("Use type specific method instead", replaceWith = ReplaceWith("this.getAsFloat()"))
override fun get(): Float {
return getAsFloat()
}
fun getAsFloat(): Float
}

View File

@ -0,0 +1,79 @@
package ru.dbotthepony.kstarbound.world
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import ru.dbotthepony.kommons.io.VarIntValueCodec
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kommons.util.value
import ru.dbotthepony.kstarbound.defs.world.SkyParameters
import ru.dbotthepony.kstarbound.defs.world.SkyType
import ru.dbotthepony.kstarbound.defs.world.WarpPhase
import ru.dbotthepony.kstarbound.network.syncher.BasicNetworkedElement
import ru.dbotthepony.kstarbound.network.syncher.GroupElement
import ru.dbotthepony.kstarbound.network.syncher.MasterElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
import ru.dbotthepony.kstarbound.network.syncher.networkedDouble
import ru.dbotthepony.kstarbound.network.syncher.networkedEnumStupid
import ru.dbotthepony.kstarbound.network.syncher.networkedFloat
import ru.dbotthepony.kstarbound.network.syncher.networkedJson
import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt
import ru.dbotthepony.kstarbound.network.syncher.networkedUnsignedInt
import ru.dbotthepony.kstarbound.network.syncher.networkedVec2f
class Sky() {
private val skyParametersNetState = networkedJson(SkyParameters())
private val skyTypeNetState = networkedEnumStupid(SkyType.ORBITAL)
private val timeNetState = networkedDouble()
private val flyingTypeNetState = networkedUnsignedInt()
private val enterHyperspaceNetState = networkedBoolean()
private val startInWarpNetState = networkedBoolean()
private val worldMoveNetState = networkedEnumStupid(WarpPhase.MAINTAIN)
private val starMoveNetState = networkedVec2f()
private val warpPhaseNetState = networkedVec2f()
private val flyingTimerNetState = networkedFloat()
var skyType by skyTypeNetState
private set
var time by timeNetState
private set
var flyingType by flyingTypeNetState
private set
var enterHyperspace by enterHyperspaceNetState
private set
var startInWarp by startInWarpNetState
private set
var worldMove by worldMoveNetState
private set
var starMove by starMoveNetState
private set
var warpPhase by warpPhaseNetState
private set
var flyingTimer by flyingTimerNetState
private set
val networkedGroup = MasterElement(GroupElement(
skyParametersNetState,
skyTypeNetState,
timeNetState,
flyingTypeNetState,
enterHyperspaceNetState,
startInWarpNetState,
worldMoveNetState,
starMoveNetState,
warpPhaseNetState,
flyingTimerNetState,
))
constructor(parameters: SkyParameters, inOrbit: Boolean) : this() {
skyParametersNetState.value = parameters.copy()
if (inOrbit) {
skyType = SkyType.ORBITAL
} else {
skyType = parameters.skyType
}
}
}

View File

@ -34,6 +34,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val background = TileView.Background(this) val background = TileView.Background(this)
val foreground = TileView.Foreground(this) val foreground = TileView.Foreground(this)
val mailbox = MailboxExecutorService() val mailbox = MailboxExecutorService()
val sky = Sky()
override fun getCellDirect(x: Int, y: Int): AbstractCell { override fun getCellDirect(x: Int, y: Int): AbstractCell {
if (!geometry.x.inBoundsCell(x) || !geometry.y.inBoundsCell(y)) return AbstractCell.NULL if (!geometry.x.inBoundsCell(x) || !geometry.y.inBoundsCell(y)) return AbstractCell.NULL

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.test
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import ru.dbotthepony.kstarbound.util.HashTableInterner import ru.dbotthepony.kstarbound.util.HashTableInterner
import ru.dbotthepony.kstarbound.util.random.MWCRandom
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
@ -16,7 +17,7 @@ object InternerTest {
for (i in 0 until 8) { for (i in 0 until 8) {
threads.add(Thread { threads.add(Thread {
val rand = RandomGenerator.of("Xoroshiro128PlusPlus") val rand = MWCRandom()
for (i2 in 0 until 100_000) { for (i2 in 0 until 100_000) {
val v = rand.nextInt() val v = rand.nextInt()

View File

@ -0,0 +1,126 @@
package ru.dbotthepony.kstarbound.test
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import ru.dbotthepony.kommons.io.Vector2fCodec
import ru.dbotthepony.kommons.vector.Vector2f
import ru.dbotthepony.kstarbound.network.syncher.BasicNetworkedElement
import ru.dbotthepony.kstarbound.network.syncher.EventCounterElement
import ru.dbotthepony.kstarbound.network.syncher.FloatingNetworkedElement
import ru.dbotthepony.kstarbound.network.syncher.GroupElement
import ru.dbotthepony.kstarbound.network.syncher.MasterElement
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
import ru.dbotthepony.kstarbound.network.syncher.networkedDouble
import ru.dbotthepony.kstarbound.network.syncher.networkedEnum
import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint
import ru.dbotthepony.kstarbound.network.syncher.networkedFloat
import ru.dbotthepony.kstarbound.network.syncher.networkedPointer
import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt
import ru.dbotthepony.kstarbound.network.syncher.networkedUnsignedInt
// reflects tests defined in original sources
// because they are quite comprehensive
object NetworkedElementTests {
enum class TestEnum {
VALUE1,
VALUE2,
VALUE3;
}
@Test
@DisplayName("NetworkedElement")
fun testAllTypes() {
val masterField1 = networkedSignedInt()
val masterField2 = networkedUnsignedInt()
val masterField3 = networkedPointer()
val masterField4 = networkedFloat()
val masterField5 = networkedDouble()
val masterField6 = networkedFixedPoint(0.01)
val masterField7 = networkedFixedPoint(0.01) // no difference
val masterField8 = networkedBoolean()
val masterField9 = networkedEnum(TestEnum.VALUE1)
val masterField10 = EventCounterElement()
val masterField11 = BasicNetworkedElement(Vector2f.ZERO, Vector2fCodec)
val master = MasterElement(GroupElement())
master.upstream.add(masterField1)
master.upstream.add(masterField2)
master.upstream.add(masterField3)
master.upstream.add(masterField4)
master.upstream.add(masterField5)
master.upstream.add(masterField6)
master.upstream.add(masterField7)
master.upstream.add(masterField8)
master.upstream.add(masterField9)
master.upstream.add(masterField10)
master.upstream.add(masterField11)
masterField1.accept(567)
masterField2.accept(17000)
masterField3.accept(22222)
masterField4.accept(1.55)
masterField5.accept(1.12345678910111213)
masterField6.accept(2000.62)
masterField7.accept(2000.62)
masterField8.accept(true)
masterField9.accept(TestEnum.VALUE2)
masterField10.trigger()
masterField11.accept(Vector2f(2.0f, 2.0f))
assertEquals(567, masterField1.get())
assertEquals(17000, masterField2.get())
assertEquals(22222, masterField3.get())
assertEquals(1.55, masterField4.get())
assertEquals(1.12345678910111213, masterField5.get())
assertEquals(2000.62, masterField6.get())
assertEquals(2000.62, masterField7.get())
assertEquals(true, masterField8.get())
assertEquals(TestEnum.VALUE2, masterField9.get())
assertTrue(masterField10.pullOccurred())
assertEquals(Vector2f(2.0f, 2.0f), masterField11.get())
val slaveField1 = networkedSignedInt()
val slaveField2 = networkedUnsignedInt()
val slaveField3 = networkedPointer()
val slaveField4 = networkedFloat()
val slaveField5 = networkedDouble()
val slaveField6 = networkedFixedPoint(0.01)
val slaveField7 = networkedFixedPoint(0.01) // no difference
val slaveField8 = networkedBoolean()
val slaveField9 = networkedEnum(TestEnum.VALUE1)
val slaveField10 = EventCounterElement()
val slaveField11 = BasicNetworkedElement(Vector2f.ZERO, Vector2fCodec)
val slave = MasterElement(GroupElement())
slave.upstream.add(slaveField1)
slave.upstream.add(slaveField2)
slave.upstream.add(slaveField3)
slave.upstream.add(slaveField4)
slave.upstream.add(slaveField5)
slave.upstream.add(slaveField6)
slave.upstream.add(slaveField7)
slave.upstream.add(slaveField8)
slave.upstream.add(slaveField9)
slave.upstream.add(slaveField10)
slave.upstream.add(slaveField11)
val result = master.write().first
slave.read(FastByteArrayInputStream(result.array, 0, result.length))
assertEquals(567, slaveField1.get())
assertEquals(17000, slaveField2.get())
assertEquals(22222, slaveField3.get())
assertEquals(1.55f, slaveField4.getAsFloat())
assertEquals(1.12345678910111213, slaveField5.get())
assertEquals(2000.62f, slaveField6.getAsFloat())
assertEquals(2000.62f, slaveField7.getAsFloat()) // due to precision fuckery, 2000.62 turns into 2000.620000001
assertEquals(true, slaveField8.get())
assertEquals(TestEnum.VALUE2, slaveField9.get())
assertTrue(slaveField10.pullOccurred())
assertEquals(Vector2f(2.0f, 2.0f), slaveField11.get())
}
}