317 lines
9.1 KiB
Kotlin
317 lines
9.1 KiB
Kotlin
package ru.dbotthepony.kstarbound.server
|
|
|
|
import com.google.gson.JsonPrimitive
|
|
import it.unimi.dsi.fastutil.objects.ObjectArraySet
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.SupervisorJob
|
|
import kotlinx.coroutines.async
|
|
import kotlinx.coroutines.cancel
|
|
import kotlinx.coroutines.future.asCompletableFuture
|
|
import kotlinx.coroutines.future.await
|
|
import kotlinx.coroutines.launch
|
|
import org.apache.logging.log4j.LogManager
|
|
import ru.dbotthepony.kstarbound.math.vector.Vector3i
|
|
import ru.dbotthepony.kstarbound.Globals
|
|
import ru.dbotthepony.kstarbound.Starbound
|
|
import ru.dbotthepony.kstarbound.defs.WorldID
|
|
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.network.packets.clientbound.UniverseTimeUpdatePacket
|
|
import ru.dbotthepony.kstarbound.server.world.ServerUniverse
|
|
import ru.dbotthepony.kstarbound.server.world.ServerWorld
|
|
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.random.random
|
|
import ru.dbotthepony.kstarbound.world.UniversePos
|
|
import java.io.File
|
|
import java.util.UUID
|
|
import java.util.concurrent.CompletableFuture
|
|
import java.util.concurrent.TimeUnit
|
|
import java.util.concurrent.locks.ReentrantLock
|
|
|
|
sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread") {
|
|
init {
|
|
if (!root.exists()) {
|
|
check(root.mkdirs()) { "Unable to create ${root.absolutePath}" }
|
|
} else if (!root.isDirectory) {
|
|
throw IllegalArgumentException("${root.absolutePath} is not a directory")
|
|
}
|
|
}
|
|
|
|
private val worlds = HashMap<WorldID, CompletableFuture<ServerWorld>>()
|
|
val universe = ServerUniverse()
|
|
val chat = ChatHandler(this)
|
|
val globalScope = CoroutineScope(Starbound.COROUTINE_EXECUTOR + SupervisorJob())
|
|
|
|
val settings = ServerSettings()
|
|
val channels = ServerChannels(this)
|
|
val lock = ReentrantLock()
|
|
var isClosed = false
|
|
private set
|
|
|
|
var serverUUID: UUID = UUID.randomUUID()
|
|
protected set
|
|
|
|
val universeClock = JVMClock()
|
|
|
|
private val systemWorlds = HashMap<Vector3i, CompletableFuture<ServerSystemWorld>>()
|
|
|
|
private suspend fun loadSystemWorld0(location: Vector3i): ServerSystemWorld {
|
|
return ServerSystemWorld.create(this, location)
|
|
}
|
|
|
|
fun loadSystemWorld(location: Vector3i): CompletableFuture<ServerSystemWorld> {
|
|
return supplyAsync {
|
|
systemWorlds.computeIfAbsent(location) {
|
|
globalScope.async { loadSystemWorld0(location) }.asCompletableFuture()
|
|
}
|
|
}.thenCompose { it }
|
|
}
|
|
|
|
private suspend fun loadCelestialWorld(location: WorldID.Celestial): ServerWorld {
|
|
LOGGER.info("Creating celestial world $location")
|
|
|
|
val template = WorldTemplate.create(location.pos, universe)
|
|
val world = ServerWorld.create(this, template, WorldStorage.Nothing, location)
|
|
|
|
try {
|
|
world.sky.referenceClock = universeClock
|
|
world.eventLoop.start()
|
|
world.prepare().await()
|
|
} catch (err: Throwable) {
|
|
LOGGER.fatal("Exception while creating celestial world at $location!", err)
|
|
world.eventLoop.shutdown()
|
|
throw err
|
|
}
|
|
|
|
return world
|
|
}
|
|
|
|
private suspend fun loadInstanceWorld(location: WorldID.Instance): ServerWorld {
|
|
val config = Globals.instanceWorlds[location.name] ?: throw NoSuchElementException("No such instance world ${location.name}")
|
|
LOGGER.info("Creating instance world $location")
|
|
val random = random(config.seed ?: System.nanoTime())
|
|
|
|
val visitable = when (config.type.lowercase()) {
|
|
"terrestrial" -> TerrestrialWorldParameters.generate(config.planetType!!, config.planetSize!!, random)
|
|
"asteroids" -> AsteroidsWorldParameters.generate(random)
|
|
"floatingdungeon" -> FloatingDungeonWorldParameters.generate(config.dungeonWorld!!)
|
|
else -> throw RuntimeException()
|
|
}
|
|
|
|
if (location.threatLevel != null) {
|
|
visitable.threatLevel = location.threatLevel
|
|
}
|
|
|
|
if (config.beamUpRule != null) {
|
|
visitable.beamUpRule = config.beamUpRule
|
|
}
|
|
|
|
visitable.disableDeathDrops = config.disableDeathDrops
|
|
|
|
val template = WorldTemplate(visitable, config.skyParameters, random)
|
|
val world = ServerWorld.create(this, template, WorldStorage.NULL, location)
|
|
|
|
try {
|
|
world.setProperty("ephemeral", JsonPrimitive(!config.persistent))
|
|
|
|
if (config.useUniverseClock)
|
|
world.sky.referenceClock = universeClock
|
|
|
|
world.eventLoop.start()
|
|
world.prepare().await()
|
|
} catch (err: Throwable) {
|
|
LOGGER.fatal("Exception while creating instance world at $location!", err)
|
|
world.eventLoop.shutdown()
|
|
throw err
|
|
}
|
|
|
|
return world
|
|
}
|
|
|
|
private suspend fun loadWorld0(location: WorldID): ServerWorld {
|
|
return when (location) {
|
|
is WorldID.ShipWorld -> throw IllegalArgumentException("Can't create ship worlds out of thin air")
|
|
is WorldID.Instance -> loadInstanceWorld(location)
|
|
is WorldID.Celestial -> loadCelestialWorld(location)
|
|
is WorldID.Limbo -> throw IllegalArgumentException("Limbo was supplied as world ID")
|
|
}
|
|
}
|
|
|
|
fun loadWorld(location: WorldID): CompletableFuture<ServerWorld> {
|
|
return supplyAsync {
|
|
var world = worlds[location]
|
|
|
|
if (world != null && world.isCompletedExceptionally) {
|
|
worlds.remove(location)
|
|
world = null
|
|
}
|
|
|
|
if (world != null) {
|
|
world
|
|
} else {
|
|
val future = globalScope.async { loadWorld0(location) }.asCompletableFuture()
|
|
worlds[location] = future
|
|
future
|
|
}
|
|
}.thenCompose { it }
|
|
}
|
|
|
|
fun loadShipWorld(connection: ServerConnection, storage: WorldStorage): CompletableFuture<ServerWorld> {
|
|
return supplyAsync {
|
|
val id = WorldID.ShipWorld(connection.uuid ?: throw NullPointerException("Connection UUID is null"))
|
|
val existing = worlds[id]
|
|
|
|
if (existing != null)
|
|
throw IllegalStateException("Already has $id!")
|
|
|
|
val world = ServerWorld.load(this, storage, id)
|
|
worlds[id] = world
|
|
world
|
|
}.thenCompose { it }
|
|
}
|
|
|
|
fun notifyWorldUnloaded(worldID: WorldID) {
|
|
execute {
|
|
worlds.remove(worldID)
|
|
}
|
|
}
|
|
|
|
fun loadSystemWorld(location: UniversePos): CompletableFuture<ServerSystemWorld> {
|
|
return loadSystemWorld(location.location)
|
|
}
|
|
|
|
init {
|
|
scheduleAtFixedRate(Runnable {
|
|
channels.broadcast(UniverseTimeUpdatePacket(universeClock.time), false)
|
|
}, Globals.universeServer.clockUpdatePacketInterval, Globals.universeServer.clockUpdatePacketInterval, TimeUnit.MILLISECONDS)
|
|
|
|
scheduleAtFixedRate(Runnable {
|
|
tickNormal(Starbound.TIMESTEP)
|
|
}, Starbound.TIMESTEP_NANOS, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS)
|
|
|
|
scheduleAtFixedRate(Runnable {
|
|
tickSystemWorlds()
|
|
}, Starbound.SYSTEM_WORLD_TIMESTEP_NANOS, Starbound.SYSTEM_WORLD_TIMESTEP_NANOS, TimeUnit.NANOSECONDS)
|
|
|
|
isDaemon = false
|
|
start()
|
|
}
|
|
|
|
private val occupiedNicknames = ObjectArraySet<String>()
|
|
|
|
fun reserveNickname(name: String, alternative: String): String {
|
|
synchronized(occupiedNicknames) {
|
|
var name = name
|
|
|
|
if (name.lowercase() == "server" || name.isBlank()) {
|
|
name = alternative
|
|
}
|
|
|
|
while (name in occupiedNicknames) {
|
|
name += "_"
|
|
}
|
|
|
|
occupiedNicknames.add(name)
|
|
return name
|
|
}
|
|
}
|
|
|
|
fun freeNickname(name: String): Boolean {
|
|
return synchronized(occupiedNicknames) {
|
|
occupiedNicknames.remove(name)
|
|
}
|
|
}
|
|
|
|
fun clientByUUID(uuid: UUID): ServerConnection? {
|
|
return channels.connections.firstOrNull { it.uuid == uuid }
|
|
}
|
|
|
|
protected abstract fun close0()
|
|
protected abstract fun tick0(delta: Double)
|
|
|
|
private fun tickSystemWorlds() {
|
|
systemWorlds.values.removeIf {
|
|
if (it.isCompletedExceptionally) {
|
|
return@removeIf true
|
|
}
|
|
|
|
if (!it.isDone) {
|
|
return@removeIf false
|
|
}
|
|
|
|
scope.launch {
|
|
try {
|
|
it.get().tick(Starbound.SYSTEM_WORLD_TIMESTEP)
|
|
} catch (err: Throwable) {
|
|
LOGGER.fatal("Exception in system world $it event loop", err)
|
|
}
|
|
}
|
|
|
|
if (it.get().shouldClose()) {
|
|
LOGGER.info("Stopping idling ${it.get()}")
|
|
return@removeIf true
|
|
}
|
|
|
|
return@removeIf false
|
|
}
|
|
}
|
|
|
|
private fun tickNormal(delta: Double) {
|
|
try {
|
|
// universeClock.nanos += Starbound.TIMESTEP_NANOS
|
|
|
|
channels.connections.forEach {
|
|
try {
|
|
it.tick()
|
|
} catch (err: Throwable) {
|
|
LOGGER.error("Exception while ticking client connection", err)
|
|
it.disconnect("Exception while ticking client connection: $err")
|
|
}
|
|
}
|
|
|
|
tick0(delta)
|
|
} catch (err: Throwable) {
|
|
LOGGER.fatal("Exception in main server event loop", err)
|
|
shutdown()
|
|
}
|
|
}
|
|
|
|
override fun performShutdown() {
|
|
super.performShutdown()
|
|
|
|
scope.cancel("Server shutting down")
|
|
channels.close()
|
|
|
|
worlds.values.forEach {
|
|
if (it.isDone && !it.isCompletedExceptionally) {
|
|
it.get().eventLoop.shutdown()
|
|
}
|
|
}
|
|
|
|
worlds.values.forEach {
|
|
if (it.isDone && !it.isCompletedExceptionally) {
|
|
it.get().eventLoop.awaitTermination(10L, TimeUnit.SECONDS)
|
|
|
|
if (!it.get().eventLoop.isTerminated) {
|
|
LOGGER.warn("World ${it.get()} did not shutdown in 10 seconds, forcing termination. This might leave world in inconsistent state!")
|
|
it.get().eventLoop.shutdownNow()
|
|
}
|
|
}
|
|
|
|
it.cancel(true)
|
|
}
|
|
|
|
universe.close()
|
|
close0()
|
|
}
|
|
|
|
companion object {
|
|
private val LOGGER = LogManager.getLogger()
|
|
}
|
|
}
|