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>() 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>() private suspend fun loadSystemWorld0(location: Vector3i): ServerSystemWorld { return ServerSystemWorld.create(this, location) } fun loadSystemWorld(location: Vector3i): CompletableFuture { 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 { 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 { 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 { 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() 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() } }