diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 3cdedb3d..bc224339 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -110,7 +110,7 @@ object Starbound : ISBFileLocator { private val LOGGER = LogManager.getLogger() - val thread = Thread(::universeThread, "Starbound Universe") + val thread = Thread(::universeThread, "Universe") val mailbox = MailboxExecutorService(thread).also { it.exceptionHandler = ExceptionLogger(LOGGER) } val mailboxBootstrapped = MailboxExecutorService(thread).also { it.exceptionHandler = ExceptionLogger(LOGGER) } val mailboxInitialized = MailboxExecutorService(thread).also { it.exceptionHandler = ExceptionLogger(LOGGER) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index d5479e6e..26cd0aa3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -1064,7 +1064,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable { future.complete(client) client.spin() - }, "Starbound Client $clientID") + }, "Client Thread $clientID") thread.start() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldID.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldID.kt index 81d69f2f..4908b585 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldID.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldID.kt @@ -43,7 +43,7 @@ sealed class WorldID { } override fun toString(): String { - return "WorldID.ShipWorld[$uuid]" + return "WorldID.ShipWorld[${uuid.toString().substring(0, 8)}]" } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt index cae3d4d6..00286c7e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt @@ -242,7 +242,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : private val legacyWarpActionCodec = StreamCodec.Pair(WarpAction.LEGACY_CODEC, WarpMode.CODEC).koptional() val NIO_POOL by lazy { - NioEventLoopGroup(1, ThreadFactoryBuilder().setDaemon(true).setNameFormat("Starbound Network IO %d").build()) + NioEventLoopGroup(1, ThreadFactoryBuilder().setDaemon(true).setNameFormat("Network IO %d").build()) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerChannels.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerChannels.kt index e0d84419..c8302484 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerChannels.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerChannels.kt @@ -159,7 +159,7 @@ class ServerChannels(val server: StarboundServer) : Closeable { lock.withLock { if (isClosed) return - connections.forEach { it.disconnect("Server is stopping") } + connections.forEach { it.disconnect("Server shutting down") } channels.forEach { it.channel().close() } channels.clear() connections.clear() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt index de800ffa..201d6f96 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt @@ -30,6 +30,7 @@ import kotlin.properties.Delegates class ServerConnection(val server: StarboundServer, type: ConnectionType) : Connection(ConnectionSide.SERVER, type) { var tracker: ServerWorldTracker? = null var worldStartAcknowledged = false + var returnWarp: WarpAction? = null val world: ServerWorld? get() = tracker?.world @@ -53,8 +54,8 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn override fun toString(): String { val channel = if (hasChannel) channel.remoteAddress().toString() else "" - val ship = if (::shipWorld.isInitialized) shipWorld.toString() else "" - return "ServerConnection[ID=$connectionID channel=$channel / $ship]" + val world = tracker?.world?.toString() ?: "" + return "ServerConnection[$nickname $uuid ID=$connectionID channel=$channel / $world]" } private val shipChunks = HashMap>() @@ -142,6 +143,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn if (pendingWarp != null) { val (request, deploy) = pendingWarp + LOGGER.info("Trying to warp $this to $request") val resolve = request.resolve(this) @@ -231,13 +233,16 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn server.channels.incrementPlayerCount() if (isLegacy) { - LOGGER.info("Initializing ship world for $this") - ServerWorld.load(server, shipChunkSource, WorldID.ShipWorld(uuid!!)).thenAccept { - shipWorld = it - shipWorld.thread.start() - enqueueWarp(WarpAlias.OwnShip) - warpingAllowed = true + if (!isConnected || !channel.isOpen) { + LOGGER.warn("$this disconnected before loaded their ShipWorld") + it.close() + } else { + shipWorld = it + shipWorld.thread.start() + enqueueWarp(WarpAlias.OwnShip) + warpingAllowed = true + } }.exceptionally { LOGGER.error("Error while initializing shipworld for $this", it) disconnect("Error while initializing shipworld for player: $it") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt index 39656e4f..257af535 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt @@ -30,11 +30,12 @@ sealed class StarboundServer(val root: File) : Closeable { } } + val limboWorldIndex = AtomicInteger() + val limboWorlds = CopyOnWriteArrayList() val worlds = ConcurrentHashMap() - val serverID = threadCounter.getAndIncrement() val mailbox = MailboxExecutorService().also { it.exceptionHandler = ExceptionLogger(LOGGER) } val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::tick, Starbound.TIMESTEP_NANOS) - val thread = Thread(spinner, "Server $serverID Thread") + val thread = Thread(spinner, "Server Thread") val universe = ServerUniverse() val chat = ChatHandler(this) @@ -117,6 +118,7 @@ sealed class StarboundServer(val root: File) : Closeable { channels.close() worlds.values.forEach { it.close() } + limboWorlds.forEach { it.close() } universe.close() close0() } @@ -130,7 +132,6 @@ sealed class StarboundServer(val root: File) : Closeable { } companion object { - private val threadCounter = AtomicInteger() private val LOGGER = LogManager.getLogger() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index c09c6461..793db4ab 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -9,6 +9,7 @@ import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.SpawnTarget import ru.dbotthepony.kstarbound.defs.WarpAction import ru.dbotthepony.kstarbound.defs.WorldID import ru.dbotthepony.kstarbound.defs.tile.TileDamage @@ -19,6 +20,7 @@ import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.packets.StepUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStopPacket import ru.dbotthepony.kstarbound.server.StarboundServer import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.util.AssetPathStack @@ -51,10 +53,14 @@ class ServerWorld private constructor( if (server.isClosed) throw RuntimeException() - if (server.worlds.containsKey(worldID)) - throw IllegalStateException("Duplicate world ID: $worldID") + if (worldID != WorldID.Limbo) { + if (server.worlds.containsKey(worldID)) + throw IllegalStateException("Duplicate world ID: $worldID") - server.worlds[worldID] = this + server.worlds[worldID] = this + } else { + server.limboWorlds.add(this) + } } val players = CopyOnWriteArrayList() @@ -96,7 +102,8 @@ class ServerWorld private constructor( } val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TIMESTEP_NANOS) - val thread = Thread(spinner, "Server World $worldID") + private val str = "Server World ${if (worldID == WorldID.Limbo) "limbo(${server.limboWorldIndex.getAndIncrement()})" else worldID.toString()}" + val thread = Thread(spinner, str) val ticketListLock = ReentrantLock() private val isClosed = AtomicBoolean() @@ -115,19 +122,25 @@ class ServerWorld private constructor( override fun toString(): String { if (isClosed.get()) - return "NULL ServerWorld at $worldID" + return "NULL $str" else - return "ServerWorld at $worldID" + return str } override fun close() { - if (isClosed.compareAndSet(false, true)) { + if (!isClosed.get()) LOGGER.info("Shutting down $this") + if (isClosed.compareAndSet(false, true)) { super.close() spinner.unpause() players.forEach { it.remove() } - server.worlds.remove(worldID) + + if (worldID != WorldID.Limbo) + server.worlds.remove(worldID) + else + server.limboWorlds.remove(this) + LockSupport.unpark(thread) } } @@ -177,7 +190,7 @@ class ServerWorld private constructor( try { it.tick() } catch (err: Throwable) { - LOGGER.error("Exception while ticking player $it", err) + LOGGER.error("Exception while ticking player ${it.client}", err) //it.disconnect("Exception while ticking player: $err") } } @@ -450,7 +463,10 @@ class ServerWorld private constructor( } fun load(server: StarboundServer, storage: WorldStorage, worldID: WorldID = WorldID.Limbo): CompletableFuture { + LOGGER.info("Attempting to load world at $worldID") + return storage.loadMetadata().thenApply { + LOGGER.info("Loading world at $worldID") AssetPathStack("/") { _ -> val meta = it.map { Starbound.gson.fromJson(it.data.content, MetadataJson::class.java) }.orThrow { NoSuchElementException("No world metadata is present") } @@ -462,6 +478,9 @@ class ServerWorld private constructor( world.protectedDungeonIDs.addAll(meta.protectedDungeonIds) world } + }.exceptionally { + LOGGER.error("Error while instancing world $worldID", it) + null } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt index 2bf85ba4..c52c7759 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt @@ -6,10 +6,14 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMaps import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet +import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket +import ru.dbotthepony.kstarbound.defs.SpawnTarget +import ru.dbotthepony.kstarbound.defs.WarpAction +import ru.dbotthepony.kstarbound.defs.WorldID import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket import ru.dbotthepony.kstarbound.network.packets.EntityUpdateSetPacket @@ -18,6 +22,7 @@ import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpda import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStopPacket import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.IChunkListener @@ -34,13 +39,17 @@ import java.util.concurrent.atomic.AtomicBoolean // allowing ServerConnection client to track ServerWorld state class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, playerStart: Vector2d) { init { + LOGGER.info("$client is joining $world") + client.worldStartAcknowledged = false client.tracker = this + client.worldID = world.worldID } var skyVersion = 0L private val isRemoved = AtomicBoolean() + private var isActuallyRemoved = false private val tickets = HashMap() private val pendingSend = ObjectLinkedOpenHashSet() private val tasks = ConcurrentLinkedQueue Unit>() @@ -55,7 +64,8 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p // packets which interact with world must be // executed on world's thread fun enqueue(task: ServerWorld.() -> Unit) { - tasks.add(task) + if (!isRemoved.get()) + tasks.add(task) } private inner class Ticket(val ticket: ServerWorld.ITicket, val pos: ChunkPos) : IChunkListener { @@ -109,14 +119,21 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p return if (!client.channel.isOpen) { - remove() // ??? + // ??? + remove() + remove0() + return + } + + if (isRemoved.get()) { + remove0() return } run { var next = tasks.poll() - while (next != null) { + while (next != null && !isRemoved.get()) { next.invoke(world) next = tasks.poll() } @@ -194,22 +211,41 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p } } - fun remove() { - if (isRemoved.compareAndSet(false, true)) { - client.tracker = null - client.playerEntity = null - world.players.remove(this) - tickets.values.forEach { it.ticket.cancel() } + private fun remove0() { + if (isActuallyRemoved) return - world.mailbox.execute { - val itr = world.entities.int2ObjectEntrySet().iterator() + isActuallyRemoved = true + world.players.remove(this) + tickets.values.forEach { it.ticket.cancel() } - for ((id, entity) in itr) { - if (id in client.entityIDRange) { - entity.remove() - } - } + val itr = world.entities.int2ObjectEntrySet().iterator() + + for ((id, entity) in itr) { + if (id in client.entityIDRange) { + entity.remove() } } } + + fun remove() { + if (isRemoved.compareAndSet(false, true)) { + // erase all tasks just to be sure + tasks.clear() + + val playerEntity = client.playerEntity + + if (playerEntity != null) { + client.returnWarp = WarpAction.World(world.worldID, SpawnTarget.Position(playerEntity.position)) + } + + client.tracker = null + client.playerEntity = null + client.worldID = WorldID.Limbo + client.send(WorldStopPacket("Removed")) + } + } + + companion object { + private val LOGGER = LogManager.getLogger() + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt index be435890..3706ca9b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt @@ -41,7 +41,7 @@ data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit: } override fun toString(): String { - return "UniversePos[$location, planetOrbit=$planetOrbit, satelliteOrbit=$satelliteOrbit]" + return "${location.x},${location.y}${location.z}:$planetOrbit:$satelliteOrbit" } val isSystem: Boolean