From db09de857b657661f74125308fea70ab8f79a34d Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Thu, 4 Apr 2024 00:31:57 +0700 Subject: [PATCH] SystemWorld, fixed MWCRandom, event loops, universe io --- .../kotlin/ru/dbotthepony/kstarbound/Ext.kt | 2 + .../dbotthepony/kstarbound/GlobalDefaults.kt | 10 + .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 3 +- .../ru/dbotthepony/kstarbound/Starbound.kt | 6 + .../kstarbound/client/StarboundClient.kt | 12 +- .../kstarbound/defs/InteractAction.kt | 47 ++ .../kstarbound/defs/InteractRequest.kt | 23 + .../kstarbound/defs/PlayerWarping.kt | 117 +++- .../kstarbound/defs/UniverseServerConfig.kt | 43 +- .../ru/dbotthepony/kstarbound/defs/WorldID.kt | 65 ++- .../defs/actor/player/ShipUpgrades.kt | 31 +- .../defs/world/AsteroidsWorldParameters.kt | 26 + .../defs/world/CelestialParameters.kt | 55 ++ .../kstarbound/defs/world/CelestialPlanet.kt | 7 - .../world/FloatingDungeonWorldParameters.kt | 84 +++ .../dbotthepony/kstarbound/defs/world/Sky.kt | 27 +- .../defs/world/SystemWorldConfig.kt | 41 ++ .../defs/world/SystemWorldObjectConfig.kt | 62 +++ .../defs/world/TerrestrialWorldParameters.kt | 133 ++++- .../defs/world/VisitableWorldParameters.kt | 112 ++++ .../ru/dbotthepony/kstarbound/io/Streams.kt | 23 + .../kstarbound/network/Connection.kt | 12 +- .../kstarbound/network/PacketRegistry.kt | 36 +- .../clientbound/CelestialResponsePacket.kt | 91 +++ .../clientbound/EntityInteractResultPacket.kt | 34 ++ .../clientbound/SystemObjectCreatePacket.kt | 21 + .../clientbound/SystemObjectDestroyPacket.kt | 21 + .../clientbound/SystemShipCreatePacket.kt | 21 + .../clientbound/SystemShipDestroyPacket.kt | 21 + .../clientbound/SystemWorldStartPacket.kt | 40 ++ .../clientbound/SystemWorldUpdatePacket.kt | 30 + .../UpdateWorldPropertiesPacket.kt | 34 ++ .../serverbound/CelestialRequestPacket.kt | 62 +++ .../serverbound/EntityInteractPacket.kt | 47 ++ .../packets/serverbound/FlyShipPacket.kt | 23 + .../kstarbound/network/syncher/Factories.kt | 2 +- .../kstarbound/server/ServerChannels.kt | 4 + .../kstarbound/server/ServerConnection.kt | 166 +++++- .../kstarbound/server/StarboundServer.kt | 54 ++ .../server/world/ServerSystemWorld.kt | 516 ++++++++++++++++++ .../kstarbound/server/world/ServerUniverse.kt | 63 ++- .../kstarbound/server/world/ServerWorld.kt | 13 + .../server/world/ServerWorldTracker.kt | 16 +- .../kstarbound/server/world/UniverseChunk.kt | 22 +- .../kstarbound/server/world/UniverseSource.kt | 6 +- .../ru/dbotthepony/kstarbound/util/Utils.kt | 15 +- .../kstarbound/util/random/MWCRandom.kt | 71 ++- .../kstarbound/util/random/RandomUtils.kt | 2 +- .../kstarbound/world/CoordinateMapper.kt | 41 ++ .../ru/dbotthepony/kstarbound/world/Sky.kt | 41 +- .../kstarbound/world/SystemWorld.kt | 292 ++++++++++ .../kstarbound/world/SystemWorldLocation.kt | 180 ++++++ .../dbotthepony/kstarbound/world/Universe.kt | 11 +- .../kstarbound/world/UniversePos.kt | 66 ++- .../ru/dbotthepony/kstarbound/world/World.kt | 24 +- .../kstarbound/world/WorldGeometry.kt | 8 + .../world/entities/AbstractEntity.kt | 6 + .../world/entities/tile/WorldObject.kt | 22 + 58 files changed, 2920 insertions(+), 143 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/InteractAction.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/InteractRequest.kt delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialPlanet.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SystemWorldConfig.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SystemWorldObjectConfig.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/CelestialResponsePacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/EntityInteractResultPacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemObjectCreatePacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemObjectDestroyPacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemShipCreatePacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemShipDestroyPacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemWorldStartPacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemWorldUpdatePacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/UpdateWorldPropertiesPacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/CelestialRequestPacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/EntityInteractPacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/FlyShipPacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorld.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorldLocation.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt index eb2b717c..bf963914 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt @@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound import com.google.common.collect.ImmutableMap import com.google.gson.Gson import com.google.gson.GsonBuilder +import com.google.gson.JsonElement import com.google.gson.TypeAdapter import com.google.gson.TypeAdapterFactory import com.google.gson.reflect.TypeToken @@ -48,6 +49,7 @@ operator fun ImmutableMap.Builder.set(key: K, value: V): ImmutableM fun String.sintern(): String = Starbound.STRINGS.intern(this) inline fun Gson.fromJson(reader: JsonReader): T? = fromJson(reader, T::class.java) +inline fun Gson.fromJson(reader: JsonElement): T? = fromJson(reader, T::class.java) /** * guarantees even distribution of tasks while also preserving encountered order of elements diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt index cfbf11ad..b008e2f0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt @@ -14,6 +14,8 @@ import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig import ru.dbotthepony.kstarbound.defs.world.DungeonWorldsConfig import ru.dbotthepony.kstarbound.defs.world.SkyGlobalConfig +import ru.dbotthepony.kstarbound.defs.world.SystemWorldConfig +import ru.dbotthepony.kstarbound.defs.world.SystemWorldObjectConfig import ru.dbotthepony.kstarbound.defs.world.WorldTemplateConfig import ru.dbotthepony.kstarbound.json.mapAdapter import ru.dbotthepony.kstarbound.util.AssetPathStack @@ -70,6 +72,12 @@ object GlobalDefaults { var currencies by Delegates.notNull>() private set + var systemObjects by Delegates.notNull>() + private set + + var systemWorld by Delegates.notNull() + private set + private object EmptyTask : ForkJoinTask() { private fun readResolve(): Any = EmptyTask override fun getRawResult() { @@ -119,6 +127,7 @@ object GlobalDefaults { tasks.add(load("/sky.config", ::sky)) tasks.add(load("/universe_server.config", ::universeServer)) tasks.add(load("/player.config", ::player)) + tasks.add(load("/systemworld.config", ::systemWorld)) tasks.add(load("/plants/grassDamage.config", ::grassDamage)) tasks.add(load("/plants/treeDamage.config", ::treeDamage)) @@ -127,6 +136,7 @@ object GlobalDefaults { tasks.add(load("/dungeon_worlds.config", ::dungeonWorlds, Starbound.gson.mapAdapter())) tasks.add(load("/currencies.config", ::currencies, Starbound.gson.mapAdapter())) + tasks.add(load("/system_objects.config", ::systemObjects, Starbound.gson.mapAdapter())) return tasks } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 73fad6a4..d4da0f74 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -13,6 +13,7 @@ import ru.dbotthepony.kstarbound.server.IntegratedStarboundServer import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage import ru.dbotthepony.kstarbound.server.world.ServerUniverse import ru.dbotthepony.kstarbound.server.world.ServerWorld +import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.staticRandomDouble import ru.dbotthepony.kstarbound.world.WorldGeometry import java.io.BufferedInputStream @@ -34,7 +35,7 @@ fun main() { val t = System.nanoTime() val result = Starbound.COROUTINES.future { - val systems = data.scanSystems(AABBi(Vector2i(-50, -50), Vector2i(50, 50)), setOf("whitestar")) + val systems = data.findSystems(AABBi(Vector2i(-50, -50), Vector2i(50, 50)), setOf("whitestar")) for (system in systems) { for (children in data.children(system)) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 4e4566cd..2a823c01 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -408,6 +408,10 @@ object Starbound : ISBFileLocator { private set var loadingProgress = 0.0 private set + var toLoad = 0 + private set + var loaded = 0 + private set @Volatile var terminateLoading = false @@ -571,10 +575,12 @@ object Starbound : ISBFileLocator { tasks.add(VersionRegistry.load()) val total = tasks.size.toDouble() + toLoad = tasks.size while (tasks.isNotEmpty()) { tasks.removeIf { it.isDone } checkMailbox() + loaded = toLoad - tasks.size loadingProgress = (total - tasks.size) / total LockSupport.parkNanos(5_000_000L) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index 26cd0aa3..8beeb78f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -231,7 +231,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable { GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE) GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GLFW.GLFW_TRUE) - window = GLFW.glfwCreateWindow(800, 600, "KStarbound", MemoryUtil.NULL, MemoryUtil.NULL) + window = GLFW.glfwCreateWindow(800, 600, "KStarbound: Locating files...", MemoryUtil.NULL, MemoryUtil.NULL) require(window != MemoryUtil.NULL) { "Unable to create GLFW window" } input.installCallback(window) @@ -760,6 +760,8 @@ class StarboundClient private constructor(val clientID: Int) : Closeable { if (!onlyMemory) font.render("OGL C: $openglObjectsCreated D: $openglObjectsCleaned A: ${openglObjectsCreated - openglObjectsCleaned}", y = font.lineHeight * 1.8f, scale = 0.4f) } + private var renderedLoadingScreen = false + private fun renderLoadingScreen() { executeQueuedTasks() updateViewportParams() @@ -805,6 +807,9 @@ class StarboundClient private constructor(val clientID: Int) : Closeable { builder.builder.quad(0f, viewportHeight - 20f, viewportWidth * Starbound.loadingProgress.toFloat(), viewportHeight.toFloat()) { color(RGBAColor.GREEN) } + GLFW.glfwSetWindowTitle(window, "KStarbound: Loading JSON assets ${Starbound.loaded} / ${Starbound.toLoad}") + renderedLoadingScreen = true + val runtime = Runtime.getRuntime() //if (runtime.maxMemory() <= 4L * 1024L * 1024L * 1024L) { @@ -925,6 +930,11 @@ class StarboundClient private constructor(val clientID: Int) : Closeable { return true } + if (renderedLoadingScreen) { + renderedLoadingScreen = false + GLFW.glfwSetWindowTitle(window, "KStarbound") + } + input.think() camera.think(Starbound.TIMESTEP) executeQueuedTasks() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/InteractAction.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/InteractAction.kt new file mode 100644 index 00000000..79efc5eb --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/InteractAction.kt @@ -0,0 +1,47 @@ +package ru.dbotthepony.kstarbound.defs + +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +import ru.dbotthepony.kstarbound.json.readJsonElement +import ru.dbotthepony.kstarbound.json.writeJsonElement +import java.io.DataInputStream +import java.io.DataOutputStream + +data class InteractAction(val type: Type = Type.NONE, val entityID: Int = 0, val data: JsonElement = JsonNull.INSTANCE) { + // int32_t + enum class Type(override val jsonName: String) : IStringSerializable { + NONE("None"), + OPEN_CONTAINER("OpenContainer"), + SIT_DOWN("SitDown"), + OPEN_CRAFTING_INTERFACE("OpenCraftingInterface"), + OPEN_SONGBOOK_INTERFACE("OpenSongbookInterface"), + OPEN_NPC_CRAFTING_INTERFACE("OpenNpcCraftingInterface"), + OPEN_MERCHANT_INTERFACE("OpenMerchantInterface"), + OPEN_AI_INTERFACE("OpenAiInterface"), + OPEN_TELEPORT_DIALOG("OpenTeleportDialog"), + SHOW_POPUP("ShowPopup"), + SCRIPT_PANE("ScriptPane"), + MESSAGE("Message"); + } + + constructor(type: String, entityID: Int = 0, data: JsonElement = JsonNull.INSTANCE) : this( + Type.entries.firstOrNull { it.jsonName == type } ?: throw NoSuchElementException("No such interaction action $type!"), entityID, data + ) + + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + if (isLegacy) Type.entries[stream.readInt()] else Type.entries[stream.readUnsignedByte()], + stream.readInt(), + stream.readJsonElement() + ) + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + if (isLegacy) stream.writeInt(type.ordinal) else stream.writeByte(type.ordinal) + stream.writeInt(entityID) + stream.writeJsonElement(data) + } + + companion object { + val NONE = InteractAction() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/InteractRequest.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/InteractRequest.kt new file mode 100644 index 00000000..6a537b8b --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/InteractRequest.kt @@ -0,0 +1,23 @@ +package ru.dbotthepony.kstarbound.defs + +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.io.readVector2d +import ru.dbotthepony.kstarbound.io.writeStruct2d +import java.io.DataInputStream +import java.io.DataOutputStream + +data class InteractRequest(val source: Int, val sourcePos: Vector2d, val target: Int, val targetPos: Vector2d) { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + stream.readInt(), + stream.readVector2d(isLegacy), + stream.readInt(), + stream.readVector2d(isLegacy), + ) + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeInt(source) + stream.writeStruct2d(sourcePos, isLegacy) + stream.writeInt(target) + stream.writeStruct2d(targetPos, isLegacy) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt index b6444f5b..8d9db0ac 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt @@ -1,5 +1,10 @@ package ru.dbotthepony.kstarbound.defs +import com.google.gson.Gson +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter import ru.dbotthepony.kommons.io.StreamCodec import ru.dbotthepony.kommons.io.readUUID import ru.dbotthepony.kommons.io.readVector2d @@ -18,6 +23,7 @@ import ru.dbotthepony.kstarbound.server.world.ServerWorld import java.io.DataInputStream import java.io.DataOutputStream import java.util.UUID +import kotlin.math.roundToInt // original game has MVariant here // MVariant prepends InvalidValue to Variant<> template @@ -26,7 +32,7 @@ import java.util.UUID // typedef MVariant WarpAction; // -> Variant WarpAction // hence WarpToWorld has index 1, WarpToPlayer 2, WarpAlias 3 - +@JsonAdapter(SpawnTarget.Adapter::class) sealed class SpawnTarget { abstract fun write(stream: DataOutputStream, isLegacy: Boolean) abstract fun resolve(world: ServerWorld): Vector2d? @@ -41,7 +47,7 @@ sealed class SpawnTarget { } override fun toString(): String { - return "SpawnTarget.SpawnTarget" + return "Whatever" } } @@ -56,7 +62,7 @@ sealed class SpawnTarget { } override fun toString(): String { - return "SpawnTarget.Entity[$id]" + return id } } @@ -72,7 +78,7 @@ sealed class SpawnTarget { } override fun toString(): String { - return "SpawnTarget.Position[$position]" + return "${position.x.roundToInt()}.${position.y.roundToInt()}" } override fun resolve(world: ServerWorld): Vector2d { @@ -92,7 +98,7 @@ sealed class SpawnTarget { } override fun toString(): String { - return "SpawnTarget.X[$position]" + return position.roundToInt().toString() } override fun resolve(world: ServerWorld): Vector2d { @@ -100,7 +106,20 @@ sealed class SpawnTarget { } } + class Adapter : TypeAdapter() { + override fun write(out: JsonWriter, value: SpawnTarget) { + out.value(value.toString()) + } + + override fun read(`in`: JsonReader): SpawnTarget { + return parse(`in`.nextString()) + } + } + companion object { + private val position = Regex("\\d+.\\d+") + private val positionX = Regex("\\d+") + fun read(stream: DataInputStream, isLegacy: Boolean): SpawnTarget { return when (val type = stream.readUnsignedByte()) { 0 -> Whatever @@ -110,14 +129,31 @@ sealed class SpawnTarget { else -> throw IllegalArgumentException("Unknown SpawnTarget type $type!") } } + + fun parse(value: String): SpawnTarget { + val matchPos = position.matchEntire(value) + + if (matchPos != null) { + return Position(Vector2d(matchPos.groups[0]!!.value.toDouble(), matchPos.groups[0]!!.value.toDouble())) + } + + val matchX = positionX.matchEntire(value) + + if (matchX != null) { + return X(matchX.groups[0]!!.value.toDouble()) + } + + return Entity(value) + } } } +@JsonAdapter(WarpAction.Adapter::class) sealed class WarpAction { abstract fun write(stream: DataOutputStream, isLegacy: Boolean) abstract fun resolve(connection: ServerConnection): WorldID - data class World(val worldID: WorldID, val target: SpawnTarget) : WarpAction() { + data class World(val worldID: WorldID, val target: SpawnTarget = SpawnTarget.Whatever) : WarpAction() { override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeByte(1) worldID.write(stream, isLegacy) @@ -125,7 +161,14 @@ sealed class WarpAction { } override fun resolve(connection: ServerConnection): WorldID { - TODO("Not yet implemented") + return worldID + } + + override fun toString(): String { + if (target != SpawnTarget.Whatever) + return "$worldID=$target" + + return "$worldID" } } @@ -141,6 +184,20 @@ sealed class WarpAction { return connection.server.clientByUUID(uuid)?.world?.worldID ?: WorldID.Limbo } + + override fun toString(): String { + return "Player:$uuid" + } + } + + class Adapter(gson: Gson) : TypeAdapter() { + override fun write(out: JsonWriter, value: WarpAction) { + out.value(value.toString()) + } + + override fun read(`in`: JsonReader): WarpAction { + return parse(`in`.nextString()) + } } companion object { @@ -161,45 +218,71 @@ sealed class WarpAction { } } + fun parse(value: String): WarpAction { + if (value.lowercase() == "return") { + return WarpAlias.Return + } else if (value.lowercase() == "orbitedworld") { + return WarpAlias.OrbitedWorld + } else if (value.lowercase().startsWith("player:")) { + return Player(UUID.fromString(value.substring(7))) + } else { + val parts = value.split('=') + val world = WorldID.parse(parts[0]) + var spawnTarget: SpawnTarget = SpawnTarget.Whatever + + if (parts.size == 2) { + spawnTarget = SpawnTarget.parse(parts[1]) + } + + return World(world, spawnTarget) + } + } + val CODEC = nativeCodec(::read, WarpAction::write) val LEGACY_CODEC = legacyCodec(::read, WarpAction::write) } } sealed class WarpAlias(val index: Int) : WarpAction() { - override fun write(stream: DataOutputStream, isLegacy: Boolean) { + final override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.write(3) // because it is defined as enum class WarpAlias, without specifying uint8_t as type stream.writeInt(index) } + abstract fun remap(connection: ServerConnection): WarpAction + + final override fun resolve(connection: ServerConnection): WorldID { + throw RuntimeException("Trying to use WarpAlias as regular warp action") + } + object Return : WarpAlias(0) { - override fun resolve(connection: ServerConnection): WorldID { - TODO("Not yet implemented") + override fun remap(connection: ServerConnection): WarpAction { + return connection.returnWarp ?: World(connection.shipWorld.worldID) } override fun toString(): String { - return "WarpAlias.Return" + return "Return" } } object OrbitedWorld : WarpAlias(1) { - override fun resolve(connection: ServerConnection): WorldID { - TODO("Not yet implemented") + override fun remap(connection: ServerConnection): WarpAction { + return connection.orbitalWarpAction.orNull()?.first ?: World(connection.shipWorld.worldID) } override fun toString(): String { - return "WarpAlias.OrbitedWorld" + return "OrbitedWorld" } } object OwnShip : WarpAlias(2) { - override fun resolve(connection: ServerConnection): WorldID { - return connection.shipWorld.worldID + override fun remap(connection: ServerConnection): WarpAction { + return World(connection.shipWorld.worldID) } override fun toString(): String { - return "WarpAlias.OwnShip" + return "OwnShip" } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt index a43a78f2..d8f43c38 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt @@ -1,10 +1,51 @@ package ru.dbotthepony.kstarbound.defs +import com.google.common.collect.ImmutableList +import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters +import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldParameters +import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParameters import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import java.util.function.Predicate @JsonFactory data class UniverseServerConfig( // in milliseconds val clockUpdatePacketInterval: Long = 500L, -) + val findStarterWorldParameters: StarterWorld, + val queuedFlightWaitTime: Double = 0.0, +) { + @JsonFactory + data class WorldPredicate( + val terrestrialBiome: String? = null, + val terrestrialSize: String? = null, + val floatingDungeon: String? = null, + ) : Predicate { + override fun test(t: VisitableWorldParameters): Boolean { + if (terrestrialBiome != null) { + if (t !is TerrestrialWorldParameters) return false + if (t.primaryBiome != terrestrialBiome) return false + } + + if (terrestrialSize != null) { + if (t !is TerrestrialWorldParameters) return false + if (t.sizeName != terrestrialSize) return false + } + + if (floatingDungeon != null) { + if (t !is FloatingDungeonWorldParameters) return false + if (t.primaryDungeon != floatingDungeon) return false + } + + return true + } + } + + @JsonFactory + data class StarterWorld( + val tries: Int, + val range: Int, + val starterWorld: WorldPredicate, + val requiredSystemWorlds: ImmutableList = ImmutableList.of(), + ) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldID.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldID.kt index 4908b585..53f9c85a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldID.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldID.kt @@ -1,16 +1,24 @@ package ru.dbotthepony.kstarbound.defs +import com.google.gson.Gson +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter import ru.dbotthepony.kommons.io.readUUID import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeUUID import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.network.syncher.legacyCodec import ru.dbotthepony.kstarbound.network.syncher.nativeCodec +import ru.dbotthepony.kstarbound.util.toStarboundString +import ru.dbotthepony.kstarbound.util.uuidFromStarboundString import ru.dbotthepony.kstarbound.world.UniversePos import java.io.DataInputStream import java.io.DataOutputStream import java.util.UUID +@JsonAdapter(WorldID.Adapter::class) sealed class WorldID { abstract fun write(stream: DataOutputStream, isLegacy: Boolean) val isLimbo: Boolean get() = this is Limbo @@ -21,7 +29,7 @@ sealed class WorldID { } override fun toString(): String { - return "WorldID.Limbo" + return "Nowhere" } } @@ -32,7 +40,7 @@ sealed class WorldID { } override fun toString(): String { - return "WorldID.Celestial[$pos]" + return "CelestialWorld:$pos" } } @@ -43,7 +51,7 @@ sealed class WorldID { } override fun toString(): String { - return "WorldID.ShipWorld[${uuid.toString().substring(0, 8)}]" + return "ClientShipWorld:${uuid.toStarboundString()}" } } @@ -66,7 +74,17 @@ sealed class WorldID { } override fun toString(): String { - return "WorldID.Instance[$name, uuid=$uuid, threat level=$threatLevel]" + return "InstanceWorld:$name:${uuid?.toStarboundString() ?: "-"}:${threatLevel ?: "-"}" + } + } + + class Adapter(gson: Gson) : TypeAdapter() { + override fun write(out: JsonWriter, value: WorldID) { + out.value(value.toString()) + } + + override fun read(`in`: JsonReader): WorldID { + return parse(`in`.nextString()) } } @@ -74,6 +92,45 @@ sealed class WorldID { val CODEC = nativeCodec(::read, WorldID::write) val LEGACY_CODEC = legacyCodec(::read, WorldID::write) + fun parse(value: String): WorldID { + if (value.isBlank()) + return Limbo + + val parts = value.split(':') + + return when (val type = parts[0].lowercase()) { + "nowhere" -> Limbo + "instanceworld" -> { + val rest = parts[1].split(':') + + if (rest.isEmpty() || rest.size > 3) { + throw IllegalArgumentException("Malformed InstanceWorld string: $value") + } + + val name = rest[0] + var uuid: UUID? = null + var threatLevel: Double? = null + + if (rest.size > 1) { + uuid = if (rest[1] == "-") null else uuidFromStarboundString(rest[1]) + + if (rest.size > 2) { + threatLevel = if (rest[2] == "-") null else rest[2].toDouble() + + if (threatLevel != null && threatLevel < 0.0) + throw IllegalArgumentException("InstanceWorld threat level is negative: $value") + } + } + + Instance(name, uuid, threatLevel) + } + + "celestialworld" -> Celestial(UniversePos.parse(parts[1])) + "clientshipworld" -> ShipWorld(uuidFromStarboundString(parts[1])) + else -> throw IllegalArgumentException("Invalid WorldID type: $type (input: $value)") + } + } + fun read(stream: DataInputStream, isLegacy: Boolean): WorldID { return when (val type = stream.readUnsignedByte()) { 0 -> Limbo diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/ShipUpgrades.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/ShipUpgrades.kt index 7700d2ee..b0c86f5d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/ShipUpgrades.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/ShipUpgrades.kt @@ -1,12 +1,14 @@ package ru.dbotthepony.kstarbound.defs.actor.player import com.google.common.collect.ImmutableSet +import it.unimi.dsi.fastutil.objects.ObjectArraySet import ru.dbotthepony.kommons.guava.immutableSet -import ru.dbotthepony.kommons.io.readBinaryString import ru.dbotthepony.kommons.io.readCollection import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeCollection +import ru.dbotthepony.kstarbound.io.readDouble import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.io.writeDouble import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.network.syncher.legacyCodec import ru.dbotthepony.kstarbound.network.syncher.nativeCodec @@ -19,15 +21,15 @@ data class ShipUpgrades( val maxFuel: Int = 0, val crewSize: Int = 0, val fuelEfficiency: Double = 1.0, - val shipSpeed: Int = 0, + val shipSpeed: Double = 30.0, val capabilities: ImmutableSet = ImmutableSet.of() ) { constructor(stream: DataInputStream, isLegacy: Boolean) : this( stream.readInt(), stream.readInt(), stream.readInt(), - if (isLegacy) stream.readFloat().toDouble() else stream.readDouble(), - stream.readInt(), + stream.readDouble(isLegacy), + stream.readDouble(isLegacy), ImmutableSet.copyOf(stream.readCollection { readInternedString() }) ) @@ -42,17 +44,24 @@ data class ShipUpgrades( ) } + fun addCapability(capability: String): ShipUpgrades { + val copy = ObjectArraySet(capabilities) + copy.add(capability) + return copy(capabilities = ImmutableSet.copyOf(copy)) + } + + fun removeCapability(capability: String): ShipUpgrades { + val copy = ObjectArraySet(capabilities) + copy.remove(capability) + return copy(capabilities = ImmutableSet.copyOf(copy)) + } + fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeInt(shipLevel) stream.writeInt(maxFuel) stream.writeInt(crewSize) - - if (isLegacy) - stream.writeFloat(fuelEfficiency.toFloat()) - else - stream.writeDouble(fuelEfficiency) - - stream.writeInt(shipSpeed) + stream.writeDouble(fuelEfficiency, isLegacy) + stream.writeDouble(shipSpeed, isLegacy) stream.writeCollection(capabilities) { writeBinaryString(it) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AsteroidsWorldParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AsteroidsWorldParameters.kt index b4c88974..523bf2dc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AsteroidsWorldParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AsteroidsWorldParameters.kt @@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.defs.world import com.google.gson.JsonObject import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.vector.Vector2d @@ -9,9 +10,14 @@ import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.GlobalDefaults import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.fromJson +import ru.dbotthepony.kstarbound.io.readColor +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.io.writeColor import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.util.random.nextRange import ru.dbotthepony.kstarbound.util.random.random +import java.io.DataInputStream +import java.io.DataOutputStream import kotlin.properties.Delegates class AsteroidsWorldParameters : VisitableWorldParameters() { @@ -68,6 +74,26 @@ class AsteroidsWorldParameters : VisitableWorldParameters() { data[k] = v } + override fun read0(stream: DataInputStream) { + super.read0(stream) + + asteroidTopLevel = stream.readInt() + asteroidBottomLevel = stream.readInt() + blendSize = stream.readFloat().toDouble() + asteroidBiome = stream.readInternedString() + ambientLightLevel = stream.readColor() + } + + override fun write0(stream: DataOutputStream) { + super.write0(stream) + + stream.writeInt(asteroidTopLevel) + stream.writeInt(asteroidBottomLevel) + stream.writeFloat(blendSize.toFloat()) + stream.writeBinaryString(asteroidBiome) + stream.writeColor(ambientLightLevel) + } + override fun createLayout(seed: Long): WorldLayout { val random = random(seed) val terrain = GlobalDefaults.asteroidWorlds.terrains.random(random) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialParameters.kt index 458c1cdf..bb0d9f46 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialParameters.kt @@ -10,13 +10,23 @@ import com.google.gson.TypeAdapterFactory import com.google.gson.reflect.TypeToken import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter +import it.unimi.dsi.fastutil.io.FastByteArrayInputStream +import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.gson.value +import ru.dbotthepony.kommons.io.readByteArray +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeByteArray import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.readJsonElement +import ru.dbotthepony.kstarbound.json.writeJsonElement import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.world.UniversePos +import java.io.DataInputStream +import java.io.DataOutputStream class CelestialParameters private constructor(val coordinate: UniversePos, val seed: Long, val name: String, val parameters: JsonObject, marker: Nothing?) { constructor(coordinate: UniversePos, seed: Long, name: String, parameters: JsonObject) : this(coordinate, seed, name, parameters, null) { @@ -55,6 +65,40 @@ class CelestialParameters private constructor(val coordinate: UniversePos, val s this.visitableParameters = visitableParameters } + private constructor(stream: DataInputStream, isLegacy: Boolean) : this( + UniversePos(stream, isLegacy), + stream.readLong(), + stream.readInternedString(), + stream.readJsonElement() as JsonObject, + VisitableWorldParameters.fromNetwork(stream, isLegacy) + ) + + private fun write0(stream: DataOutputStream, isLegacy: Boolean) { + coordinate.write(stream, isLegacy) + stream.writeLong(seed) + stream.writeBinaryString(name) + stream.writeJsonElement(parameters) + + val visitableParameters = visitableParameters + + if (visitableParameters == null) { + stream.writeBoolean(false) + } else { + visitableParameters.write(stream, isLegacy) + } + } + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + if (isLegacy) { + // holy fucking shit + val wrap = FastByteArrayOutputStream() + write0(DataOutputStream(wrap), true) + stream.writeByteArray(wrap.array, 0, wrap.length) + } else { + write0(stream, false) + } + } + var visitableParameters: VisitableWorldParameters? = null private set @@ -80,5 +124,16 @@ class CelestialParameters private constructor(val coordinate: UniversePos, val s return CelestialParameters(read.coordinate, read.seed, read.name, read.parameters, read.visitableParameters) } } + + companion object { + fun read(stream: DataInputStream, isLegacy: Boolean): CelestialParameters { + if (isLegacy) { + val wrap = FastByteArrayInputStream(stream.readByteArray()) + return CelestialParameters(DataInputStream(wrap), true) + } else { + return CelestialParameters(stream, false) + } + } + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialPlanet.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialPlanet.kt deleted file mode 100644 index 75f654c3..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialPlanet.kt +++ /dev/null @@ -1,7 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.world - -import it.unimi.dsi.fastutil.ints.Int2ObjectMap -import ru.dbotthepony.kstarbound.json.builder.JsonFactory - -@JsonFactory -data class CelestialPlanet(val parameters: CelestialParameters, val satellites: Int2ObjectMap) \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/FloatingDungeonWorldParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/FloatingDungeonWorldParameters.kt index d72778ad..8a9e4a76 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/FloatingDungeonWorldParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/FloatingDungeonWorldParameters.kt @@ -1,10 +1,23 @@ package ru.dbotthepony.kstarbound.defs.world +import com.google.gson.JsonObject +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kstarbound.GlobalDefaults import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.fromJson +import ru.dbotthepony.kstarbound.io.readColor +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.io.readNullableString +import ru.dbotthepony.kstarbound.io.writeColor +import ru.dbotthepony.kstarbound.io.writeNullableString +import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.util.random.random +import java.io.DataInputStream +import java.io.DataOutputStream import kotlin.properties.Delegates class FloatingDungeonWorldParameters : VisitableWorldParameters() { @@ -58,6 +71,77 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() { return layout } + @JsonFactory + data class JsonData( + val dungeonSurfaceHeight: Int, + val dungeonUndergroundLevel: Int, + val primaryDungeon: String, + val biome: String? = null, + val ambientLightLevel: RGBAColor, + val dayMusicTrack: String? = null, + val nightMusicTrack: String? = null, + val dayAmbientNoises: String? = null, + val nightAmbientNoises: String? = null, + ) + + override fun fromJson(data: JsonObject) { + super.fromJson(data) + + val read = Starbound.gson.fromJson(data, JsonData::class.java) + + dungeonSurfaceHeight = read.dungeonSurfaceHeight + dungeonUndergroundLevel = read.dungeonUndergroundLevel + primaryDungeon = read.primaryDungeon + biome = read.biome + ambientLightLevel = read.ambientLightLevel + dayMusicTrack = read.dayMusicTrack + nightMusicTrack = read.nightMusicTrack + dayAmbientNoises = read.dayAmbientNoises + nightAmbientNoises = read.nightAmbientNoises + } + + override fun toJson(data: JsonObject, isLegacy: Boolean) { + super.toJson(data, isLegacy) + + val serialize = Starbound.gson.toJsonTree(JsonData( + dungeonSurfaceHeight, dungeonUndergroundLevel, primaryDungeon, biome, ambientLightLevel, dayMusicTrack, nightMusicTrack, dayAmbientNoises, nightAmbientNoises + )) as JsonObject + + for ((k, v) in serialize.entrySet()) { + data[k] = v + } + } + + override fun read0(stream: DataInputStream) { + super.read0(stream) + + dungeonBaseHeight = stream.readInt() + dungeonSurfaceHeight = stream.readInt() + dungeonUndergroundLevel = stream.readInt() + primaryDungeon = stream.readInternedString() + biome = stream.readNullableString() + ambientLightLevel = stream.readColor() + dayMusicTrack = stream.readNullableString() + nightMusicTrack = stream.readNullableString() + dayAmbientNoises = stream.readNullableString() + nightAmbientNoises = stream.readNullableString() + } + + override fun write0(stream: DataOutputStream) { + super.write0(stream) + + stream.writeInt(dungeonBaseHeight) + stream.writeInt(dungeonSurfaceHeight) + stream.writeInt(dungeonUndergroundLevel) + stream.writeBinaryString(primaryDungeon) + stream.writeNullableString(biome) + stream.writeColor(ambientLightLevel) + stream.writeNullableString(dayMusicTrack) + stream.writeNullableString(nightMusicTrack) + stream.writeNullableString(dayAmbientNoises) + stream.writeNullableString(nightAmbientNoises) + } + companion object { fun generate(typeName: String): FloatingDungeonWorldParameters { val config = GlobalDefaults.dungeonWorlds[typeName] ?: throw NoSuchElementException("Unknown dungeon world type $typeName!") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt index 91399fb3..a4f21671 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt @@ -123,15 +123,26 @@ data class SkyWorldHorizon(val center: Vector2d, val scale: Double, val rotation @JsonFactory data class SkyParameters( - var skyType: SkyType = SkyType.BARREN, - var seed: Long = 0L, - var dayLength: Double? = null, - var horizonClouds: Boolean = false, - var skyColoring: Either = Either.left(SkyColoring()), - var spaceLevel: Double? = null, - var surfaceLevel: Double? = null, - var nearbyPlanet: Pair>, Vector2d>? = null, + val skyType: SkyType = SkyType.BARREN, + val seed: Long = 0L, + val dayLength: Double? = null, + val horizonClouds: Boolean = false, + val skyColoring: Either = Either.left(SkyColoring()), + val spaceLevel: Double? = null, + val surfaceLevel: Double? = null, + val nearbyPlanet: Planet? = null, + val nearbyMoons: ImmutableList = ImmutableList.of(), + val horizonImages: ImmutableList = ImmutableList.of(), ) { + @JsonFactory + data class HorizonImage(val left: String, val right: String) + + @JsonFactory + data class Layer(val image: String, val scale: Double) + + @JsonFactory + data class Planet(val pos: Vector2d, val layers: ImmutableList) + companion object { suspend fun create(coordinate: UniversePos, universe: Universe): SkyParameters { if (coordinate.isSystem) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SystemWorldConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SystemWorldConfig.kt new file mode 100644 index 00000000..91b15fa6 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SystemWorldConfig.kt @@ -0,0 +1,41 @@ +package ru.dbotthepony.kstarbound.defs.world + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.collect.WeightedList +import ru.dbotthepony.kstarbound.json.builder.JsonFactory + +@JsonFactory +data class SystemWorldConfig( + val starGravitationalConstant: Double, + val planetGravitationalConstant: Double, + val emptyOrbitSize: Double, + val unvisitablePlanetSize: Double, + val floatingDungeonWorldSizes: ImmutableMap, + val planetSizes: ImmutableList>, + val starSize: Double, + val planetaryOrbitPadding: Vector2d, + val satelliteOrbitPadding: Vector2d, + val arrivalRange: Vector2d, + val objectSpawnPadding: Double, + val clientObjectSpawnPadding: Double, + val objectSpawnInterval: Vector2d, + val objectSpawnCycle: Double, + val minObjectOrbitTime: Double, + val asteroidBeamDistance: Double, + val emptySkyParameters: SkyParameters, + + val objectSpawnPool: WeightedList, + val initialObjectPools: WeightedList>>, + + val clientShip: ClientShip, +) { + @JsonFactory + data class ClientShip( + val speed: Double, + val orbitDistance: Double, + val departTime: Double, + val spaceDepartTime: Double, + ) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SystemWorldObjectConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SystemWorldObjectConfig.kt new file mode 100644 index 00000000..b55494a5 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SystemWorldObjectConfig.kt @@ -0,0 +1,62 @@ +package ru.dbotthepony.kstarbound.defs.world + +import com.google.common.collect.ImmutableMap +import com.google.gson.JsonObject +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.defs.WarpAction +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.util.random.nextRange +import ru.dbotthepony.kstarbound.util.random.random +import ru.dbotthepony.kstarbound.util.random.staticRandom64 +import java.util.UUID + +@JsonFactory +data class SystemWorldObjectConfig( + val warpAction: WarpAction, + val orbitRange: Vector2d, + val lifeTime: Vector2d, + val permanent: Boolean = false, + val moving: Boolean = false, + val threatLevel: Double? = null, + val skyParameters: SkyParameters, + val parameters: JsonObject = JsonObject(), + val speed: Double = 0.0, + val generatedParameters: ImmutableMap = ImmutableMap.of(), +) { + init { + require(speed >= 0.0) { "Negative speed $speed" } + } + + fun create(uuid: UUID, name: String): Data { + val random = random(staticRandom64(uuid.mostSignificantBits, uuid.leastSignificantBits)) + + return Data( + warpAction = warpAction, + orbitDistance = random.nextRange(orbitRange), + lifeTime = random.nextRange(lifeTime), + permanent = permanent, + moving = moving, + threatLevel = threatLevel, + skyParameters = skyParameters, + parameters = parameters, + speed = speed, + generatedParameters = generatedParameters, + name = name, + ) + } + + @JsonFactory + data class Data( + val warpAction: WarpAction, + val orbitDistance: Double, + val lifeTime: Double, + val permanent: Boolean = false, + val moving: Boolean = false, + val threatLevel: Double? = null, + val skyParameters: SkyParameters, + val parameters: JsonObject = JsonObject(), + val speed: Double = 0.0, + val generatedParameters: ImmutableMap = ImmutableMap.of(), + val name: String, + ) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt index f703f654..dd6a353c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt @@ -13,6 +13,9 @@ import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.getArray import ru.dbotthepony.kommons.gson.getObject import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.io.readCollection +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeCollection import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2i @@ -22,14 +25,22 @@ import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.defs.JsonDriven import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters +import ru.dbotthepony.kstarbound.fromJson +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.io.readVector2d +import ru.dbotthepony.kstarbound.io.writeStruct2d import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.json.pairAdapter +import ru.dbotthepony.kstarbound.json.readJsonElement import ru.dbotthepony.kstarbound.json.stream +import ru.dbotthepony.kstarbound.json.writeJsonElement import ru.dbotthepony.kstarbound.util.binnedChoice import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise import ru.dbotthepony.kstarbound.util.random.nextRange import ru.dbotthepony.kstarbound.util.random.random +import java.io.DataInputStream +import java.io.DataOutputStream import java.util.random.RandomGenerator import kotlin.properties.Delegates @@ -71,7 +82,46 @@ class TerrestrialWorldParameters : VisitableWorldParameters() { val encloseLiquids: Boolean, val fillMicrodungeons: Boolean, - ) + ) { + constructor(stream: DataInputStream) : this( + stream.readInternedString(), + + stream.readInternedString(), + stream.readInternedString(), + stream.readInternedString(), + stream.readInternedString(), + stream.readInternedString(), + stream.readInternedString(), + + Either.left(stream.readUnsignedByte()), + stream.readFloat().toDouble(), + + Either.left(stream.readUnsignedByte()), + stream.readInt(), + + stream.readBoolean(), + stream.readBoolean(), + ) + + fun write(stream: DataOutputStream) { + stream.writeBinaryString(biome) + + stream.writeBinaryString(blockSelector) + stream.writeBinaryString(fgCaveSelector) + stream.writeBinaryString(bgCaveSelector) + stream.writeBinaryString(fgOreSelector) + stream.writeBinaryString(bgOreSelector) + stream.writeBinaryString(subBlockSelector) + + stream.writeByte(caveLiquid?.map({ it }, { Registries.liquid[it]?.id }) ?: 0) + stream.writeFloat(caveLiquidSeedDensity.toFloat()) + stream.writeByte(oceanLiquid?.map({ it }, { Registries.liquid[it]?.id }) ?: 0) + stream.writeInt(oceanLiquidLevel) + + stream.writeBoolean(encloseLiquids) + stream.writeBoolean(fillMicrodungeons) + } + } @JsonFactory data class Layer( @@ -89,7 +139,40 @@ class TerrestrialWorldParameters : VisitableWorldParameters() { val secondaryRegionSizeRange: Vector2d, val subRegionSizeRange: Vector2d, - ) + ) { + constructor(stream: DataInputStream) : this( + stream.readInt(), + stream.readInt(), + ImmutableSet.copyOf(stream.readCollection { readInternedString() }), + stream.readInt(), + + Region(stream), + Region(stream), + + ImmutableList.copyOf(stream.readCollection { Region(this) }), + ImmutableList.copyOf(stream.readCollection { Region(this) }), + + stream.readVector2d(true), + stream.readVector2d(true), + ) + + fun write(stream: DataOutputStream) { + stream.writeInt(layerMinHeight) + stream.writeInt(layerBaseHeight) + + stream.writeCollection(dungeons) { writeBinaryString(it) } + stream.writeInt(dungeonXVariance) + + primaryRegion.write(stream) + primarySubRegion.write(stream) + + stream.writeCollection(secondaryRegions) { it.write(this) } + stream.writeCollection(secondarySubRegions) { it.write(this) } + + stream.writeStruct2d(secondaryRegionSizeRange, true) + stream.writeStruct2d(subRegionSizeRange, true) + } + } override fun fromJson(data: JsonObject) { super.fromJson(data) @@ -206,6 +289,52 @@ class TerrestrialWorldParameters : VisitableWorldParameters() { override val type: VisitableWorldParametersType get() = VisitableWorldParametersType.TERRESTRIAL + override fun read0(stream: DataInputStream) { + super.read0(stream) + + primaryBiome = stream.readInternedString() + surfaceLiquid = Either.left(stream.readUnsignedByte()) + sizeName = stream.readInternedString() + hueShift = stream.readFloat().toDouble() + skyColoring = SkyColoring.read(stream, true) + dayLength = stream.readFloat().toDouble() + blendSize = stream.readFloat().toDouble() + + blockNoiseConfig = Starbound.gson.fromJson(stream.readJsonElement()) + blendNoiseConfig = Starbound.gson.fromJson(stream.readJsonElement()) + + spaceLayer = Layer(stream) + atmosphereLayer = Layer(stream) + surfaceLayer = Layer(stream) + subsurfaceLayer = Layer(stream) + undergroundLayers = stream.readCollection { Layer(this) } + coreLayer = Layer(stream) + } + + override fun write0(stream: DataOutputStream) { + super.write0(stream) + + stream.writeBinaryString(primaryBiome) + stream.writeByte(surfaceLiquid?.map({ it }, { Registries.liquid[it]?.id }) ?: 0) + stream.writeBinaryString(sizeName) + stream.writeFloat(hueShift.toFloat()) + skyColoring.write(stream, true) + stream.writeFloat(dayLength.toFloat()) + stream.writeFloat(blendSize.toFloat()) + + Starbound.legacyJson { + stream.writeJsonElement(Starbound.gson.toJsonTree(blockNoiseConfig)) + stream.writeJsonElement(Starbound.gson.toJsonTree(blendNoiseConfig)) + } + + spaceLayer.write(stream) + atmosphereLayer.write(stream) + surfaceLayer.write(stream) + subsurfaceLayer.write(stream) + stream.writeCollection(undergroundLayers) { it.write(this) } + coreLayer.write(stream) + } + // why override fun createLayout(seed: Long): WorldLayout { val layout = WorldLayout() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/VisitableWorldParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/VisitableWorldParameters.kt index 09ce5fb0..f903230f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/VisitableWorldParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/VisitableWorldParameters.kt @@ -1,5 +1,7 @@ package ru.dbotthepony.kstarbound.defs.world +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableSet import com.google.gson.Gson import com.google.gson.JsonObject import com.google.gson.TypeAdapter @@ -7,19 +9,41 @@ import com.google.gson.TypeAdapterFactory import com.google.gson.reflect.TypeToken import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter +import it.unimi.dsi.fastutil.io.FastByteArrayInputStream +import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.gson.value +import ru.dbotthepony.kommons.io.readByteArray +import ru.dbotthepony.kommons.io.readCollection +import ru.dbotthepony.kommons.io.readVarInt +import ru.dbotthepony.kommons.io.readVector2i +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeByteArray +import ru.dbotthepony.kommons.io.writeCollection +import ru.dbotthepony.kommons.io.writeStruct2i import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.collect.WeightedList +import ru.dbotthepony.kstarbound.fromJson +import ru.dbotthepony.kstarbound.io.readDouble +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.io.readNullable +import ru.dbotthepony.kstarbound.io.writeDouble +import ru.dbotthepony.kstarbound.io.writeNullable import ru.dbotthepony.kstarbound.json.builder.DispatchingAdapter import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.readJsonElement +import ru.dbotthepony.kstarbound.json.readJsonObject +import ru.dbotthepony.kstarbound.json.writeJsonObject +import java.io.DataInputStream +import java.io.DataOutputStream import kotlin.properties.Delegates +// uint8_t enum class BeamUpRule(override val jsonName: String) : IStringSerializable { NOWHERE("Nowhere"), SURFACE("Surface"), @@ -27,6 +51,7 @@ enum class BeamUpRule(override val jsonName: String) : IStringSerializable { ANYWHERE_WITH_WARNING("AnywhereWithWarning"); } +// uint8_t enum class WorldEdgeForceRegion(override val jsonName: String) : IStringSerializable { NONE("None"), TOP("Top"), @@ -34,6 +59,7 @@ enum class WorldEdgeForceRegion(override val jsonName: String) : IStringSerializ TOP_AND_BOTTOM("TopAndBottom"); } +// uint8_t enum class VisitableWorldParametersType(override val jsonName: String, val token: TypeToken) : IStringSerializable { TERRESTRIAL("TerrestrialWorldParameters", TypeToken.get(TerrestrialWorldParameters::class.java)), ASTEROIDS("AsteroidsWorldParameters", TypeToken.get(AsteroidsWorldParameters::class.java)), @@ -169,4 +195,90 @@ abstract class VisitableWorldParameters { toJson(data, isLegacy) return data } + + // called only for legacy protocol + protected open fun read0(stream: DataInputStream) { + typeName = stream.readInternedString() + threatLevel = stream.readFloat().toDouble() + worldSize = stream.readVector2i() + gravity = Vector2d(y = stream.readFloat().toDouble()) + airless = stream.readBoolean() + + val collection = stream.readCollection { readDouble() to readInternedString() } + + if (collection.isNotEmpty()) + weatherPool = WeightedList(ImmutableList.copyOf(collection)) + + environmentStatusEffects = ImmutableSet.copyOf(stream.readCollection { readInternedString() }) + overrideTech = stream.readNullable { ImmutableSet.copyOf(readCollection { readInternedString() }) } + globalDirectives = stream.readNullable { ImmutableSet.copyOf(readCollection { readInternedString() }) } + + beamUpRule = BeamUpRule.entries[stream.readUnsignedByte()] + disableDeathDrops = stream.readBoolean() + terraformed = stream.readBoolean() + worldEdgeForceRegions = WorldEdgeForceRegion.entries[stream.readUnsignedByte()] + } + + // called only for legacy protocol + protected open fun write0(stream: DataOutputStream) { + stream.writeBinaryString(typeName) + stream.writeFloat(threatLevel.toFloat()) + stream.writeStruct2i(worldSize) + stream.writeFloat(gravity.y.toFloat()) + stream.writeBoolean(airless) + + if (weatherPool == null) + stream.writeByte(0) + else + stream.writeCollection(weatherPool!!.parent) { writeDouble(it.first); writeBinaryString(it.second) } + + stream.writeCollection(environmentStatusEffects) { writeBinaryString(it) } + stream.writeNullable(overrideTech) { writeCollection(it) { writeBinaryString(it) } } + stream.writeNullable(globalDirectives) { writeCollection(it) { writeBinaryString(it) } } + stream.writeByte(beamUpRule.ordinal) + stream.writeBoolean(disableDeathDrops) + stream.writeBoolean(terraformed) + stream.writeByte(worldEdgeForceRegions.ordinal) + } + + // because writing as json is too easy. + // Tard. + fun write(stream: DataOutputStream, isLegacy: Boolean) { + if (isLegacy) { + val wrapper = FastByteArrayOutputStream() + wrapper.write(type.ordinal) + write0(DataOutputStream(wrapper)) + stream.writeByteArray(wrapper.array, 0, wrapper.length) + } else { + stream.writeJsonObject(toJson()) + } + } + + companion object { + fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): VisitableWorldParameters? { + if (isLegacy) { + val readData = stream.readByteArray() + + if (readData.isEmpty()) { + return null + } + + val data = DataInputStream(FastByteArrayInputStream(readData)) + + val create = when (VisitableWorldParametersType.entries[data.readUnsignedByte()]) { + VisitableWorldParametersType.TERRESTRIAL -> TerrestrialWorldParameters() + VisitableWorldParametersType.ASTEROIDS -> AsteroidsWorldParameters() + VisitableWorldParametersType.FLOATING_DUNGEON -> FloatingDungeonWorldParameters() + } + + create.read0(data) + return create + } else { + if (!stream.readBoolean()) + return null + + return Starbound.gson.fromJson(stream.readJsonObject(), VisitableWorldParameters::class.java) + } + } + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt index b96d2627..10d47fad 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt @@ -74,6 +74,29 @@ fun InputStream.readAABBLegacy(): AABB { return AABB(mins.toDoubleVector(), maxs.toDoubleVector()) } +fun S.readEither(isLegacy: Boolean, left: S.() -> L, right: S.() -> R): Either { + var type = readUnsignedByte() + + if (isLegacy) + type-- + + return when (type) { + 0 -> Either.left(left(this)) + 1 -> Either.right(right(this)) + else -> throw IllegalArgumentException("Unexpected either type $type") + } +} + +fun S.writeEither(value: Either, isLegacy: Boolean, left: S.(L) -> Unit, right: S.(R) -> Unit) { + if (value.isLeft) { + if (isLegacy) write(1) else write(0) + left(value.left()) + } else { + if (isLegacy) write(2) else write(1) + right(value.right()) + } +} + fun InputStream.readAABBLegacyOptional(): KOptional { val mins = readVector2f() val maxs = readVector2f() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt index 78c6ad9d..607035ba 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt @@ -50,7 +50,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : var entityIDRange: IntRange by Delegates.notNull() private set - protected val coroutineScope = CoroutineScope(Starbound.COROUTINE_EXECUTOR) + val scope = CoroutineScope(Starbound.COROUTINE_EXECUTOR) var connectionID: Int = -1 set(value) { @@ -116,7 +116,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : protected open fun onChannelClosed() { isConnected = false LOGGER.info("$this is terminated") - coroutineScope.cancel("$this is terminated") + scope.cancel("$this is terminated") } fun bind(channel: Channel) { @@ -248,6 +248,14 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : private val warpActionCodec = StreamCodec.Pair(WarpAction.CODEC, WarpMode.CODEC).koptional() private val legacyWarpActionCodec = StreamCodec.Pair(WarpAction.LEGACY_CODEC, WarpMode.CODEC).koptional() + fun connectionForEntityID(id: Int): Int { + if (id >= 0) { + return 0 + } else { + return (-id - 1) / 65536 + 1 + } + } + val NIO_POOL by lazy { NioEventLoopGroup(1, ThreadFactoryBuilder().setDaemon(true).setNameFormat("Network IO %d").build()) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt index 29afdffb..6050fa86 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt @@ -31,9 +31,11 @@ import ru.dbotthepony.kstarbound.network.packets.serverbound.HandshakeResponsePa import ru.dbotthepony.kstarbound.network.packets.ProtocolRequestPacket import ru.dbotthepony.kstarbound.network.packets.ProtocolResponsePacket import ru.dbotthepony.kstarbound.network.packets.StepUpdatePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.CelestialResponsePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.CentralStructureUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ChatReceivePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectFailurePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.EntityInteractResultPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.EnvironmentUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.FindUniqueEntityResponsePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket @@ -42,14 +44,24 @@ import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPac import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerInfoPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemObjectCreatePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemObjectDestroyPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemShipCreatePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemShipDestroyPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemWorldStartPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemWorldUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.UpdateWorldPropertiesPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStopPacket +import ru.dbotthepony.kstarbound.network.packets.serverbound.CelestialRequestPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.ChatSendPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientDisconnectRequestPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.DamageTileGroupPacket +import ru.dbotthepony.kstarbound.network.packets.serverbound.EntityInteractPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.FindUniqueEntityPacket +import ru.dbotthepony.kstarbound.network.packets.serverbound.FlyShipPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.PlayerWarpPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldClientStateUpdatePacket import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldStartAcknowledgePacket @@ -389,7 +401,7 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.add(::HandshakeChallengePacket) // HandshakeChallenge LEGACY.add(::ChatReceivePacket) LEGACY.add(::UniverseTimeUpdatePacket) - LEGACY.skip("CelestialResponse") + LEGACY.add(::CelestialResponsePacket) LEGACY.add(::PlayerWarpResultPacket) LEGACY.skip("PlanetTypeUpdate") LEGACY.skip("Pause") @@ -400,9 +412,9 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.add(ClientDisconnectRequestPacket::read) LEGACY.add(::HandshakeResponsePacket) // HandshakeResponse LEGACY.add(::PlayerWarpPacket) - LEGACY.skip("FlyShip") + LEGACY.add(::FlyShipPacket) LEGACY.add(::ChatSendPacket) - LEGACY.skip("CelestialRequest") + LEGACY.add(::CelestialRequestPacket) // Packets sent bidirectionally between the universe client and the universe // server @@ -445,23 +457,23 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.add(::EntityCreatePacket) LEGACY.add(EntityUpdateSetPacket::read) LEGACY.add(::EntityDestroyPacket) - LEGACY.skip("EntityInteract") - LEGACY.skip("EntityInteractResult") + LEGACY.add(::EntityInteractPacket) + LEGACY.add(::EntityInteractResultPacket) LEGACY.skip("HitRequest") LEGACY.skip("DamageRequest") LEGACY.skip("DamageNotification") LEGACY.skip("EntityMessage") LEGACY.skip("EntityMessageResponse") - LEGACY.skip("UpdateWorldProperties") + LEGACY.add(::UpdateWorldPropertiesPacket) LEGACY.add(::StepUpdatePacket) // Packets sent system server -> system client - LEGACY.skip("SystemWorldStart") - LEGACY.skip("SystemWorldUpdate") - LEGACY.skip("SystemObjectCreate") - LEGACY.skip("SystemObjectDestroy") - LEGACY.skip("SystemShipCreate") - LEGACY.skip("SystemShipDestroy") + LEGACY.add(::SystemWorldStartPacket) + LEGACY.add(::SystemWorldUpdatePacket) + LEGACY.add(::SystemObjectCreatePacket) + LEGACY.add(::SystemObjectDestroyPacket) + LEGACY.add(::SystemShipCreatePacket) + LEGACY.add(::SystemShipDestroyPacket) // Packets sent system client -> system server LEGACY.skip("SystemObjectSpawn") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/CelestialResponsePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/CelestialResponsePacket.kt new file mode 100644 index 00000000..9e9fe860 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/CelestialResponsePacket.kt @@ -0,0 +1,91 @@ +package ru.dbotthepony.kstarbound.network.packets.clientbound + +import ru.dbotthepony.kommons.io.readCollection +import ru.dbotthepony.kommons.io.readMap +import ru.dbotthepony.kommons.io.readVector2i +import ru.dbotthepony.kommons.io.readVector3i +import ru.dbotthepony.kommons.io.writeCollection +import ru.dbotthepony.kommons.io.writeMap +import ru.dbotthepony.kommons.io.writeStruct2i +import ru.dbotthepony.kommons.io.writeStruct3i +import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kommons.vector.Vector3i +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.defs.world.CelestialParameters +import ru.dbotthepony.kstarbound.io.readEither +import ru.dbotthepony.kstarbound.io.writeEither +import ru.dbotthepony.kstarbound.network.IClientPacket +import java.io.DataInputStream +import java.io.DataOutputStream + +class CelestialResponsePacket(val responses: Collection>) : IClientPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + stream.readCollection { + readEither(isLegacy, { ChunkData(stream, isLegacy) }, { SystemData(stream, isLegacy) }) + } + ) + + data class ChunkData( + val chunkIndex: Vector2i, + // lol + // val constellations: List>>, + val constellations: List>, + val systemParameters: Map, + val systemObjects: Map>, + ) { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + stream.readVector2i(), + if (isLegacy) stream.readCollection { readCollection { readVector2i() to readVector2i() } }.flatten() else stream.readCollection { readVector2i() to readVector2i() }, + stream.readMap({ readVector3i() }, { CelestialParameters.read(this, isLegacy) }), + stream.readMap({ readVector3i() }, { readMap({ readInt() }, { PlanetData(this, isLegacy) }) }), + ) + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeStruct2i(chunkIndex) + if (isLegacy) stream.writeByte(1) // outer List<> + stream.writeCollection(constellations) { writeStruct2i(it.first); writeStruct2i(it.second) } + stream.writeMap(systemParameters, { writeStruct3i(it) }, { it.write(this, isLegacy) }) + stream.writeMap(systemObjects, { writeStruct3i(it) }, { writeMap(it, { writeInt(it) }, { it.write(this, isLegacy) }) }) + } + } + + data class SystemData(val systemLocation: Vector3i, val planets: Map) { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + stream.readVector3i(), + stream.readMap({ readInt() }, { PlanetData(this, isLegacy) }) + ) + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeStruct3i(systemLocation) + stream.writeMap(planets, { writeInt(it) }, { it.write(this, isLegacy) }) + } + } + + data class PlanetData( + val planetParameters: CelestialParameters, + val satelliteParameters: Map, + ) { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + CelestialParameters.read(stream, isLegacy), + stream.readMap({ readInt() }, { CelestialParameters.read(this, isLegacy) }) + ) + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + planetParameters.write(stream, isLegacy) + stream.writeMap(satelliteParameters, { writeInt(it) }, { it.write(this, isLegacy) }) + } + } + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + // this is top-notch design, having Either<> with no value, lol + // original sources continue to surprise me by unimaginable wonders + stream.writeCollection(responses) { + writeEither(it, isLegacy, { it.write(stream, isLegacy) }, { it.write(stream, isLegacy) }) + } + } + + override fun play(connection: ClientConnection) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/EntityInteractResultPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/EntityInteractResultPacket.kt new file mode 100644 index 00000000..a50b86c8 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/EntityInteractResultPacket.kt @@ -0,0 +1,34 @@ +package ru.dbotthepony.kstarbound.network.packets.clientbound + +import ru.dbotthepony.kommons.io.readUUID +import ru.dbotthepony.kommons.io.writeUUID +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.defs.InteractAction +import ru.dbotthepony.kstarbound.network.IClientPacket +import ru.dbotthepony.kstarbound.network.IServerPacket +import ru.dbotthepony.kstarbound.server.ServerConnection +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.UUID + +class EntityInteractResultPacket(val action: InteractAction, val id: UUID, val source: Int) : IServerPacket, IClientPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + InteractAction(stream, isLegacy), + stream.readUUID(), + stream.readInt() + ) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + action.write(stream, isLegacy) + stream.writeUUID(id) + stream.writeInt(source) + } + + override fun play(connection: ClientConnection) { + TODO("Not yet implemented") + } + + override fun play(connection: ServerConnection) { + TODO("Not yet implemented") + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemObjectCreatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemObjectCreatePacket.kt new file mode 100644 index 00000000..f3f32c9b --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemObjectCreatePacket.kt @@ -0,0 +1,21 @@ +package ru.dbotthepony.kstarbound.network.packets.clientbound + +import it.unimi.dsi.fastutil.bytes.ByteArrayList +import ru.dbotthepony.kommons.io.readByteArray +import ru.dbotthepony.kommons.io.writeByteArray +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.network.IClientPacket +import java.io.DataInputStream +import java.io.DataOutputStream + +class SystemObjectCreatePacket(val data: ByteArrayList) : IClientPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(ByteArrayList.wrap(stream.readByteArray())) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByteArray(data.elements(), 0, data.size) + } + + override fun play(connection: ClientConnection) { + TODO("Not yet implemented") + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemObjectDestroyPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemObjectDestroyPacket.kt new file mode 100644 index 00000000..f7fbfce2 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemObjectDestroyPacket.kt @@ -0,0 +1,21 @@ +package ru.dbotthepony.kstarbound.network.packets.clientbound + +import ru.dbotthepony.kommons.io.readUUID +import ru.dbotthepony.kommons.io.writeUUID +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.network.IClientPacket +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.UUID + +class SystemObjectDestroyPacket(val uuid: UUID) : IClientPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readUUID()) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeUUID(uuid) + } + + override fun play(connection: ClientConnection) { + TODO("Not yet implemented") + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemShipCreatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemShipCreatePacket.kt new file mode 100644 index 00000000..f35b97cd --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemShipCreatePacket.kt @@ -0,0 +1,21 @@ +package ru.dbotthepony.kstarbound.network.packets.clientbound + +import it.unimi.dsi.fastutil.bytes.ByteArrayList +import ru.dbotthepony.kommons.io.readByteArray +import ru.dbotthepony.kommons.io.writeByteArray +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.network.IClientPacket +import java.io.DataInputStream +import java.io.DataOutputStream + +class SystemShipCreatePacket(val data: ByteArrayList) : IClientPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(ByteArrayList.wrap(stream.readByteArray())) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByteArray(data.elements(), 0, data.size) + } + + override fun play(connection: ClientConnection) { + TODO("Not yet implemented") + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemShipDestroyPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemShipDestroyPacket.kt new file mode 100644 index 00000000..b6aa98a9 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemShipDestroyPacket.kt @@ -0,0 +1,21 @@ +package ru.dbotthepony.kstarbound.network.packets.clientbound + +import ru.dbotthepony.kommons.io.readUUID +import ru.dbotthepony.kommons.io.writeUUID +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.network.IClientPacket +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.UUID + +class SystemShipDestroyPacket(val uuid: UUID) : IClientPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readUUID()) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeUUID(uuid) + } + + override fun play(connection: ClientConnection) { + TODO("Not yet implemented") + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemWorldStartPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemWorldStartPacket.kt new file mode 100644 index 00000000..4a937618 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemWorldStartPacket.kt @@ -0,0 +1,40 @@ +package ru.dbotthepony.kstarbound.network.packets.clientbound + +import it.unimi.dsi.fastutil.bytes.ByteArrayList +import ru.dbotthepony.kommons.io.readByteArray +import ru.dbotthepony.kommons.io.readCollection +import ru.dbotthepony.kommons.io.readUUID +import ru.dbotthepony.kommons.io.readVector3i +import ru.dbotthepony.kommons.io.writeByteArray +import ru.dbotthepony.kommons.io.writeCollection +import ru.dbotthepony.kommons.io.writeStruct3i +import ru.dbotthepony.kommons.io.writeUUID +import ru.dbotthepony.kommons.vector.Vector3i +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.network.IClientPacket +import ru.dbotthepony.kstarbound.world.SystemWorldLocation +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.UUID + +class SystemWorldStartPacket(val location: Vector3i, val objects: Collection, val ships: Collection, val shipUUID: UUID, val shipLocation: SystemWorldLocation) : IClientPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + stream.readVector3i(), + stream.readCollection { ByteArrayList.wrap(readByteArray()) }, + stream.readCollection { ByteArrayList.wrap(readByteArray()) }, + stream.readUUID(), + SystemWorldLocation.read(stream, isLegacy) + ) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeStruct3i(location) + stream.writeCollection(objects) { writeByteArray(it.elements(), 0, it.size) } + stream.writeCollection(ships) { writeByteArray(it.elements(), 0, it.size) } + stream.writeUUID(shipUUID) + shipLocation.write(stream, isLegacy) + } + + override fun play(connection: ClientConnection) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemWorldUpdatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemWorldUpdatePacket.kt new file mode 100644 index 00000000..4719a53f --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SystemWorldUpdatePacket.kt @@ -0,0 +1,30 @@ +package ru.dbotthepony.kstarbound.network.packets.clientbound + +import it.unimi.dsi.fastutil.bytes.ByteArrayList +import ru.dbotthepony.kommons.io.readByteArray +import ru.dbotthepony.kommons.io.readMap +import ru.dbotthepony.kommons.io.readUUID +import ru.dbotthepony.kommons.io.writeByteArray +import ru.dbotthepony.kommons.io.writeMap +import ru.dbotthepony.kommons.io.writeUUID +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.network.IClientPacket +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.UUID + +class SystemWorldUpdatePacket(val objects: Map, val ships: Map) : IClientPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + stream.readMap({ readUUID() }, { ByteArrayList.wrap(readByteArray()) }), + stream.readMap({ readUUID() }, { ByteArrayList.wrap(readByteArray()) }) + ) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeMap(objects, { writeUUID(it) }, { writeByteArray(it.elements(), 0, it.size) }) + stream.writeMap(ships, { writeUUID(it) }, { writeByteArray(it.elements(), 0, it.size) }) + } + + override fun play(connection: ClientConnection) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/UpdateWorldPropertiesPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/UpdateWorldPropertiesPacket.kt new file mode 100644 index 00000000..0c0323f1 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/UpdateWorldPropertiesPacket.kt @@ -0,0 +1,34 @@ +package ru.dbotthepony.kstarbound.network.packets.clientbound + +import com.google.gson.JsonObject +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.json.mergeJson +import ru.dbotthepony.kstarbound.json.readJsonObject +import ru.dbotthepony.kstarbound.json.writeJsonObject +import ru.dbotthepony.kstarbound.network.IClientPacket +import ru.dbotthepony.kstarbound.network.IServerPacket +import ru.dbotthepony.kstarbound.server.ServerConnection +import java.io.DataInputStream +import java.io.DataOutputStream + +class UpdateWorldPropertiesPacket(val update: JsonObject) : IClientPacket, IServerPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readJsonObject()) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeJsonObject(update) + } + + override fun play(connection: ClientConnection) { + connection.enqueue { + world?.updateProperties(update) + } + } + + override fun play(connection: ServerConnection) { + connection.enqueue { + updateProperties(update) + broadcast(this@UpdateWorldPropertiesPacket) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/CelestialRequestPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/CelestialRequestPacket.kt new file mode 100644 index 00000000..ab9059ac --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/CelestialRequestPacket.kt @@ -0,0 +1,62 @@ +package ru.dbotthepony.kstarbound.network.packets.serverbound + +import kotlinx.coroutines.launch +import ru.dbotthepony.kommons.io.readCollection +import ru.dbotthepony.kommons.io.readVector2i +import ru.dbotthepony.kommons.io.readVector3i +import ru.dbotthepony.kommons.io.writeCollection +import ru.dbotthepony.kommons.io.writeStruct2i +import ru.dbotthepony.kommons.io.writeStruct3i +import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kommons.vector.Vector3i +import ru.dbotthepony.kstarbound.defs.world.CelestialParameters +import ru.dbotthepony.kstarbound.io.readEither +import ru.dbotthepony.kstarbound.io.writeEither +import ru.dbotthepony.kstarbound.network.IServerPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.CelestialResponsePacket +import ru.dbotthepony.kstarbound.server.ServerConnection +import ru.dbotthepony.kstarbound.world.UniversePos +import java.io.DataInputStream +import java.io.DataOutputStream + +class CelestialRequestPacket(val requests: Collection>) : IServerPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readCollection { readEither(isLegacy, { readVector2i() }, { readVector3i() }) }) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeCollection(requests) { + writeEither(it, isLegacy, { writeStruct2i(it) }, { writeStruct3i(it) }) + } + } + + override fun play(connection: ServerConnection) { + connection.scope.launch { + val responses = ArrayList>() + + for (request in requests) { + if (request.isLeft) { + val chunkPos = request.left() + responses.add(Either.left(connection.server.universe.getChunk(chunkPos)?.toNetwork() ?: continue)) + } else { + val systemPos = UniversePos(request.right()) + val map = HashMap() + + for (planet in connection.server.universe.children(systemPos)) { + val planetData = connection.server.universe.parameters(planet) ?: continue + val children = HashMap() + + for (satellite in connection.server.universe.children(planet)) { + children[satellite.satelliteOrbit] = connection.server.universe.parameters(satellite) ?: continue + } + + map[planet.planetOrbit] = CelestialResponsePacket.PlanetData(planetData, children) + } + + responses.add(Either.right(CelestialResponsePacket.SystemData(systemPos.location, map))) + } + } + + connection.send(CelestialResponsePacket(responses)) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/EntityInteractPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/EntityInteractPacket.kt new file mode 100644 index 00000000..52478263 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/EntityInteractPacket.kt @@ -0,0 +1,47 @@ +package ru.dbotthepony.kstarbound.network.packets.serverbound + +import ru.dbotthepony.kommons.io.readUUID +import ru.dbotthepony.kommons.io.writeUUID +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.defs.InteractAction +import ru.dbotthepony.kstarbound.defs.InteractRequest +import ru.dbotthepony.kstarbound.network.Connection +import ru.dbotthepony.kstarbound.network.IClientPacket +import ru.dbotthepony.kstarbound.network.IServerPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.EntityInteractResultPacket +import ru.dbotthepony.kstarbound.server.ServerConnection +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.UUID + +class EntityInteractPacket(val request: InteractRequest, val id: UUID) : IServerPacket, IClientPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + InteractRequest(stream, isLegacy), + stream.readUUID() + ) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + request.write(stream, isLegacy) + stream.writeUUID(id) + } + + override fun play(connection: ServerConnection) { + if (request.target >= 0) { + connection.enqueue { + connection.send(EntityInteractResultPacket(entities[request.target]?.interact(request) ?: InteractAction.NONE, id, request.source)) + } + } else { + val other = connection.server.channels.connectionByID(Connection.connectionForEntityID(request.target)) ?: throw IllegalArgumentException("No such connection ID ${Connection.connectionForEntityID(request.target)} for EntityInteractPacket") + + if (other == connection) { + throw IllegalStateException("Attempt to interact with own entity through server?") + } + + other.send(this) + } + } + + override fun play(connection: ClientConnection) { + TODO("Not yet implemented") + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/FlyShipPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/FlyShipPacket.kt new file mode 100644 index 00000000..0143ba63 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/FlyShipPacket.kt @@ -0,0 +1,23 @@ +package ru.dbotthepony.kstarbound.network.packets.serverbound + +import ru.dbotthepony.kommons.io.readVector3i +import ru.dbotthepony.kommons.io.writeStruct3i +import ru.dbotthepony.kommons.vector.Vector3i +import ru.dbotthepony.kstarbound.network.IServerPacket +import ru.dbotthepony.kstarbound.server.ServerConnection +import ru.dbotthepony.kstarbound.world.SystemWorldLocation +import java.io.DataInputStream +import java.io.DataOutputStream + +class FlyShipPacket(val system: Vector3i, val location: SystemWorldLocation = SystemWorldLocation.Transit) : IServerPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readVector3i(), SystemWorldLocation.read(stream, isLegacy)) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeStruct3i(system) + location.write(stream, isLegacy) + } + + override fun play(connection: ServerConnection) { + connection.flyShip(system, location) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/Factories.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/Factories.kt index 08c50a08..6bf1c5c8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/Factories.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/Factories.kt @@ -160,7 +160,7 @@ fun > networkedEnum(value: E) = BasicNetworkedElement(value, StreamC // networks enum as a signed variable length integer on legacy protocol fun > networkedEnumStupid(value: E): BasicNetworkedElement { val codec = StreamCodec.Enum(value::class.java) - return BasicNetworkedElement(value, codec, VarIntValueCodec, { it.ordinal.shl(1) }, { codec.values[it.ushr(1)] }) + return BasicNetworkedElement(value, codec, VarIntValueCodec, { it.ordinal }, { codec.values[it] }) } // networks enum as string on legacy protocol diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerChannels.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerChannels.kt index c8302484..a2e23bd6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerChannels.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerChannels.kt @@ -45,6 +45,10 @@ class ServerChannels(val server: StarboundServer) : Closeable { broadcast(ServerInfoPacket(new, server.settings.maxPlayers)) } + fun connectionByID(id: Int): ServerConnection? { + return connections.firstOrNull { it.connectionID == id } + } + private fun cycleConnectionID(): Int { val v = ++nextConnectionID and MAX_PLAYERS diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt index 8c069b8c..35c27d3e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt @@ -3,16 +3,20 @@ package ru.dbotthepony.kstarbound.server import com.google.gson.JsonObject import io.netty.channel.ChannelHandlerContext import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet +import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay import kotlinx.coroutines.future.await import kotlinx.coroutines.launch import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.io.ByteKey import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kommons.vector.Vector3i +import ru.dbotthepony.kstarbound.GlobalDefaults import ru.dbotthepony.kstarbound.defs.WarpAction import ru.dbotthepony.kstarbound.defs.WarpAlias import ru.dbotthepony.kstarbound.defs.WorldID -import ru.dbotthepony.kstarbound.defs.world.SkyType +import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParameters import ru.dbotthepony.kstarbound.network.Connection import ru.dbotthepony.kstarbound.network.ConnectionSide import ru.dbotthepony.kstarbound.network.ConnectionType @@ -23,11 +27,12 @@ import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPac import ru.dbotthepony.kstarbound.server.world.ServerWorldTracker import ru.dbotthepony.kstarbound.server.world.WorldStorage import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage +import ru.dbotthepony.kstarbound.server.world.ServerSystemWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld +import ru.dbotthepony.kstarbound.world.SystemWorldLocation +import ru.dbotthepony.kstarbound.world.UniversePos import java.util.HashMap import java.util.UUID -import java.util.concurrent.CompletableFuture -import java.util.concurrent.TimeUnit import kotlin.properties.Delegates // serverside part of connection @@ -35,13 +40,16 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn var tracker: ServerWorldTracker? = null var worldStartAcknowledged = false var returnWarp: WarpAction? = null + var systemWorld: ServerSystemWorld? = null val world: ServerWorld? get() = tracker?.world // packets which interact with world must be // executed on world's thread - fun enqueue(task: ServerWorld.() -> Unit) = tracker?.enqueue(task) + fun enqueue(task: ServerWorld.() -> Unit) { + return tracker?.enqueue(task) ?: throw IllegalStateException("Not in world.") + } lateinit var shipWorld: ServerWorld private set @@ -62,6 +70,10 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn return "ServerConnection[$nickname $uuid ID=$connectionID channel=$channel / $world]" } + fun alias(): String { + return "$nickname <$connectionID/$uuid>" + } + private val shipChunks = HashMap>() private val modifiedShipChunks = ObjectOpenHashSet() var shipChunkSource by Delegates.notNull() @@ -127,9 +139,12 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn private val warpQueue = Channel>(capacity = 10) - private suspend fun handleWarps() { + private suspend fun warpEventLoop() { while (true) { - val (request, deploy) = warpQueue.receive() + var (request, deploy) = warpQueue.receive() + + if (request is WarpAlias) + request = request.remap(this) LOGGER.info("Trying to warp $this to $request") @@ -163,6 +178,139 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn } } + private val flyShipQueue = Channel>(capacity = 40) + + fun flyShip(system: Vector3i, location: SystemWorldLocation) { + flyShipQueue.trySend(system to location) + } + + // coordinates ship flights between systems + private suspend fun shipFlightEventLoop() { + shipWorld.sky.startFlying(true, true) + var visited = 0 + + LOGGER.info("Finding starter world for ${alias()}...") + val params = GlobalDefaults.universeServer.findStarterWorldParameters + + // visit all since sparsingly trying to find specific world is not healthy performance wise + var found = server.universe.findRandomWorld(params.tries, params.range, visitAll = true, predicate = { + if (++visited % 600 == 0) { + LOGGER.info("Still finding starter world for ${alias()}...") + } + + val parameters = server.universe.parameters(it) ?: return@findRandomWorld false + if (parameters.visitableParameters == null) return@findRandomWorld false + if (!params.starterWorld.test(parameters.visitableParameters!!)) return@findRandomWorld false + + val children = ArrayList() + + for (child in server.universe.children(it.system())) { + val p = server.universe.parameters(child) + + if (p?.visitableParameters != null) { + children.add(p.visitableParameters!!) + } + + for (child2 in server.universe.children(child)) { + val p2 = server.universe.parameters(child2) + + if (p2?.visitableParameters != null) { + children.add(p2.visitableParameters!!) + } + } + } + + params.requiredSystemWorlds.all { predicate -> children.any { predicate.test(it) } } + }) + + if (found == null) { + LOGGER.fatal("Unable to find starter world for $this!") + disconnect("Unable to find starter world") + return + } + + LOGGER.info("Found appropriate starter world at $found for ${alias()}") + + var world = server.loadSystemWorld(found.location).await() + var ship = world.addClient(this).await() + shipWorld.sky.stopFlyingAt(ship.location.skyParameters(world)) + shipCoordinate = found + + var currentFlightJob: Job? = null + + while (true) { + val (system, location) = flyShipQueue.receive() + + val currentSystem = systemWorld + + if (system == currentSystem?.location) { + // fly ship in current system + currentFlightJob?.cancel() + val flight = currentSystem.flyShip(this, location) + + shipWorld.mailbox.execute { + shipWorld.sky.startFlying(false) + } + + currentFlightJob = scope.launch { + val coords = flight.await() + val action = coords.orbitalAction(currentSystem) + orbitalWarpAction = action + + for (client in shipWorld.clients) { + client.client.orbitalWarpAction = action + } + + val sky = coords.skyParameters(world) + + shipWorld.mailbox.execute { + shipWorld.sky.stopFlyingAt(sky) + } + } + + orbitalWarpAction = KOptional() + + for (client in shipWorld.clients) { + client.client.orbitalWarpAction = KOptional() + } + } else { + // we need to travel to other system + val exists = server.universe.parameters(UniversePos(system)) != null + + if (!exists) + continue + + currentFlightJob?.cancel() + + shipWorld.mailbox.execute { + shipWorld.sky.startFlying(true) + } + + LOGGER.info("${alias()} is flying to new system: ${UniversePos(system)}") + val newSystem = server.loadSystemWorld(system) + + shipCoordinate = UniversePos(system) + + orbitalWarpAction = KOptional() + + for (client in shipWorld.clients) { + client.client.orbitalWarpAction = KOptional() + } + + delay((GlobalDefaults.universeServer.queuedFlightWaitTime * 1000L).toLong()) + + world = newSystem.await() + ship = world.addClient(this).await() + + val newParams = ship.location.skyParameters(world) + + shipWorld.mailbox.execute { + shipWorld.sky.stopFlyingAt(newParams) + } + } + } + } + fun enqueueWarp(destination: WarpAction, deploy: Boolean = false) { warpQueue.trySend(destination to deploy) } @@ -242,7 +390,11 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn shipWorld = it shipWorld.thread.start() enqueueWarp(WarpAlias.OwnShip) - coroutineScope.launch { handleWarps() } + shipUpgrades = shipUpgrades.addCapability("planetTravel") + shipUpgrades = shipUpgrades.addCapability("teleport") + shipUpgrades = shipUpgrades.copy(maxFuel = 10000, shipLevel = 1) + scope.launch { warpEventLoop() } + scope.launch { shipFlightEventLoop() } if (server.channels.connections.size > 1) { enqueueWarp(WarpAction.Player(server.channels.connections.first().uuid!!)) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt index 257af535..8f91b3e6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt @@ -1,25 +1,36 @@ 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.GlobalDefaults 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 { @@ -38,6 +49,25 @@ sealed class StarboundServer(val root: File) : Closeable { 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) @@ -108,6 +138,29 @@ sealed class StarboundServer(val root: File) : Closeable { } } + // 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 } @@ -116,6 +169,7 @@ sealed class StarboundServer(val root: File) : Closeable { if (isClosed) return isClosed = true + context.cancel("Server shutting down") channels.close() worlds.values.forEach { it.close() } limboWorlds.forEach { it.close() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt new file mode 100644 index 00000000..0c82c4cd --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt @@ -0,0 +1,516 @@ +package ru.dbotthepony.kstarbound.server.world + +import com.google.common.collect.ImmutableList +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import it.unimi.dsi.fastutil.bytes.ByteArrayList +import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream +import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeUUID +import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kommons.vector.Vector3i +import ru.dbotthepony.kstarbound.GlobalDefaults +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.world.SystemWorldObjectConfig +import ru.dbotthepony.kstarbound.io.writeStruct2d +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.writeJsonObject +import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemObjectCreatePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemObjectDestroyPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemShipCreatePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemShipDestroyPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemWorldStartPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemWorldUpdatePacket +import ru.dbotthepony.kstarbound.server.ServerConnection +import ru.dbotthepony.kstarbound.server.StarboundServer +import ru.dbotthepony.kstarbound.util.random.nextRange +import ru.dbotthepony.kstarbound.util.random.random +import ru.dbotthepony.kstarbound.util.random.staticRandom64 +import ru.dbotthepony.kstarbound.world.SystemWorld +import ru.dbotthepony.kstarbound.world.SystemWorldLocation +import ru.dbotthepony.kstarbound.world.UniversePos +import java.io.Closeable +import java.io.DataOutputStream +import java.util.UUID +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.atomic.AtomicBoolean +import java.util.function.Supplier +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin + +class ServerSystemWorld : SystemWorld { + @JsonFactory + data class JsonData( + val location: Vector3i, + val lastSpawn: Double, + val objectSpawnTime: Double, + val objects: ImmutableList = ImmutableList.of(), + ) + + override val entities = HashMap() + override val ships = HashMap() + + private class Task(val supplier: Supplier) : Runnable { + val future = CompletableFuture() + + override fun run() { + try { + future.complete(supplier.get()) + } catch (err: Throwable) { + future.completeExceptionally(err) + } + } + } + + private val tasks = ConcurrentLinkedQueue>() + + override fun toString(): String { + return "ServerSystemWorld at $systemLocation" + } + + @Suppress("NAME_SHADOWING") + private fun addClient0(client: ServerConnection, shipSpeed: Double, location: SystemWorldLocation): ServerShip { + if (!client.isConnected || client.uuid == null) + throw IllegalStateException("Trying to add disconnected client, or client without UUID: $client") + + if (client.uuid!! in ships) + throw IllegalStateException("Already has client $client in $this!") + + var location = location + + if (location is SystemWorldLocation.Entity && location.uuid !in entities) + location = SystemWorldLocation.Transit + + if (location == SystemWorldLocation.Transit) + location = SystemWorldLocation.Position(randomArrivalPosition()) + + client.systemWorld?.removeClient(client) + client.systemWorld = this + + val objects = entities.values.map { it.writeNetwork(client.isLegacy) } + val ships = ships.values.map { it.writeNetwork(client.isLegacy) } + + val ship = ServerShip(client, location) + ship.speed = shipSpeed + + client.send(SystemWorldStartPacket(this.location, objects, ships, client.uuid!!, location)) + + val legacyPacket by lazy { SystemShipCreatePacket(ship.writeNetwork(true)) } + val nativePacket by lazy { SystemShipCreatePacket(ship.writeNetwork(false)) } + + for (otherShip in this.ships.values) { + if (otherShip != ship) { + otherShip.client.send(if (otherShip.client.isLegacy) legacyPacket else nativePacket) + } + } + + return ship + } + + private fun removeClient0(client: ServerConnection) { + val ship = ships.remove(client.uuid) ?: throw IllegalStateException("No client $client in $this!") + + val packet = SystemShipDestroyPacket(ship.uuid) + ships.values.forEach { it.client.send(packet) } + } + + fun addClient(client: ServerConnection, shipSpeed: Double = GlobalDefaults.systemWorld.clientShip.speed, location: SystemWorldLocation = SystemWorldLocation.Transit): CompletableFuture { + val task = Task { addClient0(client, shipSpeed, location) } + tasks.add(task) + return task.future + } + + fun removeClient(client: ServerConnection): CompletableFuture { + val task = Task { removeClient0(client) } + tasks.add(task) + return task.future + } + + private fun flyShip0(client: ServerConnection, location: SystemWorldLocation, future: CompletableFuture) { + val ship = ships[client.uuid] ?: throw IllegalStateException("No client $client in $this!") + ship.destination(location, future) + } + + fun flyShip(client: ServerConnection, location: SystemWorldLocation): CompletableFuture { + val future = CompletableFuture() + val task = Task { flyShip0(client, location, future) } + tasks.add(task) + return future + } + + val server: StarboundServer + var objectSpawnTime: Double + private set + var lastSpawn = 0.0 + private set + + private constructor(server: StarboundServer, location: Vector3i) : super(location, server.universeClock, server.universe) { + this.server = server + this.lastSpawn = clock.seconds - GlobalDefaults.systemWorld.objectSpawnCycle + objectSpawnTime = random.nextRange(GlobalDefaults.systemWorld.objectSpawnInterval) + } + + private constructor(server: StarboundServer, data: JsonData) : super(data.location, server.universeClock, server.universe) { + this.server = server + objectSpawnTime = data.objectSpawnTime + + for (obj in data.objects) { + ServerEntity(obj) + } + + this.lastSpawn = data.lastSpawn + } + + private suspend fun spawnInitialObjects() { + val random = random(staticRandom64("SystemWorldGeneration", location.toString())) + + GlobalDefaults.systemWorld.initialObjectPools.sample(random).ifPresent { + for (i in 0 until it.first) { + val name = it.second.sample(random).orNull() ?: return@ifPresent + val uuid = UUID(random.nextLong(), random.nextLong()) + val prototype = GlobalDefaults.systemObjects[name] ?: throw NullPointerException("Tried to create $name system world object, but there is no such object in /system_objects.config!") + val create = ServerEntity(prototype.create(uuid, name), uuid, randomObjectSpawnPosition(), clock.seconds) + create.enterOrbit(UniversePos(location), Vector2d.ZERO, clock.seconds) // orbit center of system + } + } + } + + private suspend fun spawnObjects() { + var diff = GlobalDefaults.systemWorld.objectSpawnCycle.coerceAtMost(clock.seconds - lastSpawn) + lastSpawn = clock.seconds - diff + + while (diff > objectSpawnTime) { + lastSpawn += objectSpawnTime + objectSpawnTime = random.nextRange(GlobalDefaults.systemWorld.objectSpawnInterval) + diff = clock.seconds - lastSpawn + + GlobalDefaults.systemWorld.objectSpawnPool.sample(random).ifPresent { + val uuid = UUID(random.nextLong(), random.nextLong()) + val config = GlobalDefaults.systemObjects[it]?.create(uuid, it) ?: throw NullPointerException("Tried to create $it system world object, but there is no such object in /system_objects.config!") + val pos = randomObjectSpawnPosition() + + if (clock.seconds > lastSpawn + objectSpawnTime && config.moving) { + // if this is not the last object we're spawning, and it's moving, immediately put it in orbit around a planet + val targets = universe.children(systemLocation).filter { child -> + entities.values.none { it.orbit.map { it.target == child }.orElse(false) } + } + + if (targets.isNotEmpty()) { + val target = targets.random(random) + val targetPosition = planetPosition(target) + val relativeOrbit = (pos - targetPosition).unitVector * (clusterSize(target) / 2.0 + config.orbitDistance) + ServerEntity(config, uuid, targetPosition + relativeOrbit, lastSpawn).enterOrbit(target, targetPosition, lastSpawn) + } else { + ServerEntity(config, uuid, pos, lastSpawn) + } + } else { + ServerEntity(config, uuid, pos, lastSpawn) + } + } + } + } + + private suspend fun randomObjectSpawnPosition(): Vector2d { + val spawnRanges = ArrayList() + val orbits = universe.children(systemLocation) + + suspend fun addSpawn(inner: UniversePos, outer: UniversePos) { + val min = planetOrbitDistance(inner) + clusterSize(inner) / 2.0 + GlobalDefaults.systemWorld.objectSpawnPadding + val max = planetOrbitDistance(outer) - clusterSize(outer) / 2.0 - GlobalDefaults.systemWorld.objectSpawnPadding + spawnRanges.add(Vector2d(min, max)) + } + + addSpawn(systemLocation, orbits.first()) + + for (i in 1 until orbits.size) + addSpawn(orbits[i - 1], orbits[i]) + + val outer = orbits.last() + val rim = planetOrbitDistance(outer) + clusterSize(outer) / 2.0 + GlobalDefaults.systemWorld.objectSpawnPadding + spawnRanges.add(Vector2d(rim, rim + GlobalDefaults.systemWorld.objectSpawnPadding)) + + val range = spawnRanges.random(random) + val angle = random.nextDouble() * PI * 2.0 + val mag = range.x + random.nextDouble() * (range.y - range.x) + return Vector2d(cos(angle) * mag, sin(angle) * mag) + } + + fun toJson(): JsonObject { + return Starbound.gson.toJsonTree(JsonData( + location = location, + lastSpawn = lastSpawn, + objectSpawnTime = objectSpawnTime, + objects = entities.values.stream().map { it.toJson() }.collect(ImmutableList.toImmutableList()), + )) as JsonObject + } + + private var ticksWithoutPlayers = 0 + + fun shouldClose(): Boolean { + return ticksWithoutPlayers > 1800 + } + + // in original engine, ticking happens at 20 updates per second + // Since there is no Lua driven code, we can tick as fast as we want + suspend fun tick(delta: Double = Starbound.TIMESTEP) { + var next = tasks.poll() + + while (next != null) { + next.run() + next = tasks.poll() + } + + entities.values.forEach { it.tick(delta) } + ships.values.forEach { it.tick(delta) } + + entities.values.removeIf { + if (it.hasExpired && ships.values.none { ship -> ship.location == it.location }) { + val packet = SystemObjectDestroyPacket(it.uuid) + + ships.values.forEach { ship -> + ship.client.send(packet) + } + + return@removeIf true + } + + return@removeIf false + } + + // spawnObjects() + + ships.values.forEach { it.sendUpdates() } + + if (ships.isEmpty()) + ticksWithoutPlayers++ + else + ticksWithoutPlayers = 0 + } + + inner class ServerShip(val client: ServerConnection, location: SystemWorldLocation) : Ship(client.uuid!!, location) { + init { + ships[uuid] = this + } + + private val netVersions = Object2LongOpenHashMap() + + suspend fun tick(delta: Double = Starbound.TIMESTEP) { + val orbit = destination as? SystemWorldLocation.Orbit + + // if destination is an orbit we haven't started orbiting yet, update the time + if (orbit != null) + destination = SystemWorldLocation.Orbit(orbit.position.copy(enterTime = clock.seconds)) + + suspend fun nearPlanetOrbit(planet: UniversePos): Orbit { + val toShip = planetPosition(planet) - position + val angle = toShip.toAngle() + return Orbit(planet, 1, clock.seconds, Vector2d(cos(angle), sin(angle)) * (planetSize(planet) / 2.0 + GlobalDefaults.systemWorld.clientShip.orbitDistance)) + } + + if (location is SystemWorldLocation.Celestial) { + if (this.orbit?.target != (location as SystemWorldLocation.Celestial).position) + this.orbit = nearPlanetOrbit((location as SystemWorldLocation.Celestial).position) + } else if (location == SystemWorldLocation.Transit) { + departTimer = (departTimer - delta).coerceAtLeast(0.0) + + if (departTimer > 0.0) + return + + if (destination is SystemWorldLocation.Celestial) { + if (this.orbit?.target != (destination as SystemWorldLocation.Celestial).position) + this.orbit = nearPlanetOrbit((destination as SystemWorldLocation.Celestial).position) + } else { + this.orbit = null + } + + val destination: Vector2d + + if (this.orbit != null) { + this.orbit = this.orbit!!.copy(enterTime = clock.seconds) + destination = orbitPosition(this.orbit!!) + } else { + destination = this.destination.resolve(this@ServerSystemWorld) ?: position + } + + val toTarget = destination - position + // don't overshoot + position += toTarget.unitVector * (speed * delta).coerceAtMost(toTarget.length.coerceAtMost(1.0)) + + if (destination.distanceSquared(position) <= 0.1) { + location = this.destination + this.destination = SystemWorldLocation.Transit + destinationFuture.complete(location) + } else { + return + } + } + + if (this.orbit != null) { + position = orbitPosition(this.orbit!!) + } else { + position = location.resolve(this@ServerSystemWorld) ?: position + } + } + + fun sendUpdates() { + val objects = HashMap() + val ships = HashMap() + + for ((id, ship) in this@ServerSystemWorld.ships) { + val version = netVersions.getLong(id) + + if (version == 0L || ship.networkGroup.upstream.hasChangedSince(version)) { + val (data, newVersion) = ship.networkGroup.write(version, client.isLegacy) + netVersions.put(id, newVersion) + ships[id] = data + } + } + + for ((id, entity) in this@ServerSystemWorld.entities) { + val version = netVersions.getLong(id) + + if (version == 0L || entity.networkGroup.upstream.hasChangedSince(version)) { + val (data, newVersion) = entity.networkGroup.write(version, client.isLegacy) + netVersions.put(id, newVersion) + objects[id] = data + } + } + + client.send(SystemWorldUpdatePacket(objects, ships)) + } + + private var destinationFuture = CompletableFuture() + + fun destination(destination: SystemWorldLocation, future: CompletableFuture) { + destinationFuture.cancel(false) + destinationFuture = future + + if (location is SystemWorldLocation.Celestial || location is SystemWorldLocation.Entity) + departTimer = GlobalDefaults.systemWorld.clientShip.departTime + else if (destination == SystemWorldLocation.Transit) + departTimer = GlobalDefaults.systemWorld.clientShip.spaceDepartTime + + this.destination = destination + this.location = SystemWorldLocation.Transit + } + + fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeUUID(uuid) + location.write(stream, isLegacy) + } + + fun writeNetwork(isLegacy: Boolean): ByteArrayList { + val data = FastByteArrayOutputStream() + writeNetwork(DataOutputStream(data), isLegacy) + return ByteArrayList.wrap(data.array, data.length) + } + } + + inner class ServerEntity(data: SystemWorldObjectConfig.Data, uuid: UUID, position: Vector2d, spawnTime: Double = 0.0, parameters: JsonObject = JsonObject()) : Entity(data, uuid, position, spawnTime, parameters) { + constructor(data: EntityJsonData) : this( + GlobalDefaults.systemObjects[data.name]?.create(data.actualUUID, data.name) ?: throw NullPointerException("Tried to create ${data.name} system world object, but there is no such object in /system_objects.config!"), + data.actualUUID, + data.position, + data.spawnTime, + data.parameters + ) { + if (data.orbit != null) { + orbit = KOptional(data.orbit) + } + } + + constructor(data: JsonObject) : this(Starbound.gson.fromJson(data, EntityJsonData::class.java)) + + init { + entities[uuid] = this + + val legacy by lazy { SystemObjectCreatePacket(writeNetwork(true)) } + val native by lazy { SystemObjectCreatePacket(writeNetwork(false)) } + + ships.values.forEach { + it.client.send(if (it.client.isLegacy) legacy else native) + } + } + + val location = SystemWorldLocation.Entity(uuid) + + var hasExpired = false + private set + + suspend fun tick(delta: Double = Starbound.TIMESTEP) { + if (!data.permanent && spawnTime > 0.0 && clock.seconds > spawnTime + data.lifeTime) + hasExpired = true + + val orbit = orbit.orNull() + + if (orbit != null) { + position = orbitPosition(orbit) + } else if (data.permanent || !data.moving) { + // permanent locations always have a solar orbit + enterOrbit(systemLocation, Vector2d.ZERO, clock.seconds) + } else if (approach != null) { + if (ships.values.any { it.location == location }) + return + + if (approach!!.isPlanet) { + val approach = planetPosition(approach!!) + val toApproach = approach - position + + position += toApproach.unitVector * data.speed * delta + + if ((approach - position).length < planetSize(this.approach!!) + data.orbitDistance) + enterOrbit(this.approach!!, approach, clock.seconds) + } else { + enterOrbit(approach!!, Vector2d.ZERO, clock.seconds) + } + } else { + val planets = universe.children(systemLocation).filter { child -> + entities.values.none { it.orbit.map { it.target == child }.orElse(false) } + } + + if (planets.isNotEmpty()) + approach = planets.random(random) + } + } + + fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeUUID(uuid) + stream.writeBinaryString(data.name) + stream.writeStruct2d(position, isLegacy) + stream.writeJsonObject(parameters) + } + + fun writeNetwork(isLegacy: Boolean): ByteArrayList { + val data = FastByteArrayOutputStream() + writeNetwork(DataOutputStream(data), isLegacy) + return ByteArrayList.wrap(data.array, data.length) + } + } + + companion object { + private val LOGGER = LogManager.getLogger() + + suspend fun create(server: StarboundServer, location: Vector3i): ServerSystemWorld { + LOGGER.info("Creating new System World at $location") + val world = ServerSystemWorld(server, location) + world.spawnInitialObjects() + world.spawnObjects() + return world + } + + suspend fun load(server: StarboundServer, data: JsonElement): ServerSystemWorld { + val load = Starbound.gson.fromJson(data, JsonData::class.java) + LOGGER.info("Loading System World at ${load.location}") + val world = ServerSystemWorld(server, load) + world.spawnObjects() + return world + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerUniverse.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerUniverse.kt index de425e0d..95cbf808 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerUniverse.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerUniverse.kt @@ -21,6 +21,8 @@ import ru.dbotthepony.kstarbound.defs.world.CelestialNames import ru.dbotthepony.kstarbound.defs.world.CelestialParameters import ru.dbotthepony.kstarbound.fromJson import ru.dbotthepony.kstarbound.io.BTreeDB5 +import ru.dbotthepony.kstarbound.util.random.nextRange +import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.staticRandom64 import ru.dbotthepony.kstarbound.world.Universe import ru.dbotthepony.kstarbound.world.UniversePos @@ -94,7 +96,7 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab return listOf() } - override suspend fun scanSystems(region: AABBi, includedTypes: Set?): List { + override suspend fun findSystems(region: AABBi, includedTypes: Set?): List { val copy = if (includedTypes != null) ObjectOpenHashSet(includedTypes) else null val futures = ArrayList>>() @@ -126,7 +128,64 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab return futures.stream().flatMap { it.get().stream() }.toList() } - override suspend fun scanConstellationLines(region: AABBi): List> { + override suspend fun scanSystems(region: AABBi, callback: suspend (UniversePos) -> KOptional): KOptional { + for (pos in chunkPositions(region)) { + val chunk = getChunk(pos) ?: continue + + for (system in chunk.systems.keys) { + val result = callback(UniversePos(system)) + + if (result.isPresent) { + return result + } + } + } + + return KOptional() + } + + suspend fun findRandomWorld(tries: Int, range: Int, seed: Long? = null, visitAll: Boolean = false, predicate: suspend (UniversePos) -> Boolean = { true }): UniversePos? { + require(tries > 0) { "Non-positive amount of tries: $tries" } + require(range > 0) { "Non-positive range: $range" } + + val random = random(seed ?: System.nanoTime()) + val rect = AABBi(Vector2i(-range, -range), Vector2i(range, range)) + + for (i in 0 until tries) { + val x = random.nextRange(baseInformation.xyCoordRange) + val y = random.nextRange(baseInformation.xyCoordRange) + + val result = scanSystems(rect + Vector2i(x, y)) { + val children = children(it) + + if (children.isEmpty()) + return@scanSystems KOptional() + + if (visitAll) { + for (world in children) + if (predicate(world)) + return@scanSystems KOptional(world) + } else { + // This sucks, 50% of the time will try and return satellite, not really + // balanced probability wise + val world = children.random(random) + + if (predicate(world)) + return@scanSystems KOptional(world) + } + + KOptional() + } + + if (result.isPresent) { + return result.value + } + } + + return null + } + + override suspend fun scanConstellationLines(region: AABBi, aggressive: Boolean): List> { val futures = ArrayList>>>() for (pos in chunkPositions(region)) { 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 75a4da9e..a3f3a650 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.server.world +import com.google.gson.JsonElement import com.google.gson.JsonObject import it.unimi.dsi.fastutil.ints.IntArraySet import it.unimi.dsi.fastutil.longs.Long2ObjectFunction @@ -13,6 +14,7 @@ import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.WarpAction +import ru.dbotthepony.kstarbound.defs.WarpAlias import ru.dbotthepony.kstarbound.defs.WorldID import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult @@ -24,6 +26,7 @@ import ru.dbotthepony.kstarbound.json.jsonArrayOf 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.UpdateWorldPropertiesPacket import ru.dbotthepony.kstarbound.server.StarboundServer import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.util.AssetPathStack @@ -295,6 +298,11 @@ class ServerWorld private constructor( } } + override fun setProperty0(key: String, value: JsonElement) { + super.setProperty0(key, value) + broadcast(UpdateWorldPropertiesPacket(JsonObject().apply { add(key, value) })) + } + override fun chunkFactory(pos: ChunkPos): ServerChunk { return ServerChunk(this, pos) } @@ -521,6 +529,11 @@ class ServerWorld private constructor( world.respawnInWorld = meta.respawnInWorld world.adjustPlayerSpawn = meta.adjustPlayerStart world.centralStructure = meta.centralStructure + + for ((k, v) in meta.worldProperties.entrySet()) { + world.setProperty(k, v) + } + world.protectedDungeonIDs.addAll(meta.protectedDungeonIds) world } 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 e0223f3a..2bee1a21 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt @@ -49,7 +49,10 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p client.worldID = world.worldID } - var skyVersion = 0L + private var skyVersion = 0L + // this is required because of dumb shit regarding flash time + // if we network sky state on each tick then it will guarantee epilepsy attack + private var skyUpdateWaitTicks = 0 private val isRemoved = AtomicBoolean() private var isActuallyRemoved = false @@ -99,7 +102,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p dungeonGravity = mapOf(), dungeonBreathable = mapOf(), protectedDungeonIDs = world.protectedDungeonIDs, - worldProperties = world.properties.deepCopy(), + worldProperties = world.copyProperties(), connectionID = client.connectionID, localInterpolationMode = false, )) @@ -143,9 +146,12 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p } run { - val (data, version) = world.sky.networkedGroup.write(skyVersion, isLegacy = client.isLegacy) - skyVersion = version - send(EnvironmentUpdatePacket(data, ByteArrayList())) + if (skyUpdateWaitTicks++ >= 4) { + val (data, version) = world.sky.networkedGroup.write(skyVersion, isLegacy = client.isLegacy) + skyVersion = version + send(EnvironmentUpdatePacket(data, ByteArrayList())) + skyUpdateWaitTicks = 0 + } } run { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseChunk.kt index 86793609..7fe370d0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseChunk.kt @@ -10,23 +10,26 @@ import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter import it.unimi.dsi.fastutil.ints.Int2ObjectMap import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.getArray -import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector3i import ru.dbotthepony.kstarbound.defs.world.CelestialParameters -import ru.dbotthepony.kstarbound.defs.world.CelestialPlanet +import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.pairAdapter import ru.dbotthepony.kstarbound.json.pairListAdapter +import ru.dbotthepony.kstarbound.network.packets.clientbound.CelestialResponsePacket import ru.dbotthepony.kstarbound.world.UniversePos import java.util.HashMap +import java.util.stream.Collectors class UniverseChunk(var chunkPos: Vector2i = Vector2i.ZERO) { - data class System(val parameters: CelestialParameters, val planets: Int2ObjectMap) + data class System(val parameters: CelestialParameters, val planets: Int2ObjectMap) + + @JsonFactory + data class Planet(val parameters: CelestialParameters, val satellites: Int2ObjectMap) val systems = HashMap() val constellations = ObjectOpenHashSet>() @@ -44,6 +47,15 @@ class UniverseChunk(var chunkPos: Vector2i = Vector2i.ZERO) { throw RuntimeException("unreachable code") } + fun toNetwork(): CelestialResponsePacket.ChunkData { + return CelestialResponsePacket.ChunkData( + chunkPos, + ArrayList(constellations), + systems.entries.associate { it.key to it.value.parameters }, + systems.entries.associate { it.key to it.value.planets.int2ObjectEntrySet().associate { it.intKey to CelestialResponsePacket.PlanetData(it.value.parameters, HashMap(it.value.satellites)) } }, + ) + } + class Adapter(gson: Gson) : TypeAdapter() { private val vectors = gson.getAdapter(Vector2i::class.java) private val vectors3 = gson.getAdapter(Vector3i::class.java) @@ -164,7 +176,7 @@ class UniverseChunk(var chunkPos: Vector2i = Vector2i.ZERO) { val orbit = planetPair[0].asInt val params = planetPair[1] as JsonObject - val planet = CelestialPlanet(this.params.fromJsonTree(params["parameters"]), Int2ObjectOpenHashMap()) + val planet = Planet(this.params.fromJsonTree(params["parameters"]), Int2ObjectOpenHashMap()) val satellitesPairs = params["satellites"] as JsonArray for (satellitePair in satellitesPairs) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt index 20408ae0..641599db 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt @@ -18,8 +18,6 @@ import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector3i import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.world.CelestialParameters -import ru.dbotthepony.kstarbound.defs.world.CelestialPlanet -import ru.dbotthepony.kstarbound.defs.JsonDriven import ru.dbotthepony.kstarbound.io.BTreeDB5 import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.json.readJsonElement @@ -204,7 +202,7 @@ class NativeUniverseSource(private val db: BTreeDB6?, private val universe: Serv systemParams.parameters["constellationCapable"] = system.constellationCapable } - val planets = Int2ObjectArrayMap() + val planets = Int2ObjectArrayMap() for (planetOrbitIndex in 1 .. universe.baseInformation.planetOrbitalLevels) { // this looks dumb at first, but then it makes sense @@ -266,7 +264,7 @@ class NativeUniverseSource(private val db: BTreeDB6?, private val universe: Serv } } - planets[planetOrbitIndex] = CelestialPlanet(planetParams, satellites) + planets[planetOrbitIndex] = UniverseChunk.Planet(planetParams, satellites) } return UniverseChunk.System(systemParams, planets) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt index ede830f3..780de28d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt @@ -21,8 +21,8 @@ val JsonElement.coalesceNull: JsonElement? fun UUID.toStarboundString(): String { val builder = StringBuilder(32) - val a = mostSignificantBits.toString(16) - val b = mostSignificantBits.toString(16) + val a = java.lang.Long.toUnsignedString(mostSignificantBits, 16) + val b = java.lang.Long.toUnsignedString(leastSignificantBits, 16) for (i in a.length until 16) builder.append("0") @@ -37,6 +37,17 @@ fun UUID.toStarboundString(): String { return builder.toString() } +fun uuidFromStarboundString(value: String): UUID { + if (value.length != 32) { + throw IllegalArgumentException("Not a UUID string: $value") + } + + val a = value.substring(0, 16) + val b = value.substring(16) + + return UUID(a.toLong(16), b.toLong(16)) +} + fun paddedNumber(number: Int, digits: Int): String { val str = number.toString() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/MWCRandom.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/MWCRandom.kt index 7ee7015a..da267e5d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/MWCRandom.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/MWCRandom.kt @@ -3,15 +3,15 @@ package ru.dbotthepony.kstarbound.util.random import ru.dbotthepony.kstarbound.getValue import java.util.random.RandomGenerator -// multiply with carry random number generator, as used by original Starbound -// Game's code SHOULD NOT be tied to this generator, random() global function should be used -// What interesting though, is the size of cycle - 256 (8092 bits). Others use much bigger number - 4096 -class MWCRandom(seed: Long = System.nanoTime(), cycle: Int = 256, windupIterations: Int = 32) : RandomGenerator { - private val data = IntArray(cycle) - private var carry = 0 +// MWC Random (multiply with carry), exact replica from original code +// (for purpose of giving exactly the same results for same seed provided) +@OptIn(ExperimentalUnsignedTypes::class) +class MWCRandom(seed: ULong = System.nanoTime().toULong(), cycle: Int = 256, windupIterations: Int = 32) : RandomGenerator { + private val data = UIntArray(cycle) + private var carry = 0u private var dataIndex = 0 - var seed: Long = seed + var seed: ULong = seed private set init { @@ -22,49 +22,72 @@ class MWCRandom(seed: Long = System.nanoTime(), cycle: Int = 256, windupIteratio /** * re-initialize this MWC generator */ - fun init(seed: Long, windupIterations: Int = 0) { + fun init(seed: ULong, windupIterations: Int = 0) { this.seed = seed - carry = (seed % MAGIC).toInt() + carry = (seed % MAGIC).toUInt() - data[0] = seed.toInt() - data[1] = seed.ushr(32).toInt() + data[0] = seed.toUInt() + data[1] = seed.shr(32).toUInt() for (i in 2 until data.size) { - data[i] = 69069 * data[i - 2] + 362437 + data[i] = 69069u * data[i - 2] + 362437u } dataIndex = data.size - 1 // initial windup for (i in 0 until windupIterations) { - nextLong() + nextInt() } } - fun addEntropy(seed: Long = System.nanoTime()) { + fun addEntropy(seed: ULong = System.nanoTime().toULong()) { // Same algo as init, but bitwise xor with existing data - carry = ((carry.toLong().xor(seed)) % MAGIC).toInt() + carry = ((carry.toULong().xor(seed)) % MAGIC).toUInt() - data[0] = data[0].xor(seed.toInt()) - data[1] = data[1].xor(seed.ushr(32).xor(seed).toInt()) + data[0] = data[0].xor(seed.toUInt()) + data[1] = data[1].xor(seed.shr(32).xor(seed).toUInt()) for (i in 2 until data.size) { - data[i] = data[i].xor(69069 * data[i - 2] + 362437) + data[i] = data[i].xor(69069u * data[i - 2] + 362437u) } } + override fun nextInt(): Int { + dataIndex = (dataIndex + 1) % data.size + val t = MAGIC.toULong() * data[dataIndex] + carry + //val t = MAGIC.toLong() * data[dataIndex].toLong() + carry.toLong() + + carry = t.shr(32).toUInt() + data[dataIndex] = t.toUInt() + + return t.toInt() + } + override fun nextLong(): Long { - dataIndex = (dataIndex + 1) % data.size - val t = MAGIC.toLong() * data[dataIndex].toLong() + carry.toLong() + val a = nextInt().toLong() and 0xFFFFFFFFL + val b = nextInt().toLong() and 0xFFFFFFFFL + return a.shl(32) or b + } - carry = t.ushr(32).toInt() - data[dataIndex] = t.toInt() + override fun nextFloat(): Float { + return (nextInt() and 0x7fffffff) / 2.14748365E9f + } - return t + override fun nextDouble(): Double { + return (nextLong() and 0x7fffffffffffffffL) / 9.223372036854776E18 + } + + override fun nextDouble(origin: Double, bound: Double): Double { + return nextDouble() * (bound - origin) + origin + } + + override fun nextFloat(origin: Float, bound: Float): Float { + return nextFloat() * (bound - origin) + origin } companion object { - const val MAGIC = 809430660 + const val MAGIC = 809430660u val GLOBAL: MWCRandom by ThreadLocal.withInitial { MWCRandom() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt index c2391a29..82b221e0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt @@ -21,7 +21,7 @@ import kotlin.math.sqrt * Replacing generator returned here will affect all random generation code. */ fun random(seed: Long = System.nanoTime()): RandomGenerator { - return MWCRandom(seed) + return MWCRandom(seed.toULong()) } private fun toBytes(accept: ByteConsumer, value: Short) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/CoordinateMapper.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/CoordinateMapper.kt index 9b331332..acf6abc8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/CoordinateMapper.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/CoordinateMapper.kt @@ -65,6 +65,9 @@ abstract class CoordinateMapper { open fun isValidCellIndex(value: Int): Boolean = inBoundsCell(value) open fun isValidChunkIndex(value: Int): Boolean = inBoundsChunk(value) + abstract fun diff(a: Int, b: Int): Int + abstract fun diff(a: Double, b: Double): Double + class Wrapper(private val cells: Int) : CoordinateMapper() { override val chunks = divideUp(cells, CHUNK_SIZE) private val cellsD = cells.toDouble() @@ -88,6 +91,36 @@ abstract class CoordinateMapper { override fun isValidCellIndex(value: Int) = true override fun isValidChunkIndex(value: Int) = true + @Suppress("NAME_SHADOWING") + override fun diff(a: Int, b: Int): Int { + val a = cell(a) + val b = cell(b) + + var diff = a - b + + if (diff > cells / 2) + diff -= cells + else if (diff < -cells / 2) + diff += cells + + return diff + } + + @Suppress("NAME_SHADOWING") + override fun diff(a: Double, b: Double): Double { + val a = cell(a) + val b = cell(b) + + var diff = a - b + + if (diff > cells / 2) + diff -= cells + else if (diff < -cells / 2) + diff += cells + + return diff + } + override fun chunkFromCell(value: Int): Int { return chunk(value shr CHUNK_SIZE_BITS) } @@ -186,6 +219,14 @@ abstract class CoordinateMapper { private var cellsEdgeFloat = cellsF + override fun diff(a: Int, b: Int): Int { + return (a - b).coerceIn(0, cells) + } + + override fun diff(a: Double, b: Double): Double { + return (a - b).coerceIn(0.0, cellsD) + } + init { var power = -64f diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt index 373ef694..691c8b7c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt @@ -34,6 +34,8 @@ class Sky() { private val skyParametersNetState = networkedGroup.upstream.add(networkedJson(SkyParameters())) var skyType by networkedGroup.upstream.add(networkedEnumStupid(SkyType.ORBITAL)) + private set + var time by networkedGroup.upstream.add(networkedDouble()) private set var flyingType by networkedGroup.upstream.add(networkedEnum(FlyingType.NONE)) @@ -42,12 +44,12 @@ class Sky() { private set var startInWarp by networkedGroup.upstream.add(networkedBoolean()) private set - var warpPhase by networkedGroup.upstream.add(networkedData(WarpPhase.MAINTAIN, VarIntValueCodec.map({ WarpPhase.entries[this - 1] }, { ordinal - 1 }))) - private set var worldMoveOffset by networkedGroup.upstream.add(networkedVec2f()) private set var starMoveOffset by networkedGroup.upstream.add(networkedVec2f()) private set + var warpPhase by networkedGroup.upstream.add(networkedData(WarpPhase.MAINTAIN, VarIntValueCodec.map({ WarpPhase.entries[this - 1] }, { ordinal - 1 }))) + private set var flyingTimer by networkedGroup.upstream.add(networkedFloat()) private set @@ -61,6 +63,8 @@ class Sky() { var pathOffset = Vector2d.ZERO private set + var worldRotation: Double = 0.0 + private set var starRotation: Double = 0.0 private set var pathRotation: Double = 0.0 @@ -98,14 +102,19 @@ class Sky() { fun startFlying(enterHyperspace: Boolean, startInWarp: Boolean = false) { if (startInWarp) flyingType = FlyingType.WARP - else + else if (flyingType == FlyingType.NONE) flyingType = FlyingType.DISEMBARKING - flyingTimer = 0.0 + // flyingTimer = 0.0 this.enterHyperspace = enterHyperspace this.startInWarp = startInWarp } + fun stopFlyingAt(destination: SkyParameters) { + this.destination = destination + skyType = SkyType.ORBITAL + } + private var lastFlyingType = FlyingType.NONE private var lastWarpPhase = WarpPhase.MAINTAIN private var sentSFX = false @@ -141,11 +150,32 @@ class Sky() { when (warpPhase) { WarpPhase.SLOWING_DOWN -> { + skyType = SkyType.ORBITAL + //flashTimer = GlobalDefaults.sky.flashTimer + sentSFX = false + val origin = if (skyType == SkyType.SPACE) GlobalDefaults.sky.spaceArrivalOrigin else GlobalDefaults.sky.arrivalOrigin + val path = if (skyType == SkyType.SPACE) GlobalDefaults.sky.spaceArrivalPath else GlobalDefaults.sky.arrivalPath + + pathOffset = origin.offset + pathRotation = origin.rotationRad + + var exitDistance = GlobalDefaults.sky.flyMaxVelocity / 2.0 * slowdownTime + worldMoveOffset = Vector2d(x = exitDistance) + worldOffset = worldMoveOffset + + exitDistance *= GlobalDefaults.sky.starVelocityFactor + starMoveOffset = Vector2d(x = exitDistance) + starOffset = starMoveOffset + + worldRotation = 0.0 + starRotation = 0.0 + flyingTimer = 0.0 } WarpPhase.MAINTAIN -> { - flashTimer = GlobalDefaults.sky.flashTimer + //flashTimer = GlobalDefaults.sky.flashTimer + skyType = SkyType.WARP sentSFX = false } @@ -157,6 +187,7 @@ class Sky() { } lastFlyingType = flyingType + lastWarpPhase = warpPhase } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorld.kt new file mode 100644 index 00000000..ba283bf4 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorld.kt @@ -0,0 +1,292 @@ +package ru.dbotthepony.kstarbound.world + +import com.google.gson.JsonObject +import kotlinx.coroutines.CoroutineScope +import ru.dbotthepony.kommons.io.koptional +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeUUID +import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kommons.util.getValue +import ru.dbotthepony.kommons.util.setValue +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kommons.vector.Vector3i +import ru.dbotthepony.kstarbound.GlobalDefaults +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters +import ru.dbotthepony.kstarbound.defs.world.SystemWorldObjectConfig +import ru.dbotthepony.kstarbound.io.readDouble +import ru.dbotthepony.kstarbound.io.readVector2d +import ru.dbotthepony.kstarbound.io.writeDouble +import ru.dbotthepony.kstarbound.io.writeStruct2d +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.writeJsonObject +import ru.dbotthepony.kstarbound.math.Interpolator +import ru.dbotthepony.kstarbound.network.syncher.MasterElement +import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup +import ru.dbotthepony.kstarbound.network.syncher.legacyCodec +import ru.dbotthepony.kstarbound.network.syncher.nativeCodec +import ru.dbotthepony.kstarbound.network.syncher.networkedData +import ru.dbotthepony.kstarbound.network.syncher.networkedFloat +import ru.dbotthepony.kstarbound.util.Clock +import ru.dbotthepony.kstarbound.util.random.MWCRandom +import ru.dbotthepony.kstarbound.util.random.nextRange +import ru.dbotthepony.kstarbound.util.random.random +import ru.dbotthepony.kstarbound.util.random.staticRandom64 +import ru.dbotthepony.kstarbound.util.toStarboundString +import ru.dbotthepony.kstarbound.util.uuidFromStarboundString +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.UUID +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin +import kotlin.math.sqrt + +abstract class SystemWorld(val location: Vector3i, val clock: Clock, val universe: Universe) { + val random = random() + abstract val entities: Map + abstract val ships: Map + val systemLocation: UniversePos = UniversePos(location) + + suspend fun planetOrbitDistance(coordinate: UniversePos): Double { + if (coordinate.isSystem) + return 0.0 + + val random = MWCRandom(compatCoordinateSeed(coordinate, "PlanetOrbitDistance").toULong(), cycle = 256, windupIterations = 32) + var distance = planetSize(coordinate.parent()) / 2.0 + + for (i in 0 until coordinate.orbitNumber) { + if (i > 0) { + distance += clusterSize(coordinate.parent().child(i)) + } + + if (coordinate.isPlanet) + distance += random.nextFloat(GlobalDefaults.systemWorld.planetaryOrbitPadding.x.toFloat(), GlobalDefaults.systemWorld.planetaryOrbitPadding.y.toFloat()) + else if (coordinate.isSatellite) + distance += random.nextFloat(GlobalDefaults.systemWorld.satelliteOrbitPadding.x.toFloat(), GlobalDefaults.systemWorld.satelliteOrbitPadding.y.toFloat()) + } + + distance += clusterSize(coordinate) / 2.0 + return distance + } + + suspend fun clusterSize(coordinate: UniversePos): Double { + if (coordinate.isPlanet && universe.children(coordinate.parent()).any { it.orbitNumber == coordinate.orbitNumber }) { + val child = universe.children(coordinate).sorted() + + if (child.isNotEmpty()) { + val outer = coordinate.child(child.last().orbitNumber) + return planetOrbitDistance(outer) * 2.0 + planetSize(outer) + } else { + return planetSize(coordinate) + } + } else { + return planetSize(coordinate) + } + } + + suspend fun planetSize(coordinate: UniversePos): Double { + if (coordinate.isSystem) + return GlobalDefaults.systemWorld.starSize + + if (!universe.children(coordinate.parent()).any { it.orbitNumber == coordinate.orbitNumber }) + return GlobalDefaults.systemWorld.emptyOrbitSize + + val parameters = universe.parameters(coordinate) + + if (parameters != null) { + val visitable = parameters.visitableParameters + + if (visitable != null) { + var size = 0.0 + + if (visitable is FloatingDungeonWorldParameters) { + val getSize = GlobalDefaults.systemWorld.floatingDungeonWorldSizes[visitable.typeName] + + if (getSize != null) { + return getSize + } + } + + for ((planetSize, orbitSize) in GlobalDefaults.systemWorld.planetSizes) { + if (visitable.worldSize.x >= planetSize) + size = orbitSize + else + break + } + + return size + } + } + + return GlobalDefaults.systemWorld.unvisitablePlanetSize + } + + fun orbitInterval(distance: Double, isSatellite: Boolean): Double { + val gravityConstant = if (isSatellite) GlobalDefaults.systemWorld.planetGravitationalConstant else GlobalDefaults.systemWorld.starGravitationalConstant + return distance * 2.0 * PI / sqrt(gravityConstant / distance) + } + + fun compatCoordinateSeed(coordinate: UniversePos, seedMix: String): Long { + // original code is utterly broken here + + // consider the following: + // auto satellite = coordinate.isSatelliteBody() ? coordinate.orbitNumber() : 0; + // auto planet = coordinate.isSatelliteBody() ? coordinate.parent().orbitNumber() : coordinate.isPlanetaryBody() && coordinate.orbitNumber() || 0; + + // first obvious problem: coordinate.isPlanetaryBody() && coordinate.orbitNumber() || 0 + // this "coalesces" planet orbit into either 0 or 1 + // then, we have coordinate.parent().orbitNumber(), which is correct, but only if we are orbiting a satellite + + // TODO: Use correct logic when there are no legacy clients in this system + // Correct logic properly randomizes starting planet orbits, and they feel much more natural + + return staticRandom64(coordinate.location.x, coordinate.location.y, coordinate.location.z, if (coordinate.isPlanet) 1 else coordinate.planetOrbit, coordinate.satelliteOrbit, seedMix) + } + + suspend fun planetPosition(coordinate: UniversePos): Vector2d { + if (coordinate.isSystem) + return Vector2d.ZERO + + // this thing must produce EXACT result between legacy client and new server + val random = MWCRandom(compatCoordinateSeed(coordinate, "PlanetSystemPosition").toULong(), cycle = 256, windupIterations = 32) + val parentPosition = planetPosition(coordinate.parent()) + val distance = planetOrbitDistance(coordinate) + val interval = orbitInterval(distance, coordinate.isSatellite) + + val start = random.nextFloat().toDouble() + val offset = (clock.seconds % interval) / interval + val direction = if (random.nextFloat() > 0.5f) 1 else -1 + val angle = (start + direction * offset) * PI * 2.0 + return parentPosition + Vector2d(cos(angle) * distance, sin(angle) * distance) + } + + suspend fun orbitPosition(orbit: Orbit): Vector2d { + val targetPosition = if (orbit.target.isPlanet || orbit.target.isSatellite) planetPosition(orbit.target) else Vector2d.ZERO + val distance = orbit.enterPosition.length + val interval = orbitInterval(distance, false) + val timeOffset = ((clock.seconds - orbit.enterTime) % interval) / interval + val angle = (orbit.enterPosition * -1).toAngle() + orbit.direction * timeOffset * PI * 2.0 + return targetPosition + Vector2d(cos(angle) * distance, sin(angle) * distance) + } + + fun randomArrivalPosition(): Vector2d { + val range = random.nextRange(GlobalDefaults.systemWorld.arrivalRange) + val angle = random.nextDouble(0.0, PI * 2.0) + return Vector2d(cos(angle), sin(angle)) * range + } + + data class Orbit(val target: UniversePos, val direction: Int, val enterTime: Double, val enterPosition: Vector2d) { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + UniversePos(stream, isLegacy), + if (isLegacy) stream.readInt() else stream.readByte().toInt(), + stream.readDouble(), + stream.readVector2d(isLegacy) + ) + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + target.write(stream, isLegacy) + if (isLegacy) stream.writeInt(direction) else stream.writeByte(direction) + stream.writeDouble(enterTime) + stream.writeStruct2d(enterPosition, isLegacy) + } + + companion object { + val CODEC = nativeCodec(::Orbit, Orbit::write).koptional() + val LEGACY_CODEC = legacyCodec(::Orbit, Orbit::write).koptional() + } + } + + @JsonFactory + data class EntityJsonData( + val name: String, + val uuid: String, + val parameters: JsonObject = JsonObject(), + val spawnTime: Double, + val position: Vector2d, + val orbit: Orbit? = null, + ) { + val actualUUID = uuidFromStarboundString(uuid) + } + + abstract inner class Ship(val uuid: UUID, location: SystemWorldLocation) { + var speed = GlobalDefaults.systemWorld.clientShip.speed + var departTimer = 0.0 + + val networkGroup = MasterElement(NetworkedGroup()) + + // systemLocation should not be interpolated + // if it's stale it can point to a removed system object + var location by networkedData(location, SystemWorldLocation.CODEC, SystemWorldLocation.LEGACY_CODEC).also { networkGroup.upstream.add(it, false) } + var destination by networkedData(location, SystemWorldLocation.CODEC, SystemWorldLocation.LEGACY_CODEC).also { networkGroup.upstream.add(it, false) } + + var xPosition by networkedFloat().also { networkGroup.upstream.add(it); it.interpolator = Interpolator.Linear } + var yPosition by networkedFloat().also { networkGroup.upstream.add(it); it.interpolator = Interpolator.Linear } + + var orbit: Orbit? = null + + var position: Vector2d + get() = Vector2d(xPosition, yPosition) + set(value) { + xPosition = value.x + yPosition = value.y + } + + init { + networkGroup.upstream.enableInterpolation() + } + } + + abstract inner class Entity(val data: SystemWorldObjectConfig.Data, val uuid: UUID, position: Vector2d, val spawnTime: Double = 0.0, val parameters: JsonObject = JsonObject()) { + constructor(data: EntityJsonData) : this( + GlobalDefaults.systemObjects[data.name]?.create(data.actualUUID, data.name) ?: throw NullPointerException("Tried to create ${data.name} system world object, but there is no such object in /system_objects.config!"), + data.actualUUID, + data.position, + data.spawnTime, + data.parameters + ) { + if (data.orbit != null) { + orbit = KOptional(data.orbit) + } + } + + constructor(data: JsonObject) : this(Starbound.gson.fromJson(data, EntityJsonData::class.java)) + + val networkGroup = MasterElement(NetworkedGroup()) + + var xPosition by networkedFloat().also { networkGroup.upstream.add(it); it.interpolator = Interpolator.Linear } + var yPosition by networkedFloat().also { networkGroup.upstream.add(it); it.interpolator = Interpolator.Linear } + var orbit by networkedData(KOptional(), Orbit.CODEC, Orbit.LEGACY_CODEC).also { networkGroup.upstream.add(it) } + + var approach: UniversePos? = null + protected set + + var position: Vector2d + get() = Vector2d(xPosition, yPosition) + set(value) { + xPosition = value.x + yPosition = value.y + } + + init { + this.position = position + } + + fun enterOrbit(target: UniversePos, position: Vector2d, time: Double) { + val direction = if (random.nextBoolean()) -1 else 1 + orbit = KOptional(Orbit(target, direction, time, position - this.position)) + approach = null + } + + fun toJson(): JsonObject { + return Starbound.gson.toJsonTree(EntityJsonData( + name = data.name, + uuid = uuid.toStarboundString(), + parameters = parameters, + spawnTime = spawnTime, + position = position, + orbit = orbit.orNull(), + )) as JsonObject + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorldLocation.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorldLocation.kt new file mode 100644 index 00000000..da9f387a --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorldLocation.kt @@ -0,0 +1,180 @@ +package ru.dbotthepony.kstarbound.world + +import ru.dbotthepony.kommons.io.readUUID +import ru.dbotthepony.kommons.io.writeUUID +import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.GlobalDefaults +import ru.dbotthepony.kstarbound.defs.SpawnTarget +import ru.dbotthepony.kstarbound.defs.WarpAction +import ru.dbotthepony.kstarbound.defs.WarpMode +import ru.dbotthepony.kstarbound.defs.WorldID +import ru.dbotthepony.kstarbound.defs.world.AsteroidsWorldParameters +import ru.dbotthepony.kstarbound.defs.world.SkyParameters +import ru.dbotthepony.kstarbound.defs.world.SkyType +import ru.dbotthepony.kstarbound.io.readVector2d +import ru.dbotthepony.kstarbound.io.writeStruct2d +import ru.dbotthepony.kstarbound.network.syncher.legacyCodec +import ru.dbotthepony.kstarbound.network.syncher.nativeCodec +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.UUID +import kotlin.math.PI +import kotlin.math.absoluteValue + +sealed class SystemWorldLocation { + abstract fun write(stream: DataOutputStream, isLegacy: Boolean) + abstract suspend fun resolve(system: SystemWorld): Vector2d? + abstract suspend fun orbitalAction(system: SystemWorld): KOptional> + abstract suspend fun skyParameters(system: SystemWorld): SkyParameters + + protected suspend fun appendParameters(parameters: SkyParameters, system: SystemWorld) { + + } + + object Transit : SystemWorldLocation() { + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(0) + } + + override suspend fun resolve(system: SystemWorld): Vector2d? { + return null + } + + override suspend fun orbitalAction(system: SystemWorld): KOptional> { + return KOptional() + } + + override suspend fun skyParameters(system: SystemWorld): SkyParameters { + return GlobalDefaults.systemWorld.emptySkyParameters + } + } + + // orbiting around specific planet + data class Celestial(val position: UniversePos) : SystemWorldLocation() { + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(1) + position.write(stream, isLegacy) + } + + override suspend fun resolve(system: SystemWorld): Vector2d { + return system.planetPosition(position) + } + + override suspend fun orbitalAction(system: SystemWorld): KOptional> { + return KOptional(WarpAction.World(WorldID.Celestial(position)) to WarpMode.BEAM_OR_DEPLOY) + } + + override suspend fun skyParameters(system: SystemWorld): SkyParameters { + return SkyParameters.create(position, system.universe) + } + } + + // orbiting around celestial body + data class Orbit(val position: SystemWorld.Orbit) : SystemWorldLocation() { + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(2) + position.write(stream, isLegacy) + } + + override suspend fun resolve(system: SystemWorld): Vector2d { + return system.orbitPosition(position) + } + + override suspend fun orbitalAction(system: SystemWorld): KOptional> { + return KOptional() + } + + override suspend fun skyParameters(system: SystemWorld): SkyParameters { + if (position.target.isPlanet) { + + } + + return GlobalDefaults.systemWorld.emptySkyParameters + } + } + + data class Entity(val uuid: UUID) : SystemWorldLocation() { + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(3) + stream.writeUUID(uuid) + } + + override suspend fun resolve(system: SystemWorld): Vector2d? { + return system.entities[uuid]?.position + } + + override suspend fun orbitalAction(system: SystemWorld): KOptional> { + val action = system.entities[uuid]?.data?.warpAction ?: return KOptional() + return KOptional(action to WarpMode.DEPLOY_ONLY) + } + + override suspend fun skyParameters(system: SystemWorld): SkyParameters { + val get = system.entities[uuid] ?: return GlobalDefaults.systemWorld.emptySkyParameters + return get.data.skyParameters + } + } + + data class Position(val position: Vector2d) : SystemWorldLocation() { + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(4) + stream.writeStruct2d(position, isLegacy) + } + + override suspend fun resolve(system: SystemWorld): Vector2d { + return position + } + + override suspend fun orbitalAction(system: SystemWorld): KOptional> { + // player can beam to asteroid fields simply by being in proximity to them + for (child in system.universe.children(system.systemLocation)) { + if ((system.planetPosition(child).length - position.length).absoluteValue > GlobalDefaults.systemWorld.asteroidBeamDistance) { + continue + } + + val params = system.universe.parameters(child) ?: continue + val visitable = params.visitableParameters ?: continue + + if (visitable is AsteroidsWorldParameters) { + val targetX = position.toAngle() / (2.0 * PI) * visitable.worldSize.x + return KOptional(WarpAction.World(WorldID.Celestial(child), SpawnTarget.X(targetX)) to WarpMode.DEPLOY_ONLY) + } + } + + return KOptional() + } + + override suspend fun skyParameters(system: SystemWorld): SkyParameters { + for (child in system.universe.children(system.systemLocation)) { + if ((system.planetPosition(child).length - position.length).absoluteValue > GlobalDefaults.systemWorld.asteroidBeamDistance) { + continue + } + + val params = system.universe.parameters(child) ?: continue + val visitable = params.visitableParameters ?: continue + + if (visitable is AsteroidsWorldParameters) { + return SkyParameters.create(child, system.universe) + } + } + + return GlobalDefaults.systemWorld.emptySkyParameters + } + } + + companion object { + val CODEC = nativeCodec(::read, SystemWorldLocation::write) + val LEGACY_CODEC = legacyCodec(::read, SystemWorldLocation::write) + + fun read(stream: DataInputStream, isLegacy: Boolean): SystemWorldLocation { + return when (val type = stream.readUnsignedByte()) { + 0 -> Transit + 1 -> Celestial(UniversePos(stream, isLegacy)) + 2 -> Orbit(SystemWorld.Orbit(stream, isLegacy)) + 3 -> Entity(stream.readUUID()) + 4 -> Position(stream.readVector2d(isLegacy)) + else -> throw IllegalStateException("Unknown SystemWorldLocation type $type!") + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Universe.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Universe.kt index 88bc753b..7d1c6bad 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Universe.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Universe.kt @@ -23,9 +23,16 @@ abstract class Universe { * are guaranteed to have unique x/y coordinates, and are meant to be viewed * from the top in 2d. The z-coordinate is there simpy as a validation * parameter. + * + * [callback] determines when to stop scanning (returning non empty KOptional will stop scanning) */ - abstract suspend fun scanSystems(region: AABBi, includedTypes: Set? = null): List - abstract suspend fun scanConstellationLines(region: AABBi): List> + abstract suspend fun scanSystems(region: AABBi, callback: suspend (UniversePos) -> KOptional): KOptional + abstract suspend fun scanConstellationLines(region: AABBi, aggressive: Boolean = false): List> + + /** + * Similar to [scanSystems], but scans for ALL systems in given range, on multiple threads + */ + abstract suspend fun findSystems(region: AABBi, includedTypes: Set? = null): List abstract suspend fun hasChildren(pos: UniversePos): Boolean abstract suspend fun children(pos: UniversePos): List diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt index 9c06eccb..e5cf470e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt @@ -34,7 +34,7 @@ import java.io.DataOutputStream * exists in a specific universe or not can be expressed. */ @JsonAdapter(UniversePos.Adapter::class) -data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit: Int = 0, val satelliteOrbit: Int = 0) { +data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit: Int = 0, val satelliteOrbit: Int = 0) : Comparable { constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readVector3i(), if (isLegacy) stream.readInt() else stream.readVarInt(), if (isLegacy) stream.readInt() else stream.readVarInt()) init { @@ -43,7 +43,21 @@ data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit: } override fun toString(): String { - return "${location.x},${location.y}${location.z}:$planetOrbit:$satelliteOrbit" + if (planetOrbit == 0 && satelliteOrbit == 0) + return "${location.x}:${location.y}:${location.z}" + else if (satelliteOrbit == 0) + return "${location.x}:${location.y}:${location.z}:$planetOrbit" + else + return "${location.x}:${location.y}:${location.z}:$planetOrbit:$satelliteOrbit" + } + + override fun compareTo(other: UniversePos): Int { + var cmp = location.x.compareTo(other.location.x) + if (cmp != 0) cmp = location.y.compareTo(other.location.y) + if (cmp != 0) cmp = location.z.compareTo(other.location.z) + if (cmp != 0) cmp = planetOrbit.compareTo(other.planetOrbit) + if (cmp != 0) cmp = satelliteOrbit.compareTo(other.satelliteOrbit) + return cmp } val isSystem: Boolean @@ -53,7 +67,7 @@ data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit: get() = planetOrbit != 0 && satelliteOrbit == 0 val isSatellite: Boolean - get() = planetOrbit != 0 && satelliteOrbit == 0 + get() = planetOrbit != 0 && satelliteOrbit != 0 val orbitNumber: Int get() = if (isSatellite) satelliteOrbit else if (isPlanet) planetOrbit else 0 @@ -85,6 +99,15 @@ data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit: return this } + fun child(orbit: Int): UniversePos { + if (isSatellite) + throw IllegalStateException("Satellite can't have children!") + else if (isPlanet) + return UniversePos(location, planetOrbit, orbit) + else + return UniversePos(location, orbit) + } + fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeStruct3i(location) @@ -132,21 +155,7 @@ data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit: return ZERO else { try { - val split = read.split(splitter) - val x = split[0].toInt() - val y = split[1].toInt() - val z = split[2].toInt() - - val planet = if (split.size > 3) split[3].toInt() else 0 - val orbit = if (split.size > 4) split[4].toInt() else 0 - - if (planet <= 0) // TODO: ??? Determine, if this is a bug in original code - throw IndexOutOfBoundsException("Planetary orbit: $planet") - - if (orbit < 0) - throw IndexOutOfBoundsException("Satellite orbit: $orbit") - - return UniversePos(Vector3i(x, y, z), planet, orbit) + return parse(read) } catch (err: Throwable) { throw JsonSyntaxException("Error parsing UniversePos from string", err) } @@ -163,5 +172,26 @@ data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit: private val splitter = Regex("[ _:]") val ZERO = UniversePos() + + fun parse(value: String): UniversePos { + if (value.isBlank()) + return ZERO + + val split = value.split(splitter) + val x = split[0].toInt() + val y = split[1].toInt() + val z = split[2].toInt() + + val planet = if (split.size > 3) split[3].toInt() else 0 + val orbit = if (split.size > 4) split[4].toInt() else 0 + + if (planet <= 0) // TODO: ??? Determine, if this is a bug in original code + throw IndexOutOfBoundsException("Non-positive planetary orbit: $planet (in $value)") + + if (orbit < 0) + throw IndexOutOfBoundsException("Negative satellite orbit: $orbit (in $value)") + + return UniversePos(Vector3i(x, y, z), planet, orbit) + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 19abab30..babc94f8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.world +import com.google.gson.JsonElement import com.google.gson.JsonObject import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.ints.IntArraySet @@ -8,6 +9,7 @@ import it.unimi.dsi.fastutil.objects.ObjectArraySet import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.collect.filterNotNull +import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.util.IStruct2d import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.AABB @@ -18,6 +20,7 @@ import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.world.WorldStructure import ru.dbotthepony.kstarbound.defs.world.WorldTemplate +import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket @@ -239,7 +242,26 @@ abstract class World, ChunkType : Chunk) : TileEntit } } + private val interactAction by LazyData { + lookupProperty(JsonPath("interactAction")) { JsonNull.INSTANCE } + } + + private val interactData by LazyData { + lookupProperty(JsonPath("interactData")) { JsonNull.INSTANCE } + } + + override fun interact(request: InteractRequest): InteractAction { + val diff = world.geometry.diff(request.sourcePos, position) + // val result = + + if (!interactAction.isJsonNull) { + return InteractAction(interactAction.asString, entityID, interactData) + } + + return super.interact(request) + } + override fun invalidate() { super.invalidate() drawablesCache.invalidate()