From 195de2d16081307b3fae0d7b65929a3841d0b478 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Tue, 23 Apr 2024 16:59:20 +0700 Subject: [PATCH] Persistent universe parameters storage, as well as player context --- .../ru/dbotthepony/kstarbound/Starbound.kt | 3 + .../kstarbound/defs/PlayerWarping.kt | 38 ++++- .../kstarbound/defs/UniverseServerConfig.kt | 1 + .../ru/dbotthepony/kstarbound/defs/WorldID.kt | 10 +- .../kstarbound/defs/WorldServerConfig.kt | 3 + .../kstarbound/json/BinaryJsonReader.kt | 5 + .../kstarbound/json/BinaryJsonWriter.kt | 7 + .../kstarbound/server/ServerConnection.kt | 157 +++++++++++++----- .../kstarbound/server/StarboundServer.kt | 123 ++++++++++++-- .../server/world/ServerSystemWorld.kt | 41 ++++- .../kstarbound/server/world/ServerWorld.kt | 4 +- .../server/world/ServerWorldTracker.kt | 4 +- .../ru/dbotthepony/kstarbound/util/Clocks.kt | 5 + .../ru/dbotthepony/kstarbound/util/Utils.kt | 2 +- .../kstarbound/world/SystemWorldLocation.kt | 38 +++++ .../kstarbound/world/UniversePos.kt | 9 +- 16 files changed, 360 insertions(+), 90 deletions(-) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 823fafd7..c22f8dfe 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt index d542503a..56b48f28 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt @@ -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() + + 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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt index 68373311..ba995a28 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt @@ -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, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldID.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldID.kt index 53f9c85a..ecf22450 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldID.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldID.kt @@ -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)") } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldServerConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldServerConfig.kt index e4e8f9f0..cb4c1aa4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldServerConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldServerConfig.kt @@ -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, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt index 938f71a2..618960a7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt @@ -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] */ diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonWriter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonWriter.kt index c6cf422f..12d065ec 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonWriter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonWriter.kt @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt index c4f86dda..59d3b8b0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt @@ -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>(capacity = 10) + private data class WarpRequest(val action: WarpAction, val deploy: Boolean, val ifFailed: WarpAction?) + private val warpQueue = Channel(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>() - // 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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt index caf61573..2eb4d966 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt @@ -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 { + 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 { + 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>() - - 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 { + private val systemWorlds = HashMap>() + + 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 { 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 { - 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() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt index 4c5e5cae..506c4186 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt @@ -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 { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index 5e0121b6..a84f8801 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -69,7 +69,7 @@ class ServerWorld private constructor( val clients = CopyOnWriteArrayList() 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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt index 6dfb20ff..0df23a6a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt @@ -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) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt index 164c0776..10f92ae7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt @@ -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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt index d8594f4f..44aa886a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt @@ -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 { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorldLocation.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorldLocation.kt index 8fc2d3ec..b284cee3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorldLocation.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorldLocation.kt @@ -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> abstract suspend fun skyParameters(system: SystemWorld): SkyParameters + enum class Type(val token: TypeToken) { + 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() @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt index 66397939..511216e4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt @@ -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) {