package ru.dbotthepony.kstarbound.server import it.unimi.dsi.fastutil.objects.ObjectArraySet import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.cancel import kotlinx.coroutines.future.asCompletableFuture import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.MailboxExecutorService import ru.dbotthepony.kommons.vector.Vector3i import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.WorldID 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.util.Clock import ru.dbotthepony.kstarbound.util.ExceptionLogger import ru.dbotthepony.kstarbound.util.ExecutionSpinner import ru.dbotthepony.kstarbound.world.UniversePos import java.io.Closeable import java.io.File import java.util.UUID import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock import java.util.function.Supplier sealed class StarboundServer(val root: File) : Closeable { init { if (!root.exists()) { check(root.mkdirs()) { "Unable to create ${root.absolutePath}" } } else if (!root.isDirectory) { throw IllegalArgumentException("${root.absolutePath} is not a directory") } } val limboWorldIndex = AtomicInteger() val limboWorlds = CopyOnWriteArrayList() val worlds = ConcurrentHashMap() val mailbox = MailboxExecutorService().also { it.exceptionHandler = ExceptionLogger(LOGGER) } val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::tick, Starbound.TIMESTEP_NANOS) val thread = Thread(spinner, "Server Thread") val universe = ServerUniverse() val chat = ChatHandler(this) val context = CoroutineScope(Starbound.COROUTINE_EXECUTOR) private val systemWorlds = HashMap>() private suspend fun loadSystemWorld0(location: Vector3i): ServerSystemWorld { return ServerSystemWorld.create(this, location) } fun loadSystemWorld(location: Vector3i): CompletableFuture { return CompletableFuture.supplyAsync(Supplier { systemWorlds.computeIfAbsent(location) { context.async { loadSystemWorld0(location) }.asCompletableFuture() } }, mailbox).thenCompose { it } } fun loadSystemWorld(location: UniversePos): CompletableFuture { return loadSystemWorld(location.location) } val settings = ServerSettings() val channels = ServerChannels(this) val lock = ReentrantLock() var isClosed = false private set var serverUUID: UUID = UUID.randomUUID() protected set val universeClock = Clock() init { mailbox.scheduleAtFixedRate(Runnable { channels.broadcast(UniverseTimeUpdatePacket(universeClock.seconds)) }, Globals.universeServer.clockUpdatePacketInterval, Globals.universeServer.clockUpdatePacketInterval, TimeUnit.MILLISECONDS) thread.uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { t, e -> LOGGER.fatal("Unexpected exception in server execution loop, shutting down", e) actuallyClose() } // thread.isDaemon = this is IntegratedStarboundServer thread.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() private fun tick(): Boolean { if (isClosed) return false 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") } } // TODO: schedule to thread pool? // right now, system worlds are rather lightweight, and having separate threads for them is overkill runBlocking { systemWorlds.values.removeIf { if (it.isCompletedExceptionally) { return@removeIf true } if (!it.isDone) { return@removeIf false } launch { it.get().tick() } if (it.get().shouldClose()) { LOGGER.info("Stopping idling ${it.get()}") return@removeIf true } return@removeIf false } } tick0() return !isClosed } private fun actuallyClose() { if (isClosed) return isClosed = true context.cancel("Server shutting down") channels.close() worlds.values.forEach { it.close() } limboWorlds.forEach { it.close() } universe.close() close0() } final override fun close() { if (Thread.currentThread() == thread) { actuallyClose() } else { mailbox.execute { actuallyClose() } } } companion object { private val LOGGER = LogManager.getLogger() } }