diff --git a/gradle.properties b/gradle.properties index 90133a14..064bc69d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m kotlinVersion=1.9.10 kotlinCoroutinesVersion=1.8.0 -kommonsVersion=2.9.20 +kommonsVersion=2.9.21 ffiVersion=2.2.13 lwjglVersion=3.3.0 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt index d84a166e..01da1dad 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt @@ -10,6 +10,7 @@ import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig 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.json.mapAdapter import ru.dbotthepony.kstarbound.json.pairSetAdapter @@ -53,6 +54,9 @@ object GlobalDefaults { var bushDamage by Delegates.notNull() private set + var sky by Delegates.notNull() + private set + private object EmptyTask : ForkJoinTask() { private fun readResolve(): Any = EmptyTask override fun getRawResult() { @@ -99,12 +103,14 @@ object GlobalDefaults { tasks.add(load("/terrestrial_worlds.config", ::terrestrialWorlds)) tasks.add(load("/asteroids_worlds.config", ::asteroidWorlds)) 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/treeDamage.config", ::treeDamage)) tasks.add(load("/plants/bushDamage.config", ::bushDamage)) + tasks.add(load("/dungeon_worlds.config", ::dungeonWorlds, Starbound.gson.mapAdapter())) + return tasks } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IItemDefinition.kt index 54bdf69b..f13161f5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IItemDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IItemDefinition.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.defs.item.api +import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.defs.AssetPath import ru.dbotthepony.kstarbound.defs.IThingWithDescription @@ -37,7 +38,7 @@ interface IItemDefinition : IThingWithDescription { /** * Иконка в инвентаре, относительный и абсолютный пути */ - val inventoryIcon: List? + val inventoryIcon: Either>? /** * Теги предмета diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/ItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/ItemDefinition.kt index cbedd824..937a160d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/ItemDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/ItemDefinition.kt @@ -1,11 +1,13 @@ package ru.dbotthepony.kstarbound.defs.item.impl import com.google.common.collect.ImmutableList +import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.defs.AssetPath import ru.dbotthepony.kstarbound.defs.IThingWithDescription import ru.dbotthepony.kstarbound.defs.ThingDescription 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.ItemRarity import ru.dbotthepony.kstarbound.json.builder.JsonFactory @@ -20,7 +22,7 @@ data class ItemDefinition( override val price: Long = 0, override val rarity: ItemRarity = ItemRarity.COMMON, override val category: String? = null, - override val inventoryIcon: ImmutableList? = null, + override val inventoryIcon: Either>? = null, override val itemTags: ImmutableList = ImmutableList.of(), override val learnBlueprintsOnPickup: ImmutableList> = ImmutableList.of(), override val maxStack: Long = 9999L, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt index 021bf35e..378eed22 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt @@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.defs.tile import com.google.common.collect.ImmutableList import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.defs.IThingWithDescription @@ -16,8 +17,8 @@ data class TileDefinition( val materialName: String, val particleColor: RGBAColor? = null, val itemDrop: String? = null, - val footstepSound: ImmutableList = ImmutableList.of(), - val miningSounds: ImmutableList = ImmutableList.of(), + val footstepSound: Either, String> = Either.left(ImmutableList.of()), + val miningSounds: Either, String> = Either.left(ImmutableList.of()), val blocksLiquidFlow: Boolean = true, val soil: Boolean = false, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt index 641ac61f..4c88d0ae 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt @@ -1,9 +1,12 @@ package ru.dbotthepony.kstarbound.defs.world +import com.google.common.collect.ImmutableList import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kommons.io.StreamCodec import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.defs.AssetPath import ru.dbotthepony.kstarbound.io.readColor import ru.dbotthepony.kstarbound.io.writeColor import ru.dbotthepony.kstarbound.json.builder.IStringSerializable @@ -20,14 +23,22 @@ enum class SkyType { ATMOSPHERELESS, ORBITAL, WARP, - SPACE + SPACE; + + companion object { + val CODEC = StreamCodec.Enum(SkyType::class.java) + } } enum class FlyingType { NONE, DISEMBARKING, WARP, - ARRIVING + ARRIVING; + + companion object { + val CODEC = StreamCodec.Enum(FlyingType::class.java) + } } 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>) -class SkyParameters() { - var skyType = SkyType.BARREN - var seed = 0L - var dayLength: Double? = null - var horizonClouds = false - var skyColoring: Either = Either.right(RGBAColor.BLACK) - var spaceLevel: Double? = null - var surfaceLevel: Double? = null - var nearbyPlanet: Pair>, Vector2d>? = null - +@JsonFactory +data class SkyParameters( + var skyType: SkyType = SkyType.BARREN, + var seed: Long = 0L, + var dayLength: Double? = null, + var horizonClouds: Boolean = false, + var skyColoring: Either = Either.left(SkyColoring()), + var spaceLevel: Double? = null, + var surfaceLevel: Double? = null, + var nearbyPlanet: Pair>, Vector2d>? = null, +) { companion object { suspend fun create(coordinate: UniversePos, universe: Universe): SkyParameters { if (coordinate.isSystem) @@ -163,3 +175,13 @@ class SkyParameters() { } } } + +data class SkyGlobalConfig( + val stars: Stars, +) { + data class Stars( + val frames: Int, + val list: ImmutableList, + val hyperlist: ImmutableList, + ) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/RootBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/RootBindings.kt index 00ea102a..0dea446c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/RootBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/RootBindings.kt @@ -215,13 +215,13 @@ private fun materialMiningSound(context: ExecutionContext, arguments: ArgumentIt val tile = lookup(Registries.tiles, arguments.nextAny()) val mod = lookup(Registries.tiles, arguments.nextOptionalAny(null)) - if (mod != null && mod.value.miningSounds.isNotEmpty()) { - context.returnBuffer.setTo(mod.value.miningSounds.random()) + if (mod != null && mod.value.miningSounds.map({ it.isNotEmpty() }, { true })) { + context.returnBuffer.setTo(mod.value.miningSounds.map({ it.random() }, { it })) return } - if (tile != null && tile.value.miningSounds.isNotEmpty()) { - context.returnBuffer.setTo(tile.value.miningSounds.random()) + if (tile != null && tile.value.miningSounds.map({ it.isNotEmpty() }, { true })) { + context.returnBuffer.setTo(tile.value.miningSounds.map({ it.random() }, { it })) return } @@ -233,13 +233,13 @@ private fun materialFootstepSound(context: ExecutionContext, arguments: Argument val tile = lookup(Registries.tiles, arguments.nextAny()) val mod = lookup(Registries.tiles, arguments.nextOptionalAny(null)) - if (mod != null && mod.value.footstepSound.isNotEmpty()) { - context.returnBuffer.setTo(mod.value.footstepSound.random()) + if (mod != null && mod.value.footstepSound.map({ it.isNotEmpty() }, { true })) { + context.returnBuffer.setTo(mod.value.footstepSound.map({ it.random() }, { it })) return } - if (tile != null && tile.value.footstepSound.isNotEmpty()) { - context.returnBuffer.setTo(tile.value.footstepSound.random()) + if (tile != null && tile.value.footstepSound.map({ it.isNotEmpty() }, { true })) { + context.returnBuffer.setTo(tile.value.footstepSound.map({ it.random() }, { it })) return } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Interpolator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Interpolator.kt new file mode 100644 index 00000000..d424c8e1 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Interpolator.kt @@ -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 + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/PeriodicFunction.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/PeriodicFunction.kt index f7830d4f..4970a000 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/PeriodicFunction.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/PeriodicFunction.kt @@ -12,16 +12,6 @@ data class PeriodicFunction( val periodVariance: 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 timerMax = 1.0 @@ -60,6 +50,6 @@ data class PeriodicFunction( } fun sinValue(): Double { - return value(Sin) + return value(Interpolator.Sin) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/API.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/API.kt index 9a55ed68..496d5174 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/API.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/API.kt @@ -75,8 +75,16 @@ enum class ConnectionType { MEMORY; } -fun interface IPacketReader { +fun interface IPacketReaderDetailed { + fun read(stream: DataInputStream, isLegacy: Boolean, side: ConnectionSide): T +} + +fun interface IPacketReader : IPacketReaderDetailed { fun read(stream: DataInputStream, isLegacy: Boolean): T + + override fun read(stream: DataInputStream, isLegacy: Boolean, side: ConnectionSide): T { + return read(stream, isLegacy) + } } interface IPacket { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt index a320f7b7..97bef3d5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt @@ -73,6 +73,8 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : isLegacy = false isConnected = true + + inGame() } protected open fun onChannelClosed() { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/JsonRPC.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/JsonRPC.kt index c717ed11..95df22a4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/JsonRPC.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/JsonRPC.kt @@ -25,13 +25,15 @@ import kotlin.concurrent.withLock class JsonRPC { enum class Command { REQUEST, RESPONSE, FAIL; + + val jsonName = name.lowercase() } data class Entry(val command: Command, val id: Int, val handler: KOptional, val arguments: KOptional) { fun write(stream: DataOutputStream, isLegacy: Boolean) { if (isLegacy) { stream.writeJsonElement(JsonObject().also { - it["command"] = command.name.lowercase() + it["command"] = command.jsonName it["id"] = id handler.ifPresent { v -> it["handler"] = 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 { val data = stream.readJsonElement() 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 handler = KOptional.ofNullable(data["handler"]?.asString) 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 { @@ -111,8 +113,13 @@ class JsonRPC { try { when (entry.command) { Command.REQUEST -> { - val handler = handlers[entry.handler.value] ?: throw IllegalArgumentException("No such handler ${entry.handler.value}") - pendingWrite.add(Entry(Command.RESPONSE, entry.id, KOptional(), KOptional(handler(entry.arguments.value)))) + val handler = handlers[entry.handler.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 -> { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt index c3fc5036..ca71d81d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt @@ -50,12 +50,12 @@ class PacketRegistry(val isLegacy: Boolean) { private val missingNames = Int2ObjectArrayMap() private val clazz2Type = Reference2ObjectOpenHashMap, Type<*>>() - private data class Type(val id: Int, val type: KClass, val factory: IPacketReader, val direction: PacketDirection) + private data class Type(val id: Int, val type: KClass, val factory: IPacketReaderDetailed, val direction: PacketDirection) val size: Int get() = packets.size - private fun add(type: KClass, reader: IPacketReader, direction: PacketDirection = PacketDirection.get(type)): PacketRegistry { + private fun add(type: KClass, reader: IPacketReaderDetailed, direction: PacketDirection = PacketDirection.get(type)): PacketRegistry { if (packets.size >= 255) 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) } + private inline fun add(reader: IPacketReaderDetailed, direction: PacketDirection = PacketDirection.get(T::class)): PacketRegistry { + return add(T::class, reader, direction) + } + private inline fun 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) { @@ -154,12 +158,16 @@ class PacketRegistry(val isLegacy: Boolean) { // legacy protocol allows to stitch multiple packets of same type together without // 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 { - ctx.fireChannelRead(readingType!!.factory.read(DataInputStream(stream), isLegacy)) + ctx.fireChannelRead(readingType!!.factory.read(DataInputStream(stream), isLegacy, side)) } catch (err: Throwable) { LOGGER.error("Error while reading incoming packet from network (type ${readingType!!.id}; ${readingType!!.type})", err) } + + if (!isLegacy) break } stream.close() @@ -219,8 +227,8 @@ class PacketRegistry(val isLegacy: Boolean) { val stream = FastByteArrayOutputStream() (msg as IPacket).write(DataOutputStream(stream), isLegacy) - if (isLegacy) - check(stream.length > 0) { "Packet $msg didn't write any data to network, this is not allowed by legacy protocol" } + if (isLegacy && stream.length == 0) + throw IllegalStateException("Packet $msg didn't write any data to network, this is not allowed by legacy protocol") if (stream.length >= 512) { // compress @@ -228,6 +236,7 @@ class PacketRegistry(val isLegacy: Boolean) { val buffers = ByteArrayList(1024) val buffer = ByteArray(1024) deflater.setInput(stream.array, 0, stream.length) + deflater.finish() while (!deflater.needsInput()) { val deflated = deflater.deflate(buffer) @@ -369,7 +378,7 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.skip("SetDungeonBreathable") LEGACY.skip("SetPlayerStart") LEGACY.skip("FindUniqueEntityResponse") - LEGACY.add(PongPacket) + LEGACY.add(PongPacket::read) // Packets sent world client -> world server LEGACY.skip("ModifyTileList") @@ -382,7 +391,7 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.skip("WorldClientStateUpdate") LEGACY.skip("FindUniqueEntity") LEGACY.skip("WorldStartAcknowledge") - LEGACY.add(PingPacket) + LEGACY.add(PingPacket::read) // Packets sent bidirectionally between world client and world server LEGACY.skip("EntityCreate") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ClientContextUpdatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ClientContextUpdatePacket.kt index 81adf72d..3fea8ba6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ClientContextUpdatePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ClientContextUpdatePacket.kt @@ -17,6 +17,8 @@ import ru.dbotthepony.kommons.io.writeMap import ru.dbotthepony.kommons.io.writeVarInt import ru.dbotthepony.kommons.util.KOptional 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.IServerPacket import ru.dbotthepony.kstarbound.network.JsonRPC @@ -34,23 +36,31 @@ class ClientContextUpdatePacket( ) : IClientPacket, IServerPacket { override fun write(stream: DataOutputStream, isLegacy: Boolean) { if (isLegacy) { - // this is stupid - run { - val wrap = FastByteArrayOutputStream() - DataOutputStream(wrap).writeCollection(rpcEntries) { it.write(this, true) } - stream.writeVarInt(wrap.length) - stream.write(wrap.array, 0, wrap.length) - } + if (!shipChunks.isPresent && !networkedVars.isPresent) { + // client to server + stream.writeCollection(rpcEntries) { it.write(this, true) } + } else { + // server to client + // this is so dumb + val wrap2 = FastByteArrayOutputStream() - shipChunks.ifPresent { - val wrap = FastByteArrayOutputStream() - DataOutputStream(wrap).writeMap(it, { it.write(this) }, { writeKOptional(it) { writeByteArray(it) } }) - stream.writeByteArray(wrap.array, 0, wrap.length) - } + run { + val wrap = FastByteArrayOutputStream() + DataOutputStream(wrap).writeCollection(rpcEntries) { it.write(this, true) } + wrap2.writeByteArray(wrap.array, 0, wrap.length) + } - networkedVars.ifPresent { - stream.writeVarInt(it.size) - stream.write(it.elements(), 0, it.size) + shipChunks.ifPresent { + val wrap = FastByteArrayOutputStream() + 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 { stream.writeCollection(rpcEntries) { it.write(this, false) } @@ -75,16 +85,25 @@ class ClientContextUpdatePacket( } companion object { - fun read(stream: DataInputStream, isLegacy: Boolean): ClientContextUpdatePacket { + fun read(stream: DataInputStream, isLegacy: Boolean, side: ConnectionSide): ClientContextUpdatePacket { if (isLegacy) { // 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( - DataInputStream(FastByteArrayInputStream(rpc)).readCollection { JsonRPC.Entry.legacy(this) }, - if (stream.available() > 0) KOptional(stream.readMap({ readByteKey() }, { readKOptional { readByteArray() } })) else KOptional(), - if (stream.available() > 0) KOptional(ByteArrayList.wrap(stream.readByteArray())) else KOptional(), - ) + return ClientContextUpdatePacket( + DataInputStream(FastByteArrayInputStream(rpc)).readCollection { JsonRPC.Entry.legacy(this) }, + if (wrap.available() > 0) KOptional(wrap.readMap({ readByteKey() }, { readKOptional { readByteArray() } })) else KOptional(), + if (wrap.available() > 0) KOptional(ByteArrayList.wrap(wrap.readByteArray())) else KOptional(), + ) + } } else { return ClientContextUpdatePacket( stream.readCollection { JsonRPC.Entry.native(this) }, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/PingPong.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/PingPong.kt index dc8a4e81..ee6b60e6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/PingPong.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/PingPong.kt @@ -4,6 +4,7 @@ import ru.dbotthepony.kstarbound.client.ClientConnection import ru.dbotthepony.kstarbound.network.IClientPacket import ru.dbotthepony.kstarbound.network.IServerPacket import ru.dbotthepony.kstarbound.server.ServerConnection +import java.io.DataInputStream import java.io.DataOutputStream object PongPacket : IClientPacket { @@ -14,6 +15,11 @@ object PongPacket : IClientPacket { override fun play(connection: ClientConnection) { TODO("Not yet implemented") } + + fun read(stream: DataInputStream, isLegacy: Boolean): PongPacket { + if (isLegacy) stream.readBoolean() + return PongPacket + } } object PingPacket : IServerPacket { @@ -22,6 +28,11 @@ object PingPacket : IServerPacket { } override fun play(connection: ServerConnection) { - connection.send(PongPacket) + connection.sendAndFlush(PongPacket) + } + + fun read(stream: DataInputStream, isLegacy: Boolean): PingPacket { + if (isLegacy) stream.readBoolean() + return PingPacket } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/WorldStartPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/WorldStartPacket.kt index 6a50fc41..11a55347 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/WorldStartPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/WorldStartPacket.kt @@ -24,10 +24,16 @@ import java.io.DataInputStream import java.io.DataOutputStream class WorldStartPacket( - val templateData: JsonElement, val skyData: ByteArray, val weatherData: ByteArray, - val playerStart: Vector2d, val playerRespawn: Vector2d, val respawnInWorld: Boolean, - val dungeonGravity: Map, val dungeonBreathable: Map, - val protectedDungeonIDs: Set, val worldProperties: JsonElement, val connectionID: Int, + val templateData: JsonElement, + val skyData: ByteArray, + val weatherData: ByteArray, + val playerStart: Vector2d, + val playerRespawn: Vector2d, + val respawnInWorld: Boolean, + val worldProperties: JsonElement, + val dungeonGravity: Map, + val dungeonBreathable: Map, + val protectedDungeonIDs: Set, val connectionID: Int, val localInterpolationMode: Boolean, ) : IClientPacket { 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(), 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() }, { readBoolean() }, ::Int2ObjectOpenHashMap) else stream.readMap({ readInt() }, { readBoolean() }, { Int2BooleanAVLTreeMap() }), if (isLegacy) stream.readCollection({ readUnsignedShort() }, { IntAVLTreeSet() }) else stream.readCollection({ readInt() }, { IntAVLTreeSet() }), - stream.readJsonElement(), stream.readUnsignedShort(), stream.readBoolean() ) @@ -54,6 +60,7 @@ class WorldStartPacket( stream.writeStruct2f(playerStart.toFloatVector()) stream.writeStruct2f(playerRespawn.toFloatVector()) stream.writeBoolean(respawnInWorld) + stream.writeJsonElement(worldProperties) stream.writeMap(dungeonGravity, { writeShort(it) }, { writeFloat(it.y.toFloat()) }) stream.writeMap(dungeonBreathable, { writeShort(it) }, { writeBoolean(it) }) stream.writeCollection(protectedDungeonIDs) { writeShort(it) } @@ -61,12 +68,12 @@ class WorldStartPacket( stream.writeStruct2d(playerStart) stream.writeStruct2d(playerRespawn) stream.writeBoolean(respawnInWorld) + stream.writeJsonElement(worldProperties) stream.writeMap(dungeonGravity, { writeInt(it) }, { writeStruct2d(it) }) stream.writeMap(dungeonBreathable, { writeInt(it) }, { writeBoolean(it) }) stream.writeCollection(protectedDungeonIDs) { writeInt(it) } } - stream.writeJsonElement(worldProperties) stream.writeShort(connectionID) stream.writeBoolean(localInterpolationMode) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/BasicNetworkedElement.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/BasicNetworkedElement.kt new file mode 100644 index 00000000..81ee4baf --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/BasicNetworkedElement.kt @@ -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(private var value: TYPE, val codec: StreamCodec, val legacyCodec: StreamCodec, val toLegacy: (TYPE) -> LEGACY, val fromLegacy: (LEGACY) -> TYPE) : NetworkedElement(), ListenableDelegate { + protected val valueListeners = Listenable.Impl() + protected val queue = LinkedList>() + 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): 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) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/EventCounterElement.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/EventCounterElement.kt new file mode 100644 index 00000000..a8ecb6a4 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/EventCounterElement.kt @@ -0,0 +1,45 @@ +package ru.dbotthepony.kstarbound.network.syncher + +import java.io.DataInputStream +import java.util.function.Consumer + +class EventCounterElement : BasicNetworkedElement(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 + }) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/Factories.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/Factories.kt new file mode 100644 index 00000000..1dfa3b4c --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/Factories.kt @@ -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 BasicNetworkedElement(value: TYPE, codec: StreamCodec): BasicNetworkedElement { + 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..> networkedEnum(value: E) = BasicNetworkedElement(value, StreamCodec.Enum(value::class.java)) + +inline fun networkedJson(value: T, adapter: TypeAdapter = Starbound.gson.getAdapter(T::class.java), legacyIsArray: Boolean = true): BasicNetworkedElement { + 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 > networkedEnumStupid(value: E): BasicNetworkedElement { + val codec = StreamCodec.Enum(value::class.java) + return BasicNetworkedElement(value, codec, VarIntValueCodec, { it.ordinal.shl(1) }, { codec.values[it.ushr(1)] }) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/FloatingNetworkedElement.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/FloatingNetworkedElement.kt new file mode 100644 index 00000000..9c8b0755 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/FloatingNetworkedElement.kt @@ -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, 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>() + + var currentTime = 0.0 + private set + var isInterpolating = false + private set + var extrapolation = 0.0 + private set + + private val valueListeners = Listenable.Impl() + + 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): 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)) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/GroupElement.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/GroupElement.kt new file mode 100644 index 00000000..e79a3645 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/GroupElement.kt @@ -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>() + + 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() } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/JsonCodec.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/JsonCodec.kt new file mode 100644 index 00000000..71f60b7a --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/JsonCodec.kt @@ -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(val adapter: TypeAdapter, val wrapIntoArray: Boolean = false) : StreamCodec { + 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)) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/MasterElement.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/MasterElement.kt new file mode 100644 index 00000000..2b92837a --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/MasterElement.kt @@ -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(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 { + 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) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedElement.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedElement.kt new file mode 100644 index 00000000..cc5d7c8c --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedElement.kt @@ -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() + + 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 + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt index 85b4192b..62f52c20 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt @@ -37,6 +37,8 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn lateinit var shipWorld: ServerWorld private set + var skyVersion = 0L + init { connectionID = server.nextConnectionID.incrementAndGet() } @@ -86,6 +88,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn override fun setupNative() { super.setupNative() + shipChunkSource = IChunkSource.Void } fun receiveShipChunks(chunks: Map>) { @@ -219,13 +222,15 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn } override fun inGame() { - // server.playerInGame(this) - - LOGGER.info("Initializing ship world for $this") - shipWorld = ServerWorld(server, WorldGeometry(Vector2i(2048, 2048), false, false)) - shipWorld.addChunkSource(shipChunkSource) - shipWorld.thread.start() - shipWorld.acceptPlayer(this) + if (!isLegacy) { + server.playerInGame(this) + } else { + LOGGER.info("Initializing ship world for $this") + shipWorld = ServerWorld(server, WorldGeometry(Vector2i(2048, 2048), false, false)) + shipWorld.addChunkSource(shipChunkSource) + shipWorld.thread.start() + shipWorld.acceptPlayer(this) + } } companion object { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index f1184cc7..d1fef7ad 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -49,9 +49,12 @@ class ServerWorld( player.world = this if (player.isLegacy) { + val (skyData, skyVersion) = sky.networkedGroup.write(isLegacy = true) + player.skyVersion = skyVersion + player.sendAndFlush(WorldStartPacket( templateData = WorldTemplate(geometry).toJson(true), - skyData = ByteArray(0), + skyData = skyData.toByteArray(), weatherData = ByteArray(0), playerStart = playerSpawnPosition, playerRespawn = playerSpawnPosition, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/FloatSupplier.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/FloatSupplier.kt new file mode 100644 index 00000000..7ef01691 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/FloatSupplier.kt @@ -0,0 +1,12 @@ +package ru.dbotthepony.kstarbound.util + +import java.util.function.Supplier + +fun interface FloatSupplier : Supplier { + @Deprecated("Use type specific method instead", replaceWith = ReplaceWith("this.getAsFloat()")) + override fun get(): Float { + return getAsFloat() + } + + fun getAsFloat(): Float +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt new file mode 100644 index 00000000..bf1c49f6 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt @@ -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 + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 26c06370..886eaea9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -34,6 +34,7 @@ abstract class World, ChunkType : Chunk