Persistent universe parameters storage, as well as player context

This commit is contained in:
DBotThePony 2024-04-23 16:59:20 +07:00
parent c016dade54
commit 195de2d160
Signed by: DBot
GPG Key ID: DCC23B5715498507
16 changed files with 360 additions and 90 deletions

View File

@ -73,6 +73,7 @@ import ru.dbotthepony.kstarbound.util.HashTableInterner
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.util.random.nextRange import ru.dbotthepony.kstarbound.util.random.nextRange
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.SystemWorldLocation
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.* import java.io.*
import java.lang.ref.Cleaner import java.lang.ref.Cleaner
@ -368,6 +369,8 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
registerTypeAdapterFactory(BiomePlacementItemType.DEFINITION_ADAPTER) registerTypeAdapterFactory(BiomePlacementItemType.DEFINITION_ADAPTER)
registerTypeAdapterFactory(BiomePlaceables.Item.Companion) registerTypeAdapterFactory(BiomePlaceables.Item.Companion)
registerTypeAdapterFactory(SystemWorldLocation.ADAPTER)
// register companion first, so it has lesser priority than dispatching adapter // register companion first, so it has lesser priority than dispatching adapter
registerTypeAdapterFactory(VisitableWorldParametersType.Companion) registerTypeAdapterFactory(VisitableWorldParametersType.Companion)
registerTypeAdapterFactory(VisitableWorldParametersType.ADAPTER) registerTypeAdapterFactory(VisitableWorldParametersType.ADAPTER)

View File

