NetworkedElement, Json RPC and ClientContextUpdate fixes
This commit is contained in:
parent
d37bad79c6
commit
66aa99acc2
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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>>?
|
||||
|
||||
/**
|
||||
* Теги предмета
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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>,
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -73,6 +73,8 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
|
||||
|
||||
isLegacy = false
|
||||
isConnected = true
|
||||
|
||||
inGame()
|
||||
}
|
||||
|
||||
protected open fun onChannelClosed() {
|
||||
|
@ -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 -> {
|
||||
|
@ -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")
|
||||
|
@ -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) },
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
@ -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)] })
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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() }
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
79
src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt
Normal file
79
src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user