KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt

192 lines
5.5 KiB
Kotlin

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<ServerWorld>()
val worlds = ConcurrentHashMap<WorldID, ServerWorld>()
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<Vector3i, CompletableFuture<ServerSystemWorld>>()
private suspend fun loadSystemWorld0(location: Vector3i): ServerSystemWorld {
return ServerSystemWorld.create(this, location)
}
fun loadSystemWorld(location: Vector3i): CompletableFuture<ServerSystemWorld> {
return CompletableFuture.supplyAsync(Supplier {
systemWorlds.computeIfAbsent(location) {
context.async { loadSystemWorld0(location) }.asCompletableFuture()
}
}, mailbox).thenCompose { it }
}
fun loadSystemWorld(location: UniversePos): CompletableFuture<ServerSystemWorld> {
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<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()
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()
}
}