@ -5,6 +5,7 @@ import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import kotlinx.coroutines.future.await
import ru.dbotthepony.kommons.io.StreamCodec import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.readUUID import ru.dbotthepony.kommons.io.readUUID
import ru.dbotthepony.kstarbound.io.readVector2d import ru.dbotthepony.kstarbound.io.readVector2d
@ -13,16 +14,20 @@ import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeStruct2d import ru.dbotthepony.kommons.io.writeStruct2d
import ru.dbotthepony.kommons.io.writeStruct2f import ru.dbotthepony.kommons.io.writeStruct2f
import ru.dbotthepony.kommons.io.writeUUID import ru.dbotthepony.kommons.io.writeUUID
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.server.world.ServerChunk
import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.UUID import java.util.UUID
import kotlin.math.PI
import kotlin.math.roundToInt import kotlin.math.roundToInt
// original game has MVariant here // original game has MVariant here
@ -35,14 +40,14 @@ import kotlin.math.roundToInt
@JsonAdapter(SpawnTarget.Adapter::class) @JsonAdapter(SpawnTarget.Adapter::class)
sealed class SpawnTarget { sealed class SpawnTarget {
abstract fun write(stream: DataOutputStream, isLegacy: Boolean) abstract fun write(stream: DataOutputStream, isLegacy: Boolean)
abstract fun resolve(world: ServerWorld): Vector2d? abstract suspend fun resolve(world: ServerWorld): Vector2d?
object Whatever : SpawnTarget() { object Whatever : SpawnTarget() {
override fun write(stream: DataOutputStream, isLegacy: Boolean) { override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(0) stream.writeByte(0)
} }
override fun resolve(world: ServerWorld): Vector2d { override suspend fun resolve(world: ServerWorld): Vector2d {
return world.playerSpawnPosition return world.playerSpawnPosition
} }
@ -57,7 +62,7 @@ sealed class SpawnTarget {
stream.writeBinaryString(id) stream.writeBinaryString(id)
} }
override fun resolve(world: ServerWorld): Vector2d? { override suspend fun resolve(world: ServerWorld): Vector2d? {
return world.entities.values.firstOrNull { it.uniqueID.get().orNull() == id }?.position return world.entities.values.firstOrNull { it.uniqueID.get().orNull() == id }?.position
} }
@ -81,7 +86,7 @@ sealed class SpawnTarget {
return "${position.x.roundToInt()}.${position.y.roundToInt()}" return "${position.x.roundToInt()}.${position.y.roundToInt()}"
} }
override fun resolve(world: ServerWorld): Vector2d { override suspend fun resolve(world: ServerWorld): Vector2d {
return position return position
} }
} }
@ -101,8 +106,26 @@ sealed class SpawnTarget {
return position.roundToInt().toString() return position.roundToInt().toString()
} }
override fun resolve(world: ServerWorld): Vector2d { override suspend fun resolve(world: ServerWorld): Vector2d {
TODO("Not yet implemented") val basePos = Vector2d(position, world.geometry.size.y * 0.5)
val tickets = ArrayList<ServerChunk.ITicket>()
try {
for (i in 0 until Globals.worldServer.playerSpaceStartMaximumTries) {
val testPos = world.geometry.wrap(basePos + Vector2d.angle(world.random.nextDouble(PI * 2.0), i * Globals.worldServer.playerSpaceStartDistanceIncrement))
val testRect = AABB.withSide(testPos, Globals.worldServer.playerSpaceStartRegionSize.x, Globals.worldServer.playerSpaceStartRegionSize.y)
tickets.addAll(world.permanentChunkTicket(testRect).await())
tickets.forEach { it.chunk.await() }
if (!world.anyCellSatisfies(testRect) { x, y, cell -> cell.foreground.material.value.collisionKind.isSolidCollision })
return testPos
}
return basePos
} finally {
tickets.forEach { it.cancel() }
}
} }
} }
@ -134,7 +157,8 @@ sealed class SpawnTarget {
val matchPos = position.matchEntire(value) val matchPos = position.matchEntire(value)
if (matchPos != null) { if (matchPos != null) {
return Position(Vector2d(matchPos.groups[0]!!.value.toDouble(), matchPos.groups[0]!!.value.toDouble())) val split = matchPos.groups[0]!!.value.split('.')
return Position(Vector2d(split[0].toDouble(), split[1].toDouble()))
} }
val matchX = positionX.matchEntire(value) val matchX = positionX.matchEntire(value)

View File

@ -11,6 +11,7 @@ import java.util.function.Predicate
data class UniverseServerConfig( data class UniverseServerConfig(
// in milliseconds // in milliseconds
val clockUpdatePacketInterval: Long = 500L, val clockUpdatePacketInterval: Long = 500L,
val universeStorageInterval: Long = 10000L,
val findStarterWorldParameters: StarterWorld, val findStarterWorldParameters: StarterWorld,
val queuedFlightWaitTime: Double = 0.0, val queuedFlightWaitTime: Double = 0.0,

View File

@ -96,12 +96,10 @@ sealed class WorldID {
if (value.isBlank()) if (value.isBlank())
return Limbo return Limbo
val parts = value.split(':') return when (val type = value.substringBefore(':').lowercase()) {
return when (val type = parts[0].lowercase()) {
"nowhere" -> Limbo "nowhere" -> Limbo
"instanceworld" -> { "instanceworld" -> {
val rest = parts[1].split(':') val rest = value.substringAfter(':').split(':')
if (rest.isEmpty() || rest.size > 3) { if (rest.isEmpty() || rest.size > 3) {
throw IllegalArgumentException("Malformed InstanceWorld string: $value") throw IllegalArgumentException("Malformed InstanceWorld string: $value")
@ -125,8 +123,8 @@ sealed class WorldID {
Instance(name, uuid, threatLevel) Instance(name, uuid, threatLevel)
} }
"celestialworld" -> Celestial(UniversePos.parse(parts[1])) "celestialworld" -> Celestial(UniversePos.parse(value.substringAfter(':')))
"clientshipworld" -> ShipWorld(uuidFromStarboundString(parts[1])) "clientshipworld" -> ShipWorld(uuidFromStarboundString(value.substringAfter(':')))
else -> throw IllegalArgumentException("Invalid WorldID type: $type (input: $value)") else -> throw IllegalArgumentException("Invalid WorldID type: $type (input: $value)")
} }
} }

View File

@ -4,6 +4,9 @@ import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.vector.Vector2i
class WorldServerConfig( class WorldServerConfig(
val playerSpaceStartRegionSize: Vector2d,
val playerSpaceStartDistanceIncrement: Double,
val playerSpaceStartMaximumTries: Int,
val playerStartRegionMaximumTries: Int = 1, val playerStartRegionMaximumTries: Int = 1,
val playerStartRegionMaximumVerticalSearch: Int = 1, val playerStartRegionMaximumVerticalSearch: Int = 1,
val playerStartRegionSize: Vector2d, val playerStartRegionSize: Vector2d,

View File

@ -9,6 +9,7 @@ import com.google.gson.JsonPrimitive
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import ru.dbotthepony.kommons.io.readBinaryString import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.io.readSignedVarLong import ru.dbotthepony.kommons.io.readSignedVarLong
import ru.dbotthepony.kommons.io.readString import ru.dbotthepony.kommons.io.readString
@ -21,6 +22,10 @@ import java.io.InputStream
import java.io.Reader import java.io.Reader
import java.util.LinkedList import java.util.LinkedList
fun ByteArray.readJsonElement(): JsonElement = DataInputStream(FastByteArrayInputStream(this)).readJsonElement()
fun ByteArray.readJsonObject(): JsonObject = DataInputStream(FastByteArrayInputStream(this)).readJsonObject()
fun ByteArray.readJsonArray(): JsonArray = DataInputStream(FastByteArrayInputStream(this)).readJsonArray()
/** /**
* Позволяет читать двоичный JSON прямиком в [JsonElement] * Позволяет читать двоичный JSON прямиком в [JsonElement]
*/ */

View File

@ -5,12 +5,19 @@ import com.google.gson.JsonElement
import com.google.gson.JsonNull import com.google.gson.JsonNull
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeSignedVarLong import ru.dbotthepony.kommons.io.writeSignedVarLong
import ru.dbotthepony.kommons.io.writeVarInt import ru.dbotthepony.kommons.io.writeVarInt
import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
fun JsonElement.writeJsonElement(): ByteArray = FastByteArrayOutputStream().let { DataOutputStream(it).writeJsonElement(this); it.array.copyOf(it.length) }
fun JsonObject.writeJsonObject(): ByteArray = FastByteArrayOutputStream().let { DataOutputStream(it).writeJsonObject(this); it.array.copyOf(it.length) }
fun JsonArray.writeJsonArray(): ByteArray = FastByteArrayOutputStream().let { DataOutputStream(it).writeJsonArray(this); it.array.copyOf(it.length) }
fun DataOutputStream.writeJsonElement(value: JsonElement) { fun DataOutputStream.writeJsonElement(value: JsonElement) {
when (value) { when (value) {
is JsonNull -> write(BinaryJsonReader.TYPE_NULL) is JsonNull -> write(BinaryJsonReader.TYPE_NULL)

View File

@ -15,6 +15,7 @@ import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.math.vector.Vector3i import ru.dbotthepony.kstarbound.math.vector.Vector3i
import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.WarpAction import ru.dbotthepony.kstarbound.defs.WarpAction
import ru.dbotthepony.kstarbound.defs.WarpAlias import ru.dbotthepony.kstarbound.defs.WarpAlias
import ru.dbotthepony.kstarbound.defs.WarpMode import ru.dbotthepony.kstarbound.defs.WarpMode
@ -22,6 +23,7 @@ import ru.dbotthepony.kstarbound.defs.WorldID
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import ru.dbotthepony.kstarbound.defs.world.SkyType import ru.dbotthepony.kstarbound.defs.world.SkyType
import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParameters import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParameters
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.network.Connection import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.ConnectionSide import ru.dbotthepony.kstarbound.network.ConnectionSide
import ru.dbotthepony.kstarbound.network.ConnectionType import ru.dbotthepony.kstarbound.network.ConnectionType
@ -33,13 +35,14 @@ import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPac
import ru.dbotthepony.kstarbound.server.world.ServerWorldTracker import ru.dbotthepony.kstarbound.server.world.ServerWorldTracker
import ru.dbotthepony.kstarbound.server.world.WorldStorage import ru.dbotthepony.kstarbound.server.world.WorldStorage
import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage
import ru.dbotthepony.kstarbound.server.world.NativeWorldStorage
import ru.dbotthepony.kstarbound.server.world.ServerSystemWorld import ru.dbotthepony.kstarbound.server.world.ServerSystemWorld
import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.world.SystemWorldLocation import ru.dbotthepony.kstarbound.world.SystemWorldLocation
import ru.dbotthepony.kstarbound.world.UniversePos import ru.dbotthepony.kstarbound.world.UniversePos
import java.util.HashMap import java.util.HashMap
import java.util.UUID import java.util.UUID
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import kotlin.properties.Delegates import kotlin.properties.Delegates
// serverside part of connection // serverside part of connection
@ -108,15 +111,23 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
} }
private var remoteVersion = 0L private var remoteVersion = 0L
private var saveClientContextTask: Future<*>? = null
override fun onChannelClosed() { override fun onChannelClosed() {
playerEntity = null playerEntity = null
saveClientContextTask?.cancel(false)
tracker?.remove("Connection channel closed")
tracker = null
saveClientContext()
super.onChannelClosed() super.onChannelClosed()
warpQueue.close() warpQueue.close()
server.channels.freeConnectionID(connectionID) server.channels.freeConnectionID(connectionID)
server.channels.connections.remove(this) server.channels.connections.remove(this)
server.freeNickname(nickname) server.freeNickname(nickname)
systemWorld?.removeClient(this) systemWorld?.removeClient(this)
systemWorld = null systemWorld = null
@ -132,15 +143,19 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
} }
} }
private val warpQueue = Channel<Pair<WarpAction, Boolean>>(capacity = 10) private data class WarpRequest(val action: WarpAction, val deploy: Boolean, val ifFailed: WarpAction?)
private val warpQueue = Channel<WarpRequest>(capacity = 10)
private suspend fun warpEventLoop() { private suspend fun warpEventLoop() {
while (true) { while (true) {
var (request, deploy) = warpQueue.receive() var (request, deploy, ifFailed) = warpQueue.receive()
if (request is WarpAlias) if (request is WarpAlias)
request = request.remap(this) request = request.remap(this)
if (ifFailed is WarpAlias)
ifFailed = ifFailed.remap(this)
LOGGER.info("Trying to warp ${alias()} to $request") LOGGER.info("Trying to warp ${alias()} to $request")
val resolve = request.resolve(this) val resolve = request.resolve(this)
@ -155,7 +170,12 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
server.loadWorld(resolve).await() server.loadWorld(resolve).await()
} catch (err: Throwable) { } catch (err: Throwable) {
send(PlayerWarpResultPacket(false, request, false)) send(PlayerWarpResultPacket(false, request, false))
LOGGER.error("Unable to wark ${alias()} to $request", err) LOGGER.error("Unable to warp ${alias()} to $request", err)
if (ifFailed != null) {
enqueueWarp(ifFailed)
}
continue continue
} }
@ -192,8 +212,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
private var currentOrbitalWarpAction = KOptional<Pair<WarpAction, WarpMode>>() private var currentOrbitalWarpAction = KOptional<Pair<WarpAction, WarpMode>>()
// coordinates ship flight private suspend fun findStartingSystem(): UniversePos? {
private suspend fun shipFlightEventLoop() {
shipWorld.sky.skyType = SkyType.ORBITAL shipWorld.sky.skyType = SkyType.ORBITAL
shipWorld.sky.startFlying(true, true) shipWorld.sky.startFlying(true, true)
var visited = 0 var visited = 0
@ -235,25 +254,46 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
if (found == null) { if (found == null) {
LOGGER.fatal("Unable to find starter world for $this!") LOGGER.fatal("Unable to find starter world for $this!")
disconnect("Unable to find starter world") disconnect("Unable to find starter world")
return return null
} }
LOGGER.info("Found appropriate starter world at $found for ${alias()}") LOGGER.info("Found appropriate starter world at $found for ${alias()}")
return found
}
val worldPromise = server.loadSystemWorld(found.location) private var systemWorldLocation: SystemWorldLocation = SystemWorldLocation.Transit
worldPromise.thenApply { // coordinates ship flight
systemWorld = it private suspend fun shipFlightEventLoop(initialLocation: Vector3i, inWorldLocation: SystemWorldLocation) {
val worldPromise = server.loadSystemWorld(initialLocation)
val loadWorld = worldPromise.await()
var actualInWorldLocation = inWorldLocation
if (!isConnected) { var world: ServerSystemWorld
it.removeClient(this)
if (loadWorld == null) {
LOGGER.warn("Tried to put player to system world at $initialLocation, but we are unable to load it")
// we ended up nowhere, try to find new starter location
val find = findStartingSystem() ?: return
val tryAnother = server.loadSystemWorld(find.location).await()
if (tryAnother == null) {
// how?
disconnect("Unable to put player in system world")
return
} else {
actualInWorldLocation = SystemWorldLocation.Celestial(find)
world = tryAnother
} }
} else {
world = loadWorld
} }
var world = worldPromise.await() this.systemWorld = world
var ship = world.addClient(this, location = SystemWorldLocation.Celestial(found)).await() var ship = world.addClient(this, location = actualInWorldLocation).await()
shipWorld.sky.stopFlyingAt(ship.location.skyParameters(world)) shipWorld.sky.stopFlyingAt(ship.location.skyParameters(world))
shipCoordinate = found shipCoordinate = UniversePos(world.location)
systemWorldLocation = actualInWorldLocation
run { run {
val action = ship.location.orbitalAction(world) val action = ship.location.orbitalAction(world)
@ -280,6 +320,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
} }
currentFlightJob = scope.launch { currentFlightJob = scope.launch {
systemWorldLocation = location
val coords = flight.await() val coords = flight.await()
val action = coords.orbitalAction(world) val action = coords.orbitalAction(world)
currentOrbitalWarpAction = action currentOrbitalWarpAction = action
@ -315,24 +356,16 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
} }
LOGGER.info("${alias()} is flying to new system: ${UniversePos(system)}") LOGGER.info("${alias()} is flying to new system: ${UniversePos(system)}")
val newSystem = server.loadSystemWorld(system)
shipCoordinate = UniversePos(system)
currentOrbitalWarpAction = KOptional() currentOrbitalWarpAction = KOptional()
for (client in shipWorld.clients) { for (client in shipWorld.clients) {
client.client.orbitalWarpAction = KOptional() client.client.orbitalWarpAction = KOptional()
} }
newSystem.thenApply { world = server.loadSystemWorld(system).await() ?: world
systemWorld = it shipCoordinate = UniversePos(world.location) // update ship coordinate after we have successfully travelled to destination
this.systemWorld = world
if (!isConnected) {
it.removeClient(this)
}
}
world = newSystem.await()
ship = world.addClient(this).await() ship = world.addClient(this).await()
val newParams = ship.location.skyParameters(world) val newParams = ship.location.skyParameters(world)
@ -400,8 +433,8 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
} }
} }
fun enqueueWarp(destination: WarpAction, deploy: Boolean = false) { fun enqueueWarp(destination: WarpAction, deploy: Boolean = false, ifFailed: WarpAction? = null) {
warpQueue.trySend(destination to deploy) warpQueue.trySend(WarpRequest(destination, deploy, ifFailed))
} }
fun tick() { fun tick() {
@ -445,12 +478,10 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
} }
isReady = false isReady = false
tracker?.remove()
tracker = null
if (::shipWorld.isInitialized) { tracker?.remove("Disconnect")
shipWorld.eventLoop.shutdown() tracker = null
} saveClientContext()
if (channel.isOpen) { if (channel.isOpen) {
// say goodbye // say goodbye
@ -479,6 +510,55 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
private var countedTowardsPlayerCount = false private var countedTowardsPlayerCount = false
@JsonFactory
data class ClientContextData(
val shipCoordinate: Vector3i,
val systemLocation: SystemWorldLocation,
val returnWarp: WarpAction? = null
)
fun saveClientContext() {
if (server.isShutdown && !server.isSameThread())
return
val data = ClientContextData(shipCoordinate.location, systemWorldLocation, returnWarp)
server.writeClientContext(uuid!!, Starbound.gson.toJsonTree(data) as JsonObject)
}
private suspend fun loadDataAndDispatchEventLoops() {
val context = try {
Starbound.gson.fromJson(server.loadClientContext(uuid!!).await(), ClientContextData::class.java)
} catch (err: Throwable) {
LOGGER.warn("Exception deserializing player context for ${alias()}, considering fresh context", err)
null
}
shipUpgrades = shipUpgrades.addCapability("planetTravel")
shipUpgrades = shipUpgrades.addCapability("teleport")
shipUpgrades = shipUpgrades.copy(maxFuel = 10000, shipLevel = 3)
scope.launch { warpEventLoop() }
if (context == null) {
shipWorld.eventLoop.execute { shipWorld.sky.startFlying(true, true) }
enqueueWarp(WarpAlias.OwnShip)
val startingLocation = findStartingSystem() ?: return
scope.launch { shipFlightEventLoop(startingLocation.location, SystemWorldLocation.Celestial(startingLocation)) }
} else {
if (context.returnWarp != null) {
enqueueWarp(context.returnWarp, ifFailed = WarpAlias.OwnShip)
} else {
enqueueWarp(WarpAlias.OwnShip)
}
scope.launch { shipFlightEventLoop(context.shipCoordinate, context.systemLocation) }
}
saveClientContextTask = channel.eventLoop().scheduleWithFixedDelay(Runnable { saveClientContext() }, 0L, 10L, TimeUnit.SECONDS)
scope.launch { saveClientContext() }
}
override fun inGame() { override fun inGame() {
super.inGame() super.inGame()
announcedDisconnect = false announcedDisconnect = false
@ -498,16 +578,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
shipWorld.sky.referenceClock = server.universeClock shipWorld.sky.referenceClock = server.universeClock
// shipWorld.sky.startFlying(true, true) // shipWorld.sky.startFlying(true, true)
shipWorld.eventLoop.start() shipWorld.eventLoop.start()
enqueueWarp(WarpAlias.OwnShip) scope.launch { loadDataAndDispatchEventLoops() }
shipUpgrades = shipUpgrades.addCapability("planetTravel")
shipUpgrades = shipUpgrades.addCapability("teleport")
shipUpgrades = shipUpgrades.copy(maxFuel = 10000, shipLevel = 3)
scope.launch { shipFlightEventLoop() }
scope.launch { warpEventLoop() }
if (server.channels.connections.size > 1) {
enqueueWarp(WarpAction.Player(server.channels.connections.first().uuid!!))
}
} }
}.exceptionally { }.exceptionally {
LOGGER.error("Error while initializing shipworld for $this", it) LOGGER.error("Error while initializing shipworld for $this", it)

View File

@ -1,6 +1,12 @@
package ru.dbotthepony.kstarbound.server package ru.dbotthepony.kstarbound.server
import com.github.benmanes.caffeine.cache.CacheLoader
import com.github.benmanes.caffeine.cache.Caffeine
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@ -10,6 +16,7 @@ import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.math.vector.Vector3i import ru.dbotthepony.kstarbound.math.vector.Vector3i
import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
@ -18,6 +25,10 @@ import ru.dbotthepony.kstarbound.defs.world.AsteroidsWorldParameters
import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters
import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldParameters import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldParameters
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.readJsonObject
import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.json.writeJsonObject
import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket
import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage
import ru.dbotthepony.kstarbound.server.world.ServerUniverse import ru.dbotthepony.kstarbound.server.world.ServerUniverse
@ -26,8 +37,13 @@ import ru.dbotthepony.kstarbound.server.world.ServerSystemWorld
import ru.dbotthepony.kstarbound.server.world.WorldStorage import ru.dbotthepony.kstarbound.server.world.WorldStorage
import ru.dbotthepony.kstarbound.util.BlockableEventLoop import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.JVMClock import ru.dbotthepony.kstarbound.util.JVMClock
import ru.dbotthepony.kstarbound.util.asStringOrNull
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.toStarboundString
import ru.dbotthepony.kstarbound.util.uuidFromStarboundString
import ru.dbotthepony.kstarbound.world.UniversePos import ru.dbotthepony.kstarbound.world.UniversePos
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.File import java.io.File
import java.sql.DriverManager import java.sql.DriverManager
import java.util.UUID import java.util.UUID
@ -56,24 +72,90 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
val chat = ChatHandler(this) val chat = ChatHandler(this)
val globalScope = CoroutineScope(Starbound.COROUTINE_EXECUTOR + SupervisorJob()) val globalScope = CoroutineScope(Starbound.COROUTINE_EXECUTOR + SupervisorJob())
private val database = DriverManager.getConnection("jdbc:sqlite:${File(universeFolder, "universe.db").absolutePath.replace('\\', '/')}")
init {
database.autoCommit = false
database.createStatement().use {
it.execute("CREATE TABLE IF NOT EXISTS `metadata` (`key` VARCHAR NOT NULL PRIMARY KEY, `value` BLOB NOT NULL)")
it.execute("CREATE TABLE IF NOT EXISTS `universe_flags` (`flag` VARCHAR NOT NULL PRIMARY KEY)")
it.execute("CREATE TABLE IF NOT EXISTS `client_context` (`uuid` VARCHAR NOT NULL PRIMARY KEY, `data` BLOB NOT NULL)")
}
}
private val lookupMetadata = database.prepareStatement("SELECT `value` FROM `metadata` WHERE `key` = ?")
private val writeMetadata = database.prepareStatement("REPLACE INTO `metadata` (`key`, `value`) VALUES (?, ?)")
private val lookupClientContext = database.prepareStatement("SELECT `data` FROM `client_context` WHERE `uuid` = ?")
private val writeClientContext = database.prepareStatement("REPLACE INTO `client_context` (`uuid`, `data`) VALUES (?, ?)")
private fun getMetadata(key: String): KOptional<JsonElement> {
lookupMetadata.setString(1, key)
return lookupMetadata.executeQuery().use {
if (it.next()) {
KOptional(it.getBytes(1).readJsonElement())
} else {
KOptional()
}
}
}
private fun setMetadata(key: String, value: JsonElement) {
writeMetadata.setString(1, key)
writeMetadata.setBytes(2, value.writeJsonElement())
writeMetadata.execute()
}
fun loadClientContext(uuid: UUID): CompletableFuture<JsonObject?> {
return supplyAsync {
lookupClientContext.setString(1, uuid.toStarboundString())
lookupClientContext.executeQuery().use {
if (it.next()) {
it.getBytes(1).readJsonObject()
} else {
null
}
}
}
}
fun writeClientContext(uuid: UUID, context: JsonObject): CompletableFuture<*> {
return supplyAsync {
writeClientContext.setString(1, uuid.toStarboundString())
writeClientContext.setBytes(2, context.writeJsonObject())
writeClientContext.execute()
}
}
val settings = ServerSettings() val settings = ServerSettings()
val channels = ServerChannels(this) val channels = ServerChannels(this)
val lock = ReentrantLock() val lock = ReentrantLock()
var isClosed = false var isClosed = false
private set private set
var serverUUID: UUID = UUID.randomUUID() val serverUUID: UUID = uuidFromStarboundString(getMetadata("server_uuid").orElse { JsonPrimitive(UUID.randomUUID().toStarboundString()) }.asString)
protected set
val universeClock = JVMClock() val universeClock = JVMClock()
private val systemWorlds = HashMap<Vector3i, CompletableFuture<ServerSystemWorld>>() init {
universeClock.set(getMetadata("universe_clock").orElse { JsonPrimitive(0.0) }.asDouble)
private suspend fun loadSystemWorld0(location: Vector3i): ServerSystemWorld { setMetadata("server_uuid", JsonPrimitive(serverUUID.toStarboundString()))
return ServerSystemWorld.create(this, location) database.commit()
} }
fun loadSystemWorld(location: Vector3i): CompletableFuture<ServerSystemWorld> { private val systemWorlds = HashMap<Vector3i, CompletableFuture<ServerSystemWorld?>>()
private suspend fun loadSystemWorld0(location: Vector3i): ServerSystemWorld? {
try {
return ServerSystemWorld.create(this, location)
} catch (err: Throwable) {
LOGGER.error("Exception loading system world at $location", err)
return null
}
}
fun loadSystemWorld(location: Vector3i): CompletableFuture<ServerSystemWorld?> {
return supplyAsync { return supplyAsync {
systemWorlds.computeIfAbsent(location) { systemWorlds.computeIfAbsent(location) {
globalScope.async { loadSystemWorld0(location) }.asCompletableFuture() globalScope.async { loadSystemWorld0(location) }.asCompletableFuture()
@ -82,7 +164,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
} }
private suspend fun loadCelestialWorld(location: WorldID.Celestial): ServerWorld { private suspend fun loadCelestialWorld(location: WorldID.Celestial): ServerWorld {
val file = File(universeFolder, location.pos.toString().replace(':', '_') + ".kworld") val file = File(universeFolder, location.pos.toString().replace(':', '_') + ".db")
val firstTime = !file.exists() val firstTime = !file.exists()
val storage = LegacyWorldStorage.SQL(file) val storage = LegacyWorldStorage.SQL(file)
@ -197,21 +279,22 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
} }
} }
fun loadSystemWorld(location: UniversePos): CompletableFuture<ServerSystemWorld> {
return loadSystemWorld(location.location)
}
init { init {
scheduleAtFixedRate(Runnable { scheduleAtFixedRate(Runnable {
channels.broadcast(UniverseTimeUpdatePacket(universeClock.time), false) channels.broadcast(UniverseTimeUpdatePacket(universeClock.time), false)
setMetadata("universe_clock", JsonPrimitive(universeClock.time))
}, Globals.universeServer.clockUpdatePacketInterval, Globals.universeServer.clockUpdatePacketInterval, TimeUnit.MILLISECONDS) }, Globals.universeServer.clockUpdatePacketInterval, Globals.universeServer.clockUpdatePacketInterval, TimeUnit.MILLISECONDS)
scheduleWithFixedDelay(Runnable {
database.commit()
}, Globals.universeServer.universeStorageInterval, Globals.universeServer.universeStorageInterval, TimeUnit.MILLISECONDS)
scheduleAtFixedRate(Runnable { scheduleAtFixedRate(Runnable {
tickNormal(Starbound.TIMESTEP) tickNormal(Starbound.TIMESTEP)
}, Starbound.TIMESTEP_NANOS, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS) }, Starbound.TIMESTEP_NANOS, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS)
scheduleAtFixedRate(Runnable { scheduleAtFixedRate(Runnable {
tickSystemWorlds() tickSystemWorlds(Starbound.SYSTEM_WORLD_TIMESTEP)
}, Starbound.SYSTEM_WORLD_TIMESTEP_NANOS, Starbound.SYSTEM_WORLD_TIMESTEP_NANOS, TimeUnit.NANOSECONDS) }, Starbound.SYSTEM_WORLD_TIMESTEP_NANOS, Starbound.SYSTEM_WORLD_TIMESTEP_NANOS, TimeUnit.NANOSECONDS)
isDaemon = false isDaemon = false
@ -250,7 +333,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
protected abstract fun close0() protected abstract fun close0()
protected abstract fun tick0(delta: Double) protected abstract fun tick0(delta: Double)
private fun tickSystemWorlds() { private fun tickSystemWorlds(delta: Double) {
systemWorlds.values.removeIf { systemWorlds.values.removeIf {
if (it.isCompletedExceptionally) { if (it.isCompletedExceptionally) {
return@removeIf true return@removeIf true
@ -260,15 +343,19 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
return@removeIf false return@removeIf false
} }
if (it.get() == null) {
return@removeIf true
}
scope.launch { scope.launch {
try { try {
it.get().tick(Starbound.SYSTEM_WORLD_TIMESTEP) it.get()!!.tick(delta)
} catch (err: Throwable) { } catch (err: Throwable) {
LOGGER.fatal("Exception in system world $it event loop", err) LOGGER.fatal("Exception in system world $it event loop", err)
} }
} }
if (it.get().shouldClose()) { if (it.get()!!.shouldClose()) {
LOGGER.info("Stopping idling ${it.get()}") LOGGER.info("Stopping idling ${it.get()}")
return@removeIf true return@removeIf true
} }
@ -322,6 +409,8 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
it.cancel(true) it.cancel(true)
} }
database.commit()
database.close()
universe.close() universe.close()
close0() close0()
} }

View File

@ -264,6 +264,24 @@ class ServerSystemWorld : SystemWorld {
next = tasks.poll() next = tasks.poll()
} }
// safeguard for cases when client wasn't removed properly
ships.values.removeIf {
if (it.shouldRemove()) {
val packet = SystemObjectDestroyPacket(it.uuid)
ships.values.forEach { ship ->
if (ship !== it) {
ship.forget(it.uuid)
ship.client.send(packet)
}
}
true
} else {
false
}
}
entities.values.forEach { it.tick(delta) } entities.values.forEach { it.tick(delta) }
ships.values.forEach { it.tick(delta) } ships.values.forEach { it.tick(delta) }
@ -384,6 +402,10 @@ class ServerSystemWorld : SystemWorld {
client.send(SystemWorldUpdatePacket(objects, ships)) client.send(SystemWorldUpdatePacket(objects, ships))
} }
fun shouldRemove(): Boolean {
return !client.isConnected
}
fun forget(id: UUID) { fun forget(id: UUID) {
netVersions.removeLong(id) netVersions.removeLong(id)
} }
@ -499,12 +521,19 @@ class ServerSystemWorld : SystemWorld {
companion object { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
suspend fun create(server: StarboundServer, location: Vector3i): ServerSystemWorld { suspend fun create(server: StarboundServer, location: Vector3i): ServerSystemWorld? {
LOGGER.info("Creating new System World at $location") val anything = server.universe.parameters(UniversePos(location))
val world = ServerSystemWorld(server, location)
world.spawnInitialObjects() if (anything == null) {
world.spawnObjects() LOGGER.warn("Tried to create system world at $location, but nothing is there")
return world return null
} else {
LOGGER.info("Creating new System World at $location")
val world = ServerSystemWorld(server, location)
world.spawnInitialObjects()
world.spawnObjects()
return world
}
} }
suspend fun load(server: StarboundServer, data: JsonElement): ServerSystemWorld { suspend fun load(server: StarboundServer, data: JsonElement): ServerSystemWorld {

View File

@ -69,7 +69,7 @@ class ServerWorld private constructor(
val clients = CopyOnWriteArrayList<ServerWorldTracker>() val clients = CopyOnWriteArrayList<ServerWorldTracker>()
val shouldStopOnIdle = worldID !is WorldID.ShipWorld val shouldStopOnIdle = worldID !is WorldID.ShipWorld
private fun doAcceptClient(client: ServerConnection, action: WarpAction?) { private suspend fun doAcceptClient(client: ServerConnection, action: WarpAction?) {
try { try {
isBusy++ isBusy++
@ -108,7 +108,7 @@ class ServerWorld private constructor(
check(!eventLoop.isShutdown) { "$this is invalid" } check(!eventLoop.isShutdown) { "$this is invalid" }
try { try {
val future = eventLoop.supplyAsync { doAcceptClient(player, action) } val future = eventLoop.scope.async { doAcceptClient(player, action) }.asCompletableFuture()
future.exceptionally { future.exceptionally {
LOGGER.error("Error while accepting new player into world", it) LOGGER.error("Error while accepting new player into world", it)

View File

@ -380,8 +380,10 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
val playerEntity = client.playerEntity val playerEntity = client.playerEntity
if (playerEntity != null && world.worldID is WorldID.Celestial && setReturnWarp) { if (playerEntity != null && (world.worldID is WorldID.Celestial || world.worldID is WorldID.ShipWorld && world.worldID.uuid == client.uuid) && setReturnWarp) {
client.returnWarp = WarpAction.World(world.worldID, SpawnTarget.Position(playerEntity.position)) client.returnWarp = WarpAction.World(world.worldID, SpawnTarget.Position(playerEntity.position))
} else if (setReturnWarp) {
client.returnWarp = null
} }
if (nullifyVariables) { if (nullifyVariables) {

View File

@ -111,6 +111,11 @@ class JVMClock : IClock {
baseline = nanos baseline = nanos
} }
fun set(seconds: Double) {
origin = System.nanoTime()
baseline = (seconds * 1_000_000_000L).toLong()
}
fun pause() { fun pause() {
if (!isPaused) { if (!isPaused) {
baseline += System.nanoTime() - origin baseline += System.nanoTime() - origin

View File

@ -49,7 +49,7 @@ fun uuidFromStarboundString(value: String): UUID {
val a = value.substring(0, 16) val a = value.substring(0, 16)
val b = value.substring(16) val b = value.substring(16)
return UUID(a.toLong(16), b.toLong(16)) return UUID(java.lang.Long.parseUnsignedLong(a, 16), java.lang.Long.parseUnsignedLong(b, 16))
} }
fun paddedNumber(number: Int, digits: Int): String { fun paddedNumber(number: Int, digits: Int): String {

View File

@ -1,6 +1,10 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.gson.JsonObject
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.reflect.TypeToken
import ru.dbotthepony.kommons.io.readUUID import ru.dbotthepony.kommons.io.readUUID
import ru.dbotthepony.kommons.io.writeUUID import ru.dbotthepony.kommons.io.writeUUID
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
@ -14,6 +18,9 @@ import ru.dbotthepony.kstarbound.defs.world.AsteroidsWorldParameters
import ru.dbotthepony.kstarbound.defs.world.SkyParameters import ru.dbotthepony.kstarbound.defs.world.SkyParameters
import ru.dbotthepony.kstarbound.io.readVector2d import ru.dbotthepony.kstarbound.io.readVector2d
import ru.dbotthepony.kstarbound.io.writeStruct2d import ru.dbotthepony.kstarbound.io.writeStruct2d
import ru.dbotthepony.kstarbound.json.builder.DispatchingAdapter
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonSingleton
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
@ -30,6 +37,16 @@ sealed class SystemWorldLocation {
abstract suspend fun orbitalAction(system: SystemWorld): KOptional<Pair<WarpAction, WarpMode>> abstract suspend fun orbitalAction(system: SystemWorld): KOptional<Pair<WarpAction, WarpMode>>
abstract suspend fun skyParameters(system: SystemWorld): SkyParameters abstract suspend fun skyParameters(system: SystemWorld): SkyParameters
enum class Type(val token: TypeToken<out SystemWorldLocation>) {
TRANSIT(TypeToken.get(Transit::class.java)),
CELESTIAL(TypeToken.get(Celestial::class.java)),
ORBIT(TypeToken.get(Orbit::class.java)),
ENTITY(TypeToken.get(Entity::class.java)),
POSITION(TypeToken.get(Position::class.java));
}
abstract val type: Type
protected suspend fun appendParameters(parameters: SkyParameters, system: SystemWorld, orbit: UniversePos): SkyParameters { protected suspend fun appendParameters(parameters: SkyParameters, system: SystemWorld, orbit: UniversePos): SkyParameters {
val planets = ArrayList<UniversePos>() val planets = ArrayList<UniversePos>()
@ -58,6 +75,7 @@ sealed class SystemWorldLocation {
return parameters return parameters
} }
@JsonSingleton
object Transit : SystemWorldLocation() { object Transit : SystemWorldLocation() {
override fun write(stream: DataOutputStream, isLegacy: Boolean) { override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(0) stream.writeByte(0)
@ -74,9 +92,13 @@ sealed class SystemWorldLocation {
override suspend fun skyParameters(system: SystemWorld): SkyParameters { override suspend fun skyParameters(system: SystemWorld): SkyParameters {
return Globals.systemWorld.emptySkyParameters return Globals.systemWorld.emptySkyParameters
} }
override val type: Type
get() = Type.TRANSIT
} }
// orbiting around specific planet // orbiting around specific planet
@JsonFactory
data class Celestial(val position: UniversePos) : SystemWorldLocation() { data class Celestial(val position: UniversePos) : SystemWorldLocation() {
override fun write(stream: DataOutputStream, isLegacy: Boolean) { override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(1) stream.writeByte(1)
@ -94,9 +116,13 @@ sealed class SystemWorldLocation {
override suspend fun skyParameters(system: SystemWorld): SkyParameters { override suspend fun skyParameters(system: SystemWorld): SkyParameters {
return SkyParameters.create(position, system.universe) return SkyParameters.create(position, system.universe)
} }
override val type: Type
get() = Type.CELESTIAL
} }
// orbiting around celestial body // orbiting around celestial body
@JsonFactory
data class Orbit(val position: SystemWorld.Orbit) : SystemWorldLocation() { data class Orbit(val position: SystemWorld.Orbit) : SystemWorldLocation() {
override fun write(stream: DataOutputStream, isLegacy: Boolean) { override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(2) stream.writeByte(2)
@ -117,8 +143,12 @@ sealed class SystemWorldLocation {
// (but that is still technically possible to outer-orbit a satellite) // (but that is still technically possible to outer-orbit a satellite)
return appendParameters(Globals.systemWorld.emptySkyParameters.copy(), system, position.target) return appendParameters(Globals.systemWorld.emptySkyParameters.copy(), system, position.target)
} }
override val type: Type
get() = Type.ORBIT
} }
@JsonFactory
data class Entity(val uuid: UUID) : SystemWorldLocation() { data class Entity(val uuid: UUID) : SystemWorldLocation() {
override fun write(stream: DataOutputStream, isLegacy: Boolean) { override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(3) stream.writeByte(3)
@ -150,8 +180,12 @@ sealed class SystemWorldLocation {
return sky return sky
} }
override val type: Type
get() = Type.ENTITY
} }
@JsonFactory
data class Position(val position: Vector2d) : SystemWorldLocation() { data class Position(val position: Vector2d) : SystemWorldLocation() {
override fun write(stream: DataOutputStream, isLegacy: Boolean) { override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(4) stream.writeByte(4)
@ -197,9 +231,13 @@ sealed class SystemWorldLocation {
return Globals.systemWorld.emptySkyParameters return Globals.systemWorld.emptySkyParameters
} }
override val type: Type
get() = Type.POSITION
} }
companion object { companion object {
val ADAPTER = DispatchingAdapter("type", { type }, { token }, Type.entries)
val CODEC = nativeCodec(::read, SystemWorldLocation::write) val CODEC = nativeCodec(::read, SystemWorldLocation::write)
val LEGACY_CODEC = legacyCodec(::read, SystemWorldLocation::write) val LEGACY_CODEC = legacyCodec(::read, SystemWorldLocation::write)

View File

@ -1,21 +1,16 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.JsonAdapter
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kstarbound.math.vector.Vector3i import ru.dbotthepony.kstarbound.math.vector.Vector3i
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.io.readVarInt import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kstarbound.io.readVector3i import ru.dbotthepony.kstarbound.io.readVector3i
import ru.dbotthepony.kommons.io.writeSignedVarInt
import ru.dbotthepony.kommons.io.writeStruct3i import ru.dbotthepony.kommons.io.writeStruct3i
import ru.dbotthepony.kommons.io.writeVarInt import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
@ -144,8 +139,8 @@ data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit:
val values = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)!! val values = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)!!
val location = values.get("location", vectors) val location = values.get("location", vectors)
val planet = values.get("planet", 0) val planet = values.get("planet", 0)
val orbit = values.get("orbit", 0) val satellite = values.get("satellite", 0)
return UniversePos(location, planet, orbit) return UniversePos(location, planet, satellite)
} }
if (`in`.peek() == JsonToken.STRING) { if (`in`.peek() == JsonToken.STRING) {