192 lines
5.5 KiB
Kotlin
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()
|
|
}
|
|
}
|