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

View File

@ -5,6 +5,7 @@ import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import kotlinx.coroutines.future.await
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.readUUID
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.writeStruct2f
import ru.dbotthepony.kommons.io.writeUUID
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.io.readInternedString
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.nativeCodec
import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.server.world.ServerChunk
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.UUID
import kotlin.math.PI
import kotlin.math.roundToInt
// original game has MVariant here
@ -35,14 +40,14 @@ import kotlin.math.roundToInt
@JsonAdapter(SpawnTarget.Adapter::class)
sealed class SpawnTarget {
abstract fun write(stream: DataOutputStream, isLegacy: Boolean)
abstract fun resolve(world: ServerWorld): Vector2d?
abstract suspend fun resolve(world: ServerWorld): Vector2d?
object Whatever : SpawnTarget() {
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(0)
}
override fun resolve(world: ServerWorld): Vector2d {
override suspend fun resolve(world: ServerWorld): Vector2d {
return world.playerSpawnPosition
}
@ -57,7 +62,7 @@ sealed class SpawnTarget {
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
}
@ -81,7 +86,7 @@ sealed class SpawnTarget {
return "${position.x.roundToInt()}.${position.y.roundToInt()}"
}
override fun resolve(world: ServerWorld): Vector2d {
override suspend fun resolve(world: ServerWorld): Vector2d {
return position
}
}
@ -101,8 +106,26 @@ sealed class SpawnTarget {
return position.roundToInt().toString()
}
override fun resolve(world: ServerWorld): Vector2d {
TODO("Not yet implemented")
override suspend fun resolve(world: ServerWorld): Vector2d {
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)
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)

View File

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

View File

@ -96,12 +96,10 @@ sealed class WorldID {
if (value.isBlank())
return Limbo
val parts = value.split(':')
return when (val type = parts[0].lowercase()) {
return when (val type = value.substringBefore(':').lowercase()) {
"nowhere" -> Limbo
"instanceworld" -> {
val rest = parts[1].split(':')
val rest = value.substringAfter(':').split(':')
if (rest.isEmpty() || rest.size > 3) {
throw IllegalArgumentException("Malformed InstanceWorld string: $value")
@ -125,8 +123,8 @@ sealed class WorldID {
Instance(name, uuid, threatLevel)
}
"celestialworld" -> Celestial(UniversePos.parse(parts[1]))
"clientshipworld" -> ShipWorld(uuidFromStarboundString(parts[1]))
"celestialworld" -> Celestial(UniversePos.parse(value.substringAfter(':')))
"clientshipworld" -> ShipWorld(uuidFromStarboundString(value.substringAfter(':')))
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
class WorldServerConfig(
val playerSpaceStartRegionSize: Vector2d,
val playerSpaceStartDistanceIncrement: Double,
val playerSpaceStartMaximumTries: Int,
val playerStartRegionMaximumTries: Int = 1,
val playerStartRegionMaximumVerticalSearch: Int = 1,
val playerStartRegionSize: Vector2d,

View File

@ -9,6 +9,7 @@ import com.google.gson.JsonPrimitive
import com.google.gson.JsonSyntaxException
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.io.readSignedVarLong
import ru.dbotthepony.kommons.io.readString
@ -21,6 +22,10 @@ import java.io.InputStream
import java.io.Reader
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]
*/

View File

@ -5,12 +5,19 @@ import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
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.writeSignedVarLong
import ru.dbotthepony.kommons.io.writeVarInt
import java.io.DataInputStream
import java.io.DataOutputStream
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) {
when (value) {
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.Vector3i
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.WarpAction
import ru.dbotthepony.kstarbound.defs.WarpAlias
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.SkyType
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.ConnectionSide
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.WorldStorage
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.ServerWorld
import ru.dbotthepony.kstarbound.world.SystemWorldLocation
import ru.dbotthepony.kstarbound.world.UniversePos
import java.util.HashMap
import java.util.UUID
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import kotlin.properties.Delegates
// serverside part of connection
@ -108,15 +111,23 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
}
private var remoteVersion = 0L
private var saveClientContextTask: Future<*>? = null
override fun onChannelClosed() {
playerEntity = null
saveClientContextTask?.cancel(false)
tracker?.remove("Connection channel closed")
tracker = null
saveClientContext()
super.onChannelClosed()
warpQueue.close()
server.channels.freeConnectionID(connectionID)
server.channels.connections.remove(this)
server.freeNickname(nickname)
systemWorld?.removeClient(this)
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() {
while (true) {
var (request, deploy) = warpQueue.receive()
var (request, deploy, ifFailed) = warpQueue.receive()
if (request is WarpAlias)
request = request.remap(this)
if (ifFailed is WarpAlias)
ifFailed = ifFailed.remap(this)
LOGGER.info("Trying to warp ${alias()} to $request")
val resolve = request.resolve(this)
@ -155,7 +170,12 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
server.loadWorld(resolve).await()
} catch (err: Throwable) {
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
}
@ -192,8 +212,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
private var currentOrbitalWarpAction = KOptional<Pair<WarpAction, WarpMode>>()
// coordinates ship flight
private suspend fun shipFlightEventLoop() {
private suspend fun findStartingSystem(): UniversePos? {
shipWorld.sky.skyType = SkyType.ORBITAL
shipWorld.sky.startFlying(true, true)
var visited = 0
@ -235,25 +254,46 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
if (found == null) {
LOGGER.fatal("Unable to find starter world for $this!")
disconnect("Unable to find starter world")
return
return null
}
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 {
systemWorld = it
// coordinates ship flight
private suspend fun shipFlightEventLoop(initialLocation: Vector3i, inWorldLocation: SystemWorldLocation) {
val worldPromise = server.loadSystemWorld(initialLocation)
val loadWorld = worldPromise.await()
var actualInWorldLocation = inWorldLocation
if (!isConnected) {
it.removeClient(this)
var world: ServerSystemWorld
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()
var ship = world.addClient(this, location = SystemWorldLocation.Celestial(found)).await()
this.systemWorld = world
var ship = world.addClient(this, location = actualInWorldLocation).await()
shipWorld.sky.stopFlyingAt(ship.location.skyParameters(world))
shipCoordinate = found
shipCoordinate = UniversePos(world.location)
systemWorldLocation = actualInWorldLocation
run {
val action = ship.location.orbitalAction(world)
@ -280,6 +320,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
}
currentFlightJob = scope.launch {
systemWorldLocation = location
val coords = flight.await()
val action = coords.orbitalAction(world)
currentOrbitalWarpAction = action
@ -315,24 +356,16 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
}
LOGGER.info("${alias()} is flying to new system: ${UniversePos(system)}")
val newSystem = server.loadSystemWorld(system)
shipCoordinate = UniversePos(system)
currentOrbitalWarpAction = KOptional()
for (client in shipWorld.clients) {
client.client.orbitalWarpAction = KOptional()
}
newSystem.thenApply {
systemWorld = it
if (!isConnected) {
it.removeClient(this)
}
}
world = newSystem.await()
world = server.loadSystemWorld(system).await() ?: world
shipCoordinate = UniversePos(world.location) // update ship coordinate after we have successfully travelled to destination
this.systemWorld = world
ship = world.addClient(this).await()
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) {
warpQueue.trySend(destination to deploy)
fun enqueueWarp(destination: WarpAction, deploy: Boolean = false, ifFailed: WarpAction? = null) {
warpQueue.trySend(WarpRequest(destination, deploy, ifFailed))
}
fun tick() {
@ -445,12 +478,10 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
}
isReady = false
tracker?.remove()
tracker = null
if (::shipWorld.isInitialized) {
shipWorld.eventLoop.shutdown()
}
tracker?.remove("Disconnect")
tracker = null
saveClientContext()
if (channel.isOpen) {
// say goodbye
@ -479,6 +510,55 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
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() {
super.inGame()
announcedDisconnect = false
@ -498,16 +578,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
shipWorld.sky.referenceClock = server.universeClock
// shipWorld.sky.startFlying(true, true)
shipWorld.eventLoop.start()
enqueueWarp(WarpAlias.OwnShip)
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!!))
}
scope.launch { loadDataAndDispatchEventLoops() }
}
}.exceptionally {
LOGGER.error("Error while initializing shipworld for $this", it)

View File

@ -1,6 +1,12 @@
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 it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
@ -10,6 +16,7 @@ import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.math.vector.Vector3i
import ru.dbotthepony.kstarbound.Globals
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.TerrestrialWorldParameters
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.server.world.LegacyWorldStorage
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.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.JVMClock
import ru.dbotthepony.kstarbound.util.asStringOrNull
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 java.io.DataInputStream
import java.io.DataOutputStream
import java.io.File
import java.sql.DriverManager
import java.util.UUID
@ -56,24 +72,90 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
val chat = ChatHandler(this)
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 channels = ServerChannels(this)
val lock = ReentrantLock()
var isClosed = false
private set
var serverUUID: UUID = UUID.randomUUID()
protected set
val serverUUID: UUID = uuidFromStarboundString(getMetadata("server_uuid").orElse { JsonPrimitive(UUID.randomUUID().toStarboundString()) }.asString)
val universeClock = JVMClock()
private val systemWorlds = HashMap<Vector3i, CompletableFuture<ServerSystemWorld>>()
private suspend fun loadSystemWorld0(location: Vector3i): ServerSystemWorld {
return ServerSystemWorld.create(this, location)
init {
universeClock.set(getMetadata("universe_clock").orElse { JsonPrimitive(0.0) }.asDouble)
setMetadata("server_uuid", JsonPrimitive(serverUUID.toStarboundString()))
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 {
systemWorlds.computeIfAbsent(location) {
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 {
val file = File(universeFolder, location.pos.toString().replace(':', '_') + ".kworld")
val file = File(universeFolder, location.pos.toString().replace(':', '_') + ".db")
val firstTime = !file.exists()
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 {
scheduleAtFixedRate(Runnable {
channels.broadcast(UniverseTimeUpdatePacket(universeClock.time), false)
setMetadata("universe_clock", JsonPrimitive(universeClock.time))
}, Globals.universeServer.clockUpdatePacketInterval, Globals.universeServer.clockUpdatePacketInterval, TimeUnit.MILLISECONDS)
scheduleWithFixedDelay(Runnable {
database.commit()
}, Globals.universeServer.universeStorageInterval, Globals.universeServer.universeStorageInterval, TimeUnit.MILLISECONDS)
scheduleAtFixedRate(Runnable {
tickNormal(Starbound.TIMESTEP)
}, Starbound.TIMESTEP_NANOS, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS)
scheduleAtFixedRate(Runnable {
tickSystemWorlds()
tickSystemWorlds(Starbound.SYSTEM_WORLD_TIMESTEP)
}, Starbound.SYSTEM_WORLD_TIMESTEP_NANOS, Starbound.SYSTEM_WORLD_TIMESTEP_NANOS, TimeUnit.NANOSECONDS)
isDaemon = false
@ -250,7 +333,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
protected abstract fun close0()
protected abstract fun tick0(delta: Double)
private fun tickSystemWorlds() {
private fun tickSystemWorlds(delta: Double) {
systemWorlds.values.removeIf {
if (it.isCompletedExceptionally) {
return@removeIf true
@ -260,15 +343,19 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
return@removeIf false
}
if (it.get() == null) {
return@removeIf true
}
scope.launch {
try {
it.get().tick(Starbound.SYSTEM_WORLD_TIMESTEP)
it.get()!!.tick(delta)
} catch (err: Throwable) {
LOGGER.fatal("Exception in system world $it event loop", err)
}
}
if (it.get().shouldClose()) {
if (it.get()!!.shouldClose()) {
LOGGER.info("Stopping idling ${it.get()}")
return@removeIf true
}
@ -322,6 +409,8 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
it.cancel(true)
}
database.commit()
database.close()
universe.close()
close0()
}

View File

@ -264,6 +264,24 @@ class ServerSystemWorld : SystemWorld {
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) }
ships.values.forEach { it.tick(delta) }
@ -384,6 +402,10 @@ class ServerSystemWorld : SystemWorld {
client.send(SystemWorldUpdatePacket(objects, ships))
}
fun shouldRemove(): Boolean {
return !client.isConnected
}
fun forget(id: UUID) {
netVersions.removeLong(id)
}
@ -499,12 +521,19 @@ class ServerSystemWorld : SystemWorld {
companion object {
private val LOGGER = LogManager.getLogger()
suspend fun create(server: StarboundServer, location: Vector3i): ServerSystemWorld {
LOGGER.info("Creating new System World at $location")
val world = ServerSystemWorld(server, location)
world.spawnInitialObjects()
world.spawnObjects()
return world
suspend fun create(server: StarboundServer, location: Vector3i): ServerSystemWorld? {
val anything = server.universe.parameters(UniversePos(location))
if (anything == null) {
LOGGER.warn("Tried to create system world at $location, but nothing is there")
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 {

View File

@ -69,7 +69,7 @@ class ServerWorld private constructor(
val clients = CopyOnWriteArrayList<ServerWorldTracker>()
val shouldStopOnIdle = worldID !is WorldID.ShipWorld
private fun doAcceptClient(client: ServerConnection, action: WarpAction?) {
private suspend fun doAcceptClient(client: ServerConnection, action: WarpAction?) {
try {
isBusy++
@ -108,7 +108,7 @@ class ServerWorld private constructor(
check(!eventLoop.isShutdown) { "$this is invalid" }
try {
val future = eventLoop.supplyAsync { doAcceptClient(player, action) }
val future = eventLoop.scope.async { doAcceptClient(player, action) }.asCompletableFuture()
future.exceptionally {
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
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))
} else if (setReturnWarp) {
client.returnWarp = null
}
if (nullifyVariables) {

View File

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

View File

@ -49,7 +49,7 @@ fun uuidFromStarboundString(value: String): UUID {
val a = value.substring(0, 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 {

View File

@ -1,6 +1,10 @@
package ru.dbotthepony.kstarbound.world
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.writeUUID
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.io.readVector2d
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.nativeCodec
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 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 {
val planets = ArrayList<UniversePos>()
@ -58,6 +75,7 @@ sealed class SystemWorldLocation {
return parameters
}
@JsonSingleton
object Transit : SystemWorldLocation() {
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(0)
@ -74,9 +92,13 @@ sealed class SystemWorldLocation {
override suspend fun skyParameters(system: SystemWorld): SkyParameters {
return Globals.systemWorld.emptySkyParameters
}
override val type: Type
get() = Type.TRANSIT
}
// orbiting around specific planet
@JsonFactory
data class Celestial(val position: UniversePos) : SystemWorldLocation() {
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(1)
@ -94,9 +116,13 @@ sealed class SystemWorldLocation {
override suspend fun skyParameters(system: SystemWorld): SkyParameters {
return SkyParameters.create(position, system.universe)
}
override val type: Type
get() = Type.CELESTIAL
}
// orbiting around celestial body
@JsonFactory
data class Orbit(val position: SystemWorld.Orbit) : SystemWorldLocation() {
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(2)
@ -117,8 +143,12 @@ sealed class SystemWorldLocation {
// (but that is still technically possible to outer-orbit a satellite)
return appendParameters(Globals.systemWorld.emptySkyParameters.copy(), system, position.target)
}
override val type: Type
get() = Type.ORBIT
}
@JsonFactory
data class Entity(val uuid: UUID) : SystemWorldLocation() {
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(3)
@ -150,8 +180,12 @@ sealed class SystemWorldLocation {
return sky
}
override val type: Type
get() = Type.ENTITY
}
@JsonFactory
data class Position(val position: Vector2d) : SystemWorldLocation() {
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(4)
@ -197,9 +231,13 @@ sealed class SystemWorldLocation {
return Globals.systemWorld.emptySkyParameters
}
override val type: Type
get() = Type.POSITION
}
companion object {
val ADAPTER = DispatchingAdapter("type", { type }, { token }, Type.entries)
val CODEC = nativeCodec(::read, SystemWorldLocation::write)
val LEGACY_CODEC = legacyCodec(::read, SystemWorldLocation::write)

View File

@ -1,21 +1,16 @@
package ru.dbotthepony.kstarbound.world
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.annotations.JsonAdapter
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kstarbound.math.vector.Vector3i
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kstarbound.io.readVector3i
import ru.dbotthepony.kommons.io.writeSignedVarInt
import ru.dbotthepony.kommons.io.writeStruct3i
import ru.dbotthepony.kommons.io.writeVarInt
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 location = values.get("location", vectors)
val planet = values.get("planet", 0)
val orbit = values.get("orbit", 0)
return UniversePos(location, planet, orbit)
val satellite = values.get("satellite", 0)
return UniversePos(location, planet, satellite)
}
if (`in`.peek() == JsonToken.STRING) {