diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt index 146b921b..b908641e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt @@ -22,10 +22,11 @@ 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.readJsonElementInflated import ru.dbotthepony.kstarbound.json.readJsonObject import ru.dbotthepony.kstarbound.json.writeJsonElement +import ru.dbotthepony.kstarbound.json.writeJsonElementDeflated 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 import ru.dbotthepony.kstarbound.server.world.ServerWorld @@ -42,6 +43,7 @@ import java.util.UUID import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit import java.util.concurrent.locks.ReentrantLock +import kotlin.math.min sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread") { private fun makedir(file: File) { @@ -73,6 +75,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread 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)") + it.execute("CREATE TABLE IF NOT EXISTS `system_worlds` (`x` INTEGER NOT NULL, `y` INTEGER NOT NULL, `z` INTEGER NOT NULL, `data` BLOB NOT NULL, PRIMARY KEY (`x`, `y`, `z`))") } database.autoCommit = false @@ -82,6 +85,8 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread 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 val lookupSystemWorld = database.prepareStatement("SELECT `data` FROM `system_worlds` WHERE `x` = ? AND `y` = ? AND `z` = ?") + private val writeSystemWorld = database.prepareStatement("REPLACE INTO `system_worlds` (`x`, `y`, `z`, `data`) VALUES (?, ?, ?, ?)") private fun getMetadata(key: String): KOptional { lookupMetadata.setString(1, key) @@ -123,6 +128,33 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread } } + fun loadServerWorldData(pos: Vector3i): CompletableFuture { + return supplyAsync { + lookupSystemWorld.setInt(1, pos.x) + lookupSystemWorld.setInt(2, pos.y) + lookupSystemWorld.setInt(3, pos.z) + + lookupSystemWorld.executeQuery().use { + if (it.next()) { + it.getBytes(1).readJsonElementInflated() + } else { + null + } + } + } + } + + fun writeServerWorldData(pos: Vector3i, data: JsonElement) { + execute { + writeSystemWorld.setInt(1, pos.x) + writeSystemWorld.setInt(2, pos.y) + writeSystemWorld.setInt(3, pos.z) + writeSystemWorld.setBytes(4, data.writeJsonElementDeflated()) + + writeSystemWorld.execute() + } + } + val settings = ServerSettings() val channels = ServerChannels(this) val lock = ReentrantLock() @@ -146,7 +178,13 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread private suspend fun loadSystemWorld0(location: Vector3i): ServerSystemWorld? { try { - return ServerSystemWorld.create(this, location) + val load = loadServerWorldData(location).await() + + if (load == null) { + return ServerSystemWorld.create(this, location) + } else { + return ServerSystemWorld.load(this, location, load) + } } catch (err: Throwable) { LOGGER.error("Exception loading system world at $location", err) return null @@ -299,11 +337,13 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread } init { + val declareInterval = TimeUnit.MILLISECONDS.toNanos(Globals.universeServer.universeStorageInterval) + scheduleWithFixedDelay(Runnable { setMetadata("universe_clock", JsonPrimitive(universeClock.time)) database.commit() universe.flush() - }, Globals.universeServer.universeStorageInterval, Globals.universeServer.universeStorageInterval, TimeUnit.MILLISECONDS) + }, declareInterval, declareInterval, TimeUnit.NANOSECONDS) scheduleAtFixedRate(Runnable { tick(Starbound.SYSTEM_WORLD_TIMESTEP) @@ -356,6 +396,12 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread override fun performShutdown() { super.performShutdown() + systemWorlds.values.forEach { + if (it.isDone && !it.isCompletedExceptionally) { + it.get()?.save() + } + } + scope.cancel("Server shutting down") channels.close() 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 f400c9e5..6770aea2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt @@ -63,18 +63,6 @@ class ServerSystemWorld : SystemWorld { override val entities = HashMap() override val ships = HashMap() - private class Task(val supplier: Supplier) : Runnable { - val future = CompletableFuture() - - override fun run() { - try { - future.complete(supplier.get()) - } catch (err: Throwable) { - future.completeExceptionally(err) - } - } - } - override fun toString(): String { return "ServerSystemWorld at $systemLocation" } @@ -164,7 +152,7 @@ class ServerSystemWorld : SystemWorld { scope = CoroutineScope(server.coroutines + SupervisorJob()) } - private constructor(server: StarboundServer, data: JsonData) : super(data.location, server.universeClock, server.universe) { + private constructor(server: StarboundServer, location: Vector3i, data: JsonData) : super(location, server.universeClock, server.universe) { this.server = server objectSpawnTime = data.objectSpawnTime @@ -262,6 +250,11 @@ class ServerSystemWorld : SystemWorld { private var ticksWithoutPlayers = 0 private val tickSignal = Channel(120) private var tickSignaler: Future<*>? = null + private var saver: Future<*>? = null + + fun save() { + server.writeServerWorldData(location, toJson()) + } // system worlds are very lightweight, launching separate threads for them // is overkill; launch tick loop inside main server's thread @@ -274,7 +267,14 @@ class ServerSystemWorld : SystemWorld { Runnable { tickSignal.trySend(Starbound.SYSTEM_WORLD_TIMESTEP) }, Starbound.SYSTEM_WORLD_TIMESTEP_NANOS, Starbound.SYSTEM_WORLD_TIMESTEP_NANOS, - TimeUnit.NANOSECONDS) + TimeUnit.NANOSECONDS + ) + + saver = server.scheduleAtFixedRate( + Runnable { save() }, + 10L, 10L, + TimeUnit.SECONDS + ) scope.launch { while (ticksWithoutPlayers < 600) { @@ -291,6 +291,8 @@ class ServerSystemWorld : SystemWorld { LOGGER.info("Stopping system world at $location") tickSignaler?.cancel(false) + saver?.cancel(false) + server.writeServerWorldData(location, toJson()) server.notifySystemWorldUnloaded(location) } } @@ -569,6 +571,7 @@ class ServerSystemWorld : SystemWorld { world.launchTickLoop() } catch (err: Throwable) { world.tickSignaler?.cancel(false) + world.saver?.cancel(false) world.scope.cancel() world.tickSignal.close() throw err @@ -578,17 +581,23 @@ class ServerSystemWorld : SystemWorld { } } - suspend fun load(server: StarboundServer, data: JsonElement): ServerSystemWorld { + suspend fun load(server: StarboundServer, location: Vector3i, data: JsonElement): ServerSystemWorld { + LOGGER.info("Loading System World at $location") + val load = Starbound.gson.fromJson(data, JsonData::class.java) - LOGGER.info("Loading System World at ${load.location}") - val world = ServerSystemWorld(server, load) + if (load.location != location) { + throw IllegalStateException("Tried to load system world at $location, but serialized data tells us it is for ${load.location}!") + } + + val world = ServerSystemWorld(server, location, load) try { world.spawnObjects() world.launchTickLoop() } catch (err: Throwable) { world.tickSignaler?.cancel(false) + world.saver?.cancel(false) world.scope.cancel() world.tickSignal.close() throw err