Persistent system worlds
This commit is contained in:
parent
0fb5359521
commit
2a23c579bc
@ -22,10 +22,11 @@ import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters
|
|||||||
import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldParameters
|
import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldParameters
|
||||||
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
|
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
|
||||||
import ru.dbotthepony.kstarbound.json.readJsonElement
|
import ru.dbotthepony.kstarbound.json.readJsonElement
|
||||||
|
import ru.dbotthepony.kstarbound.json.readJsonElementInflated
|
||||||
import ru.dbotthepony.kstarbound.json.readJsonObject
|
import ru.dbotthepony.kstarbound.json.readJsonObject
|
||||||
import ru.dbotthepony.kstarbound.json.writeJsonElement
|
import ru.dbotthepony.kstarbound.json.writeJsonElement
|
||||||
|
import ru.dbotthepony.kstarbound.json.writeJsonElementDeflated
|
||||||
import ru.dbotthepony.kstarbound.json.writeJsonObject
|
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.LegacyWorldStorage
|
||||||
import ru.dbotthepony.kstarbound.server.world.ServerUniverse
|
import ru.dbotthepony.kstarbound.server.world.ServerUniverse
|
||||||
import ru.dbotthepony.kstarbound.server.world.ServerWorld
|
import ru.dbotthepony.kstarbound.server.world.ServerWorld
|
||||||
@ -42,6 +43,7 @@ import java.util.UUID
|
|||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread") {
|
sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread") {
|
||||||
private fun makedir(file: File) {
|
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 `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 `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 `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
|
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 writeMetadata = database.prepareStatement("REPLACE INTO `metadata` (`key`, `value`) VALUES (?, ?)")
|
||||||
private val lookupClientContext = database.prepareStatement("SELECT `data` FROM `client_context` WHERE `uuid` = ?")
|
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 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> {
|
private fun getMetadata(key: String): KOptional<JsonElement> {
|
||||||
lookupMetadata.setString(1, key)
|
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 settings = ServerSettings()
|
||||||
val channels = ServerChannels(this)
|
val channels = ServerChannels(this)
|
||||||
val lock = ReentrantLock()
|
val lock = ReentrantLock()
|
||||||
@ -146,7 +178,13 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
|
|||||||
|
|
||||||
private suspend fun loadSystemWorld0(location: Vector3i): ServerSystemWorld? {
|
private suspend fun loadSystemWorld0(location: Vector3i): ServerSystemWorld? {
|
||||||
try {
|
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) {
|
} catch (err: Throwable) {
|
||||||
LOGGER.error("Exception loading system world at $location", err)
|
LOGGER.error("Exception loading system world at $location", err)
|
||||||
return null
|
return null
|
||||||
@ -299,11 +337,13 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
val declareInterval = TimeUnit.MILLISECONDS.toNanos(Globals.universeServer.universeStorageInterval)
|
||||||
|
|
||||||
scheduleWithFixedDelay(Runnable {
|
scheduleWithFixedDelay(Runnable {
|
||||||
setMetadata("universe_clock", JsonPrimitive(universeClock.time))
|
setMetadata("universe_clock", JsonPrimitive(universeClock.time))
|
||||||
database.commit()
|
database.commit()
|
||||||
universe.flush()
|
universe.flush()
|
||||||
}, Globals.universeServer.universeStorageInterval, Globals.universeServer.universeStorageInterval, TimeUnit.MILLISECONDS)
|
}, declareInterval, declareInterval, TimeUnit.NANOSECONDS)
|
||||||
|
|
||||||
scheduleAtFixedRate(Runnable {
|
scheduleAtFixedRate(Runnable {
|
||||||
tick(Starbound.SYSTEM_WORLD_TIMESTEP)
|
tick(Starbound.SYSTEM_WORLD_TIMESTEP)
|
||||||
@ -356,6 +396,12 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
|
|||||||
override fun performShutdown() {
|
override fun performShutdown() {
|
||||||
super.performShutdown()
|
super.performShutdown()
|
||||||
|
|
||||||
|
systemWorlds.values.forEach {
|
||||||
|
if (it.isDone && !it.isCompletedExceptionally) {
|
||||||
|
it.get()?.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
scope.cancel("Server shutting down")
|
scope.cancel("Server shutting down")
|
||||||
channels.close()
|
channels.close()
|
||||||
|
|
||||||
|
@ -63,18 +63,6 @@ class ServerSystemWorld : SystemWorld {
|
|||||||
override val entities = HashMap<UUID, ServerEntity>()
|
override val entities = HashMap<UUID, ServerEntity>()
|
||||||
override val ships = HashMap<UUID, ServerShip>()
|
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 {
|
override fun toString(): String {
|
||||||
return "ServerSystemWorld at $systemLocation"
|
return "ServerSystemWorld at $systemLocation"
|
||||||
}
|
}
|
||||||
@ -164,7 +152,7 @@ class ServerSystemWorld : SystemWorld {
|
|||||||
scope = CoroutineScope(server.coroutines + SupervisorJob())
|
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
|
this.server = server
|
||||||
objectSpawnTime = data.objectSpawnTime
|
objectSpawnTime = data.objectSpawnTime
|
||||||
|
|
||||||
@ -262,6 +250,11 @@ class ServerSystemWorld : SystemWorld {
|
|||||||
private var ticksWithoutPlayers = 0
|
private var ticksWithoutPlayers = 0
|
||||||
private val tickSignal = Channel<Double>(120)
|
private val tickSignal = Channel<Double>(120)
|
||||||
private var tickSignaler: Future<*>? = null
|
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
|
// system worlds are very lightweight, launching separate threads for them
|
||||||
// is overkill; launch tick loop inside main server's thread
|
// is overkill; launch tick loop inside main server's thread
|
||||||
@ -274,7 +267,14 @@ class ServerSystemWorld : SystemWorld {
|
|||||||
Runnable { tickSignal.trySend(Starbound.SYSTEM_WORLD_TIMESTEP) },
|
Runnable { tickSignal.trySend(Starbound.SYSTEM_WORLD_TIMESTEP) },
|
||||||
Starbound.SYSTEM_WORLD_TIMESTEP_NANOS,
|
Starbound.SYSTEM_WORLD_TIMESTEP_NANOS,
|
||||||
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 {
|
scope.launch {
|
||||||
while (ticksWithoutPlayers < 600) {
|
while (ticksWithoutPlayers < 600) {
|
||||||
@ -291,6 +291,8 @@ class ServerSystemWorld : SystemWorld {
|
|||||||
|
|
||||||
LOGGER.info("Stopping system world at $location")
|
LOGGER.info("Stopping system world at $location")
|
||||||
tickSignaler?.cancel(false)
|
tickSignaler?.cancel(false)
|
||||||
|
saver?.cancel(false)
|
||||||
|
server.writeServerWorldData(location, toJson())
|
||||||
server.notifySystemWorldUnloaded(location)
|
server.notifySystemWorldUnloaded(location)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -569,6 +571,7 @@ class ServerSystemWorld : SystemWorld {
|
|||||||
world.launchTickLoop()
|
world.launchTickLoop()
|
||||||
} catch (err: Throwable) {
|
} catch (err: Throwable) {
|
||||||
world.tickSignaler?.cancel(false)
|
world.tickSignaler?.cancel(false)
|
||||||
|
world.saver?.cancel(false)
|
||||||
world.scope.cancel()
|
world.scope.cancel()
|
||||||
world.tickSignal.close()
|
world.tickSignal.close()
|
||||||
throw err
|
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)
|
val load = Starbound.gson.fromJson(data, JsonData::class.java)
|
||||||
|
|
||||||
LOGGER.info("Loading System World at ${load.location}")
|
if (load.location != location) {
|
||||||
val world = ServerSystemWorld(server, load)
|
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 {
|
try {
|
||||||
world.spawnObjects()
|
world.spawnObjects()
|
||||||
world.launchTickLoop()
|
world.launchTickLoop()
|
||||||
} catch (err: Throwable) {
|
} catch (err: Throwable) {
|
||||||
world.tickSignaler?.cancel(false)
|
world.tickSignaler?.cancel(false)
|
||||||
|
world.saver?.cancel(false)
|
||||||
world.scope.cancel()
|
world.scope.cancel()
|
||||||
world.tickSignal.close()
|
world.tickSignal.close()
|
||||||
throw err
|
throw err
|
||||||
|
Loading…
Reference in New Issue
Block a user