From f452cbeeb18bd29a38da8fe80ba04b81e8267c2b Mon Sep 17 00:00:00 2001
From: DBotThePony <dbotthepony@yandex.ru>
Date: Fri, 29 Mar 2024 18:01:17 +0700
Subject: [PATCH] Some polishing

---
 .../ru/dbotthepony/kstarbound/Starbound.kt    |  2 +-
 .../kstarbound/client/StarboundClient.kt      |  2 +-
 .../ru/dbotthepony/kstarbound/defs/WorldID.kt |  2 +-
 .../kstarbound/network/Connection.kt          |  2 +-
 .../kstarbound/server/ServerChannels.kt       |  2 +-
 .../kstarbound/server/ServerConnection.kt     | 21 +++---
 .../kstarbound/server/StarboundServer.kt      |  7 +-
 .../kstarbound/server/world/ServerWorld.kt    | 37 +++++++---
 .../server/world/ServerWorldTracker.kt        | 68 ++++++++++++++-----
 .../kstarbound/world/UniversePos.kt           |  2 +-
 10 files changed, 103 insertions(+), 42 deletions(-)

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 "<no channel>"
-		val ship = if (::shipWorld.isInitialized) shipWorld.toString() else "<no shipworld>"
-		return "ServerConnection[ID=$connectionID channel=$channel / $ship]"
+		val world = tracker?.world?.toString() ?: "<not in world>"
+		return "ServerConnection[$nickname $uuid ID=$connectionID channel=$channel / $world]"
 	}
 
 	private val shipChunks = HashMap<ByteKey, KOptional<ByteArray>>()
@@ -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<ServerWorld>()
 	val worlds = ConcurrentHashMap<WorldID, ServerWorld>()
-	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<ServerWorldTracker>()
@@ -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<ServerWorld> {
+			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<ChunkPos, Ticket>()
 	private val pendingSend = ObjectLinkedOpenHashSet<ChunkPos>()
 	private val tasks = ConcurrentLinkedQueue<ServerWorld.() -> 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