Persistent system worlds

This commit is contained in:
DBotThePony 2024-04-24 11:50:46 +07:00
parent 0fb5359521
commit 2a23c579bc
Signed by: DBot
GPG Key ID: DCC23B5715498507
2 changed files with 75 additions and 20 deletions

View File

@ -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<JsonElement> {
lookupMetadata.setString(1, key)
@ -123,6 +128,33 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
}
}
fun loadServerWorldData(pos: Vector3i): CompletableFuture<JsonElement?> {
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()

View File

@ -63,18 +63,6 @@ class ServerSystemWorld : SystemWorld {
override val entities = HashMap<UUID, ServerEntity>()
override val ships = HashMap<UUID, ServerShip>()
private class Task<T>(val supplier: Supplier<T>) : Runnable {
val future = CompletableFuture<T>()
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<Double>(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