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
kotlinCoroutinesVersion=1.8.0
kommonsVersion=2.9.20
kommonsVersion=2.9.21
ffiVersion=2.2.13
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.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<TileDamageConfig>()
private set
var sky by Delegates.notNull<SkyGlobalConfig>()
private set
private object EmptyTask : ForkJoinTask<Unit>() {
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
}
}

View File

@ -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<IInventoryIcon>?
val inventoryIcon: Either<out IInventoryIcon, out List<IInventoryIcon>>?
/**
* Теги предмета

View File

@ -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<IInventoryIcon>? = null,
override val inventoryIcon: Either<InventoryIcon, ImmutableList<IInventoryIcon>>? = null,
override val itemTags: ImmutableList<String> = ImmutableList.of(),
override val learnBlueprintsOnPickup: ImmutableList<Registry.Ref<IItemDefinition>> = ImmutableList.of(),
override val maxStack: Long = 9999L,

View File

@ -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<String> = ImmutableList.of(),
val miningSounds: ImmutableList<String> = ImmutableList.of(),
val footstepSound: Either<ImmutableList<String>, String> = Either.left(ImmutableList.of()),
val miningSounds: Either<ImmutableList<String>, String> = Either.left(ImmutableList.of()),
val blocksLiquidFlow: Boolean = true,
val soil: Boolean = false,

View File

@ -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<Pair<String, String>>)
class SkyParameters() {
var skyType = SkyType.BARREN
var seed = 0L
var dayLength: Double? = null
var horizonClouds = false
var skyColoring: Either<SkyColoring, RGBAColor> = Either.right(RGBAColor.BLACK)
var spaceLevel: Double? = null
var surfaceLevel: Double? = null
var nearbyPlanet: Pair<List<Pair<String, Double>>, 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<SkyColoring, RGBAColor> = Either.left(SkyColoring()),
var spaceLevel: Double? = null,
var surfaceLevel: Double? = null,
var nearbyPlanet: Pair<List<Pair<String, Double>>, 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<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 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
}

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 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)
}
}

View File

@ -75,8 +75,16 @@ enum class ConnectionType {
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
override fun read(stream: DataInputStream, isLegacy: Boolean, side: ConnectionSide): T {
return read(stream, isLegacy)
}
}
interface IPacket {

View File

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

View File

@ -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<String>, val arguments: KOptional<JsonElement>) {
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 -> {

View File

@ -50,12 +50,12 @@ class PacketRegistry(val isLegacy: Boolean) {
private val missingNames = Int2ObjectArrayMap<String>()
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
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)
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 <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 {
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")

View File

@ -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) },

View File

@ -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
}
}

View File

@ -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<Int, Vector2d>, val dungeonBreathable: Map<Int, Boolean>,
val protectedDungeonIDs: Set<Int>, 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<Int, Vector2d>,
val dungeonBreathable: Map<Int, Boolean>,
val protectedDungeonIDs: Set<Int>, 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)
}

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
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<ByteKey, KOptional<ByteArray>>) {
@ -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 {

View File

@ -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,

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 foreground = TileView.Foreground(this)
val mailbox = MailboxExecutorService()
val sky = Sky()
override fun getCellDirect(x: Int, y: Int): AbstractCell {
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.Test
import ru.dbotthepony.kstarbound.util.HashTableInterner
import ru.dbotthepony.kstarbound.util.random.MWCRandom
import java.util.concurrent.atomic.AtomicInteger
import java.util.random.RandomGenerator
@ -16,7 +17,7 @@ object InternerTest {
for (i in 0 until 8) {
threads.add(Thread {
val rand = RandomGenerator.of("Xoroshiro128PlusPlus")
val rand = MWCRandom()
for (i2 in 0 until 100_000) {
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())
}
}