SystemWorld, fixed MWCRandom, event loops, universe io

This commit is contained in:
DBotThePony 2024-04-04 00:31:57 +07:00
parent 3b2e1d06c3
commit db09de857b
Signed by: DBot
GPG Key ID: DCC23B5715498507
58 changed files with 2920 additions and 143 deletions

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.JsonElement
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
@ -48,6 +49,7 @@ operator fun <K, V> ImmutableMap.Builder<K, V>.set(key: K, value: V): ImmutableM
fun String.sintern(): String = Starbound.STRINGS.intern(this) fun String.sintern(): String = Starbound.STRINGS.intern(this)
inline fun <reified T> Gson.fromJson(reader: JsonReader): T? = fromJson<T>(reader, T::class.java) inline fun <reified T> Gson.fromJson(reader: JsonReader): T? = fromJson<T>(reader, T::class.java)
inline fun <reified T> Gson.fromJson(reader: JsonElement): T? = fromJson<T>(reader, T::class.java)
/** /**
* guarantees even distribution of tasks while also preserving encountered order of elements * guarantees even distribution of tasks while also preserving encountered order of elements

View File

@ -14,6 +14,8 @@ import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig
import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig
import ru.dbotthepony.kstarbound.defs.world.DungeonWorldsConfig import ru.dbotthepony.kstarbound.defs.world.DungeonWorldsConfig
import ru.dbotthepony.kstarbound.defs.world.SkyGlobalConfig 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.defs.world.WorldTemplateConfig
import ru.dbotthepony.kstarbound.json.mapAdapter import ru.dbotthepony.kstarbound.json.mapAdapter
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
@ -70,6 +72,12 @@ object GlobalDefaults {
var currencies by Delegates.notNull<ImmutableMap<String, CurrencyDefinition>>() var currencies by Delegates.notNull<ImmutableMap<String, CurrencyDefinition>>()
private set private set
var systemObjects by Delegates.notNull<ImmutableMap<String, SystemWorldObjectConfig>>()
private set
var systemWorld by Delegates.notNull<SystemWorldConfig>()
private set
private object EmptyTask : ForkJoinTask<Unit>() { private object EmptyTask : ForkJoinTask<Unit>() {
private fun readResolve(): Any = EmptyTask private fun readResolve(): Any = EmptyTask
override fun getRawResult() { override fun getRawResult() {
@ -119,6 +127,7 @@ object GlobalDefaults {
tasks.add(load("/sky.config", ::sky)) tasks.add(load("/sky.config", ::sky))
tasks.add(load("/universe_server.config", ::universeServer)) tasks.add(load("/universe_server.config", ::universeServer))
tasks.add(load("/player.config", ::player)) tasks.add(load("/player.config", ::player))
tasks.add(load("/systemworld.config", ::systemWorld))
tasks.add(load("/plants/grassDamage.config", ::grassDamage)) tasks.add(load("/plants/grassDamage.config", ::grassDamage))
tasks.add(load("/plants/treeDamage.config", ::treeDamage)) 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("/dungeon_worlds.config", ::dungeonWorlds, Starbound.gson.mapAdapter()))
tasks.add(load("/currencies.config", ::currencies, Starbound.gson.mapAdapter())) tasks.add(load("/currencies.config", ::currencies, Starbound.gson.mapAdapter()))
tasks.add(load("/system_objects.config", ::systemObjects, Starbound.gson.mapAdapter()))
return tasks return tasks
} }

View File

@ -13,6 +13,7 @@ import ru.dbotthepony.kstarbound.server.IntegratedStarboundServer
import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage
import ru.dbotthepony.kstarbound.server.world.ServerUniverse import ru.dbotthepony.kstarbound.server.world.ServerUniverse
import ru.dbotthepony.kstarbound.server.world.ServerWorld 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.util.random.staticRandomDouble
import ru.dbotthepony.kstarbound.world.WorldGeometry import ru.dbotthepony.kstarbound.world.WorldGeometry
import java.io.BufferedInputStream import java.io.BufferedInputStream
@ -34,7 +35,7 @@ fun main() {
val t = System.nanoTime() val t = System.nanoTime()
val result = Starbound.COROUTINES.future { 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 (system in systems) {
for (children in data.children(system)) { for (children in data.children(system)) {

View File

@ -408,6 +408,10 @@ object Starbound : ISBFileLocator {
private set private set
var loadingProgress = 0.0 var loadingProgress = 0.0
private set private set
var toLoad = 0
private set
var loaded = 0
private set
@Volatile @Volatile
var terminateLoading = false var terminateLoading = false
@ -571,10 +575,12 @@ object Starbound : ISBFileLocator {
tasks.add(VersionRegistry.load()) tasks.add(VersionRegistry.load())
val total = tasks.size.toDouble() val total = tasks.size.toDouble()
toLoad = tasks.size
while (tasks.isNotEmpty()) { while (tasks.isNotEmpty()) {
tasks.removeIf { it.isDone } tasks.removeIf { it.isDone }
checkMailbox() checkMailbox()
loaded = toLoad - tasks.size
loadingProgress = (total - tasks.size) / total loadingProgress = (total - tasks.size) / total
LockSupport.parkNanos(5_000_000L) LockSupport.parkNanos(5_000_000L)
} }

View File

@ -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_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE)
GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GLFW.GLFW_TRUE) 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" } require(window != MemoryUtil.NULL) { "Unable to create GLFW window" }
input.installCallback(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) 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() { private fun renderLoadingScreen() {
executeQueuedTasks() executeQueuedTasks()
updateViewportParams() 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) } 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() val runtime = Runtime.getRuntime()
//if (runtime.maxMemory() <= 4L * 1024L * 1024L * 1024L) { //if (runtime.maxMemory() <= 4L * 1024L * 1024L * 1024L) {
@ -925,6 +930,11 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
return true return true
} }
if (renderedLoadingScreen) {
renderedLoadingScreen = false
GLFW.glfwSetWindowTitle(window, "KStarbound")
}
input.think() input.think()
camera.think(Starbound.TIMESTEP) camera.think(Starbound.TIMESTEP)
executeQueuedTasks() executeQueuedTasks()

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -1,5 +1,10 @@
package ru.dbotthepony.kstarbound.defs 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.StreamCodec
import ru.dbotthepony.kommons.io.readUUID import ru.dbotthepony.kommons.io.readUUID
import ru.dbotthepony.kommons.io.readVector2d import ru.dbotthepony.kommons.io.readVector2d
@ -18,6 +23,7 @@ import ru.dbotthepony.kstarbound.server.world.ServerWorld
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.UUID import java.util.UUID
import kotlin.math.roundToInt
// original game has MVariant here // original game has MVariant here
// MVariant prepends InvalidValue to Variant<> template // MVariant prepends InvalidValue to Variant<> template
@ -26,7 +32,7 @@ import java.util.UUID
// typedef MVariant<WarpToWorld, WarpToPlayer, WarpAlias> WarpAction; // typedef MVariant<WarpToWorld, WarpToPlayer, WarpAlias> WarpAction;
// -> Variant<InvalidType, WarpToWorld, WarpToPlayer, WarpAlias> WarpAction // -> Variant<InvalidType, WarpToWorld, WarpToPlayer, WarpAlias> WarpAction
// hence WarpToWorld has index 1, WarpToPlayer 2, WarpAlias 3 // hence WarpToWorld has index 1, WarpToPlayer 2, WarpAlias 3
@JsonAdapter(SpawnTarget.Adapter::class)
sealed class SpawnTarget { sealed class SpawnTarget {
abstract fun write(stream: DataOutputStream, isLegacy: Boolean) abstract fun write(stream: DataOutputStream, isLegacy: Boolean)
abstract fun resolve(world: ServerWorld): Vector2d? abstract fun resolve(world: ServerWorld): Vector2d?
@ -41,7 +47,7 @@ sealed class SpawnTarget {
} }
override fun toString(): String { override fun toString(): String {
return "SpawnTarget.SpawnTarget" return "Whatever"
} }
} }
@ -56,7 +62,7 @@ sealed class SpawnTarget {
} }
override fun toString(): String { override fun toString(): String {
return "SpawnTarget.Entity[$id]" return id
} }
} }
@ -72,7 +78,7 @@ sealed class SpawnTarget {
} }
override fun toString(): String { override fun toString(): String {
return "SpawnTarget.Position[$position]" return "${position.x.roundToInt()}.${position.y.roundToInt()}"
} }
override fun resolve(world: ServerWorld): Vector2d { override fun resolve(world: ServerWorld): Vector2d {
@ -92,7 +98,7 @@ sealed class SpawnTarget {
} }
override fun toString(): String { override fun toString(): String {
return "SpawnTarget.X[$position]" return position.roundToInt().toString()
} }
override fun resolve(world: ServerWorld): Vector2d { override fun resolve(world: ServerWorld): Vector2d {
@ -100,7 +106,20 @@ sealed class SpawnTarget {
} }
} }
class Adapter : TypeAdapter<SpawnTarget>() {
override fun write(out: JsonWriter, value: SpawnTarget) {
out.value(value.toString())
}
override fun read(`in`: JsonReader): SpawnTarget {
return parse(`in`.nextString())
}
}
companion object { companion object {
private val position = Regex("\\d+.\\d+")
private val positionX = Regex("\\d+")
fun read(stream: DataInputStream, isLegacy: Boolean): SpawnTarget { fun read(stream: DataInputStream, isLegacy: Boolean): SpawnTarget {
return when (val type = stream.readUnsignedByte()) { return when (val type = stream.readUnsignedByte()) {
0 -> Whatever 0 -> Whatever
@ -110,14 +129,31 @@ sealed class SpawnTarget {
else -> throw IllegalArgumentException("Unknown SpawnTarget type $type!") 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 { sealed class WarpAction {
abstract fun write(stream: DataOutputStream, isLegacy: Boolean) abstract fun write(stream: DataOutputStream, isLegacy: Boolean)
abstract fun resolve(connection: ServerConnection): WorldID 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) { override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(1) stream.writeByte(1)
worldID.write(stream, isLegacy) worldID.write(stream, isLegacy)
@ -125,7 +161,14 @@ sealed class WarpAction {
} }
override fun resolve(connection: ServerConnection): WorldID { 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 return connection.server.clientByUUID(uuid)?.world?.worldID ?: WorldID.Limbo
} }
override fun toString(): String {
return "Player:$uuid"
}
}
class Adapter(gson: Gson) : TypeAdapter<WarpAction>() {
override fun write(out: JsonWriter, value: WarpAction) {
out.value(value.toString())
}
override fun read(`in`: JsonReader): WarpAction {
return parse(`in`.nextString())
}
} }
companion object { 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 CODEC = nativeCodec(::read, WarpAction::write)
val LEGACY_CODEC = legacyCodec(::read, WarpAction::write) val LEGACY_CODEC = legacyCodec(::read, WarpAction::write)
} }
} }
sealed class WarpAlias(val index: Int) : WarpAction() { 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) stream.write(3)
// because it is defined as enum class WarpAlias, without specifying uint8_t as type // because it is defined as enum class WarpAlias, without specifying uint8_t as type
stream.writeInt(index) 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) { object Return : WarpAlias(0) {
override fun resolve(connection: ServerConnection): WorldID { override fun remap(connection: ServerConnection): WarpAction {
TODO("Not yet implemented") return connection.returnWarp ?: World(connection.shipWorld.worldID)
} }
override fun toString(): String { override fun toString(): String {
return "WarpAlias.Return" return "Return"
} }
} }
object OrbitedWorld : WarpAlias(1) { object OrbitedWorld : WarpAlias(1) {
override fun resolve(connection: ServerConnection): WorldID { override fun remap(connection: ServerConnection): WarpAction {
TODO("Not yet implemented") return connection.orbitalWarpAction.orNull()?.first ?: World(connection.shipWorld.worldID)
} }
override fun toString(): String { override fun toString(): String {
return "WarpAlias.OrbitedWorld" return "OrbitedWorld"
} }
} }
object OwnShip : WarpAlias(2) { object OwnShip : WarpAlias(2) {
override fun resolve(connection: ServerConnection): WorldID { override fun remap(connection: ServerConnection): WarpAction {
return connection.shipWorld.worldID return World(connection.shipWorld.worldID)
} }
override fun toString(): String { override fun toString(): String {
return "WarpAlias.OwnShip" return "OwnShip"
} }
} }
} }

View File

@ -1,10 +1,51 @@
package ru.dbotthepony.kstarbound.defs 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 ru.dbotthepony.kstarbound.json.builder.JsonFactory
import java.util.function.Predicate
@JsonFactory @JsonFactory
data class UniverseServerConfig( data class UniverseServerConfig(
// in milliseconds // in milliseconds
val clockUpdatePacketInterval: Long = 500L, 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<VisitableWorldParameters> {
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<WorldPredicate> = ImmutableList.of(),
)
}

View File

@ -1,16 +1,24 @@
package ru.dbotthepony.kstarbound.defs 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.readUUID
import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeUUID import ru.dbotthepony.kommons.io.writeUUID
import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec 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 ru.dbotthepony.kstarbound.world.UniversePos
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.UUID import java.util.UUID
@JsonAdapter(WorldID.Adapter::class)
sealed class WorldID { sealed class WorldID {
abstract fun write(stream: DataOutputStream, isLegacy: Boolean) abstract fun write(stream: DataOutputStream, isLegacy: Boolean)
val isLimbo: Boolean get() = this is Limbo val isLimbo: Boolean get() = this is Limbo
@ -21,7 +29,7 @@ sealed class WorldID {
} }
override fun toString(): String { override fun toString(): String {
return "WorldID.Limbo" return "Nowhere"
} }
} }
@ -32,7 +40,7 @@ sealed class WorldID {
} }
override fun toString(): String { override fun toString(): String {
return "WorldID.Celestial[$pos]" return "CelestialWorld:$pos"
} }
} }
@ -43,7 +51,7 @@ sealed class WorldID {
} }
override fun toString(): String { 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 { override fun toString(): String {
return "WorldID.Instance[$name, uuid=$uuid, threat level=$threatLevel]" return "InstanceWorld:$name:${uuid?.toStarboundString() ?: "-"}:${threatLevel ?: "-"}"
}
}
class Adapter(gson: Gson) : TypeAdapter<WorldID>() {
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 CODEC = nativeCodec(::read, WorldID::write)
val LEGACY_CODEC = legacyCodec(::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 { fun read(stream: DataInputStream, isLegacy: Boolean): WorldID {
return when (val type = stream.readUnsignedByte()) { return when (val type = stream.readUnsignedByte()) {
0 -> Limbo 0 -> Limbo

View File

@ -1,12 +1,14 @@
package ru.dbotthepony.kstarbound.defs.actor.player package ru.dbotthepony.kstarbound.defs.actor.player
import com.google.common.collect.ImmutableSet import com.google.common.collect.ImmutableSet
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import ru.dbotthepony.kommons.guava.immutableSet import ru.dbotthepony.kommons.guava.immutableSet
import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.io.readCollection import ru.dbotthepony.kommons.io.readCollection
import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeCollection import ru.dbotthepony.kommons.io.writeCollection
import ru.dbotthepony.kstarbound.io.readDouble
import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.io.writeDouble
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
@ -19,15 +21,15 @@ data class ShipUpgrades(
val maxFuel: Int = 0, val maxFuel: Int = 0,
val crewSize: Int = 0, val crewSize: Int = 0,
val fuelEfficiency: Double = 1.0, val fuelEfficiency: Double = 1.0,
val shipSpeed: Int = 0, val shipSpeed: Double = 30.0,
val capabilities: ImmutableSet<String> = ImmutableSet.of() val capabilities: ImmutableSet<String> = ImmutableSet.of()
) { ) {
constructor(stream: DataInputStream, isLegacy: Boolean) : this( constructor(stream: DataInputStream, isLegacy: Boolean) : this(
stream.readInt(), stream.readInt(),
stream.readInt(), stream.readInt(),
stream.readInt(), stream.readInt(),
if (isLegacy) stream.readFloat().toDouble() else stream.readDouble(), stream.readDouble(isLegacy),
stream.readInt(), stream.readDouble(isLegacy),
ImmutableSet.copyOf(stream.readCollection { readInternedString() }) 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) { fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeInt(shipLevel) stream.writeInt(shipLevel)
stream.writeInt(maxFuel) stream.writeInt(maxFuel)
stream.writeInt(crewSize) stream.writeInt(crewSize)
stream.writeDouble(fuelEfficiency, isLegacy)
if (isLegacy) stream.writeDouble(shipSpeed, isLegacy)
stream.writeFloat(fuelEfficiency.toFloat())
else
stream.writeDouble(fuelEfficiency)
stream.writeInt(shipSpeed)
stream.writeCollection(capabilities) { writeBinaryString(it) } stream.writeCollection(capabilities) { writeBinaryString(it) }
} }

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.defs.world
import com.google.gson.JsonObject import com.google.gson.JsonObject
import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.vector.Vector2d 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.GlobalDefaults
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.fromJson 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.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.util.random.nextRange import ru.dbotthepony.kstarbound.util.random.nextRange
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import java.io.DataInputStream
import java.io.DataOutputStream
import kotlin.properties.Delegates import kotlin.properties.Delegates
class AsteroidsWorldParameters : VisitableWorldParameters() { class AsteroidsWorldParameters : VisitableWorldParameters() {
@ -68,6 +74,26 @@ class AsteroidsWorldParameters : VisitableWorldParameters() {
data[k] = v 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 { override fun createLayout(seed: Long): WorldLayout {
val random = random(seed) val random = random(seed)
val terrain = GlobalDefaults.asteroidWorlds.terrains.random(random) val terrain = GlobalDefaults.asteroidWorlds.terrains.random(random)

View File

@ -10,13 +10,23 @@ import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter 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.consumeNull
import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.gson.value 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.Starbound
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.json.builder.JsonFactory 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.util.random.random
import ru.dbotthepony.kstarbound.world.UniversePos 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?) { 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) { 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 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 var visitableParameters: VisitableWorldParameters? = null
private set 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) 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)
}
}
}
} }

View File

@ -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<CelestialParameters>)

View File

@ -1,10 +1,23 @@
package ru.dbotthepony.kstarbound.defs.world 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.math.RGBAColor
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.GlobalDefaults import ru.dbotthepony.kstarbound.GlobalDefaults
import ru.dbotthepony.kstarbound.Registries 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 ru.dbotthepony.kstarbound.util.random.random
import java.io.DataInputStream
import java.io.DataOutputStream
import kotlin.properties.Delegates import kotlin.properties.Delegates
class FloatingDungeonWorldParameters : VisitableWorldParameters() { class FloatingDungeonWorldParameters : VisitableWorldParameters() {
@ -58,6 +71,77 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() {
return layout 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 { companion object {
fun generate(typeName: String): FloatingDungeonWorldParameters { fun generate(typeName: String): FloatingDungeonWorldParameters {
val config = GlobalDefaults.dungeonWorlds[typeName] ?: throw NoSuchElementException("Unknown dungeon world type $typeName!") val config = GlobalDefaults.dungeonWorlds[typeName] ?: throw NoSuchElementException("Unknown dungeon world type $typeName!")

View File

@ -123,15 +123,26 @@ data class SkyWorldHorizon(val center: Vector2d, val scale: Double, val rotation
@JsonFactory @JsonFactory
data class SkyParameters( data class SkyParameters(
var skyType: SkyType = SkyType.BARREN, val skyType: SkyType = SkyType.BARREN,
var seed: Long = 0L, val seed: Long = 0L,
var dayLength: Double? = null, val dayLength: Double? = null,
var horizonClouds: Boolean = false, val horizonClouds: Boolean = false,
var skyColoring: Either<SkyColoring, RGBAColor> = Either.left(SkyColoring()), val skyColoring: Either<SkyColoring, RGBAColor> = Either.left(SkyColoring()),
var spaceLevel: Double? = null, val spaceLevel: Double? = null,
var surfaceLevel: Double? = null, val surfaceLevel: Double? = null,
var nearbyPlanet: Pair<List<Pair<String, Double>>, Vector2d>? = null, val nearbyPlanet: Planet? = null,
val nearbyMoons: ImmutableList<Planet> = ImmutableList.of(),
val horizonImages: ImmutableList<HorizonImage> = 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<Layer>)
companion object { companion object {
suspend fun create(coordinate: UniversePos, universe: Universe): SkyParameters { suspend fun create(coordinate: UniversePos, universe: Universe): SkyParameters {
if (coordinate.isSystem) if (coordinate.isSystem)

View File

@ -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<String, Double>,
val planetSizes: ImmutableList<Pair<Int, Double>>,
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<String>,
val initialObjectPools: WeightedList<Pair<Int, WeightedList<String>>>,
val clientShip: ClientShip,
) {
@JsonFactory
data class ClientShip(
val speed: Double,
val orbitDistance: Double,
val departTime: Double,
val spaceDepartTime: Double,
)
}

View File

@ -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<String, String> = 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<String, String> = ImmutableMap.of(),
val name: String,
)
}

View File

@ -13,6 +13,9 @@ import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.getArray import ru.dbotthepony.kommons.gson.getArray
import ru.dbotthepony.kommons.gson.getObject import ru.dbotthepony.kommons.gson.getObject
import ru.dbotthepony.kommons.gson.set 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.util.Either
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i 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.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.JsonDriven import ru.dbotthepony.kstarbound.defs.JsonDriven
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters 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.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.json.pairAdapter import ru.dbotthepony.kstarbound.json.pairAdapter
import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.stream import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.util.binnedChoice import ru.dbotthepony.kstarbound.util.binnedChoice
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.util.random.nextRange import ru.dbotthepony.kstarbound.util.random.nextRange
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
import kotlin.properties.Delegates import kotlin.properties.Delegates
@ -71,7 +82,46 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
val encloseLiquids: Boolean, val encloseLiquids: Boolean,
val fillMicrodungeons: 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 @JsonFactory
data class Layer( data class Layer(
@ -89,7 +139,40 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
val secondaryRegionSizeRange: Vector2d, val secondaryRegionSizeRange: Vector2d,
val subRegionSizeRange: 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) { override fun fromJson(data: JsonObject) {
super.fromJson(data) super.fromJson(data)
@ -206,6 +289,52 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
override val type: VisitableWorldParametersType override val type: VisitableWorldParametersType
get() = VisitableWorldParametersType.TERRESTRIAL 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 // why
override fun createLayout(seed: Long): WorldLayout { override fun createLayout(seed: Long): WorldLayout {
val layout = WorldLayout() val layout = WorldLayout()

View File

@ -1,5 +1,7 @@
package ru.dbotthepony.kstarbound.defs.world 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.Gson
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
@ -7,19 +9,41 @@ import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter 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.consumeNull
import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.gson.value 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.util.Either
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.collect.WeightedList 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.DispatchingAdapter
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.json.builder.JsonFactory 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 import kotlin.properties.Delegates
// uint8_t
enum class BeamUpRule(override val jsonName: String) : IStringSerializable { enum class BeamUpRule(override val jsonName: String) : IStringSerializable {
NOWHERE("Nowhere"), NOWHERE("Nowhere"),
SURFACE("Surface"), SURFACE("Surface"),
@ -27,6 +51,7 @@ enum class BeamUpRule(override val jsonName: String) : IStringSerializable {
ANYWHERE_WITH_WARNING("AnywhereWithWarning"); ANYWHERE_WITH_WARNING("AnywhereWithWarning");
} }
// uint8_t
enum class WorldEdgeForceRegion(override val jsonName: String) : IStringSerializable { enum class WorldEdgeForceRegion(override val jsonName: String) : IStringSerializable {
NONE("None"), NONE("None"),
TOP("Top"), TOP("Top"),
@ -34,6 +59,7 @@ enum class WorldEdgeForceRegion(override val jsonName: String) : IStringSerializ
TOP_AND_BOTTOM("TopAndBottom"); TOP_AND_BOTTOM("TopAndBottom");
} }
// uint8_t
enum class VisitableWorldParametersType(override val jsonName: String, val token: TypeToken<out VisitableWorldParameters>) : IStringSerializable { enum class VisitableWorldParametersType(override val jsonName: String, val token: TypeToken<out VisitableWorldParameters>) : IStringSerializable {
TERRESTRIAL("TerrestrialWorldParameters", TypeToken.get(TerrestrialWorldParameters::class.java)), TERRESTRIAL("TerrestrialWorldParameters", TypeToken.get(TerrestrialWorldParameters::class.java)),
ASTEROIDS("AsteroidsWorldParameters", TypeToken.get(AsteroidsWorldParameters::class.java)), ASTEROIDS("AsteroidsWorldParameters", TypeToken.get(AsteroidsWorldParameters::class.java)),
@ -169,4 +195,90 @@ abstract class VisitableWorldParameters {
toJson(data, isLegacy) toJson(data, isLegacy)
return data 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)
}
}
}
} }

View File

@ -74,6 +74,29 @@ fun InputStream.readAABBLegacy(): AABB {
return AABB(mins.toDoubleVector(), maxs.toDoubleVector()) return AABB(mins.toDoubleVector(), maxs.toDoubleVector())
} }
fun <S : InputStream, L, R> S.readEither(isLegacy: Boolean, left: S.() -> L, right: S.() -> R): Either<L, R> {
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 : OutputStream, L, R> S.writeEither(value: Either<L, R>, 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<AABB> { fun InputStream.readAABBLegacyOptional(): KOptional<AABB> {
val mins = readVector2f() val mins = readVector2f()
val maxs = readVector2f() val maxs = readVector2f()

View File

@ -50,7 +50,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
var entityIDRange: IntRange by Delegates.notNull() var entityIDRange: IntRange by Delegates.notNull()
private set private set
protected val coroutineScope = CoroutineScope(Starbound.COROUTINE_EXECUTOR) val scope = CoroutineScope(Starbound.COROUTINE_EXECUTOR)
var connectionID: Int = -1 var connectionID: Int = -1
set(value) { set(value) {
@ -116,7 +116,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
protected open fun onChannelClosed() { protected open fun onChannelClosed() {
isConnected = false isConnected = false
LOGGER.info("$this is terminated") LOGGER.info("$this is terminated")
coroutineScope.cancel("$this is terminated") scope.cancel("$this is terminated")
} }
fun bind(channel: Channel) { 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 warpActionCodec = StreamCodec.Pair(WarpAction.CODEC, WarpMode.CODEC).koptional()
private val legacyWarpActionCodec = StreamCodec.Pair(WarpAction.LEGACY_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 { val NIO_POOL by lazy {
NioEventLoopGroup(1, ThreadFactoryBuilder().setDaemon(true).setNameFormat("Network IO %d").build()) NioEventLoopGroup(1, ThreadFactoryBuilder().setDaemon(true).setNameFormat("Network IO %d").build())
} }

View File

@ -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.ProtocolRequestPacket
import ru.dbotthepony.kstarbound.network.packets.ProtocolResponsePacket import ru.dbotthepony.kstarbound.network.packets.ProtocolResponsePacket
import ru.dbotthepony.kstarbound.network.packets.StepUpdatePacket 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.CentralStructureUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ChatReceivePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ChatReceivePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectFailurePacket 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.EnvironmentUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.FindUniqueEntityResponsePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.FindUniqueEntityResponsePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket 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.ServerDisconnectPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerInfoPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerInfoPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket 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.TileDamageUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket 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.WorldStartPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStopPacket 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.ChatSendPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientDisconnectRequestPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientDisconnectRequestPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.DamageTileGroupPacket 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.FindUniqueEntityPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.FlyShipPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.PlayerWarpPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.PlayerWarpPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldClientStateUpdatePacket import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldClientStateUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldStartAcknowledgePacket import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldStartAcknowledgePacket
@ -389,7 +401,7 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.add(::HandshakeChallengePacket) // HandshakeChallenge LEGACY.add(::HandshakeChallengePacket) // HandshakeChallenge
LEGACY.add(::ChatReceivePacket) LEGACY.add(::ChatReceivePacket)
LEGACY.add(::UniverseTimeUpdatePacket) LEGACY.add(::UniverseTimeUpdatePacket)
LEGACY.skip("CelestialResponse") LEGACY.add(::CelestialResponsePacket)
LEGACY.add(::PlayerWarpResultPacket) LEGACY.add(::PlayerWarpResultPacket)
LEGACY.skip("PlanetTypeUpdate") LEGACY.skip("PlanetTypeUpdate")
LEGACY.skip("Pause") LEGACY.skip("Pause")
@ -400,9 +412,9 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.add(ClientDisconnectRequestPacket::read) LEGACY.add(ClientDisconnectRequestPacket::read)
LEGACY.add(::HandshakeResponsePacket) // HandshakeResponse LEGACY.add(::HandshakeResponsePacket) // HandshakeResponse
LEGACY.add(::PlayerWarpPacket) LEGACY.add(::PlayerWarpPacket)
LEGACY.skip("FlyShip") LEGACY.add(::FlyShipPacket)
LEGACY.add(::ChatSendPacket) LEGACY.add(::ChatSendPacket)
LEGACY.skip("CelestialRequest") LEGACY.add(::CelestialRequestPacket)
// Packets sent bidirectionally between the universe client and the universe // Packets sent bidirectionally between the universe client and the universe
// server // server
@ -445,23 +457,23 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.add(::EntityCreatePacket) LEGACY.add(::EntityCreatePacket)
LEGACY.add(EntityUpdateSetPacket::read) LEGACY.add(EntityUpdateSetPacket::read)
LEGACY.add(::EntityDestroyPacket) LEGACY.add(::EntityDestroyPacket)
LEGACY.skip("EntityInteract") LEGACY.add(::EntityInteractPacket)
LEGACY.skip("EntityInteractResult") LEGACY.add(::EntityInteractResultPacket)
LEGACY.skip("HitRequest") LEGACY.skip("HitRequest")
LEGACY.skip("DamageRequest") LEGACY.skip("DamageRequest")
LEGACY.skip("DamageNotification") LEGACY.skip("DamageNotification")
LEGACY.skip("EntityMessage") LEGACY.skip("EntityMessage")
LEGACY.skip("EntityMessageResponse") LEGACY.skip("EntityMessageResponse")
LEGACY.skip("UpdateWorldProperties") LEGACY.add(::UpdateWorldPropertiesPacket)
LEGACY.add(::StepUpdatePacket) LEGACY.add(::StepUpdatePacket)
// Packets sent system server -> system client // Packets sent system server -> system client
LEGACY.skip("SystemWorldStart") LEGACY.add(::SystemWorldStartPacket)
LEGACY.skip("SystemWorldUpdate") LEGACY.add(::SystemWorldUpdatePacket)
LEGACY.skip("SystemObjectCreate") LEGACY.add(::SystemObjectCreatePacket)
LEGACY.skip("SystemObjectDestroy") LEGACY.add(::SystemObjectDestroyPacket)
LEGACY.skip("SystemShipCreate") LEGACY.add(::SystemShipCreatePacket)
LEGACY.skip("SystemShipDestroy") LEGACY.add(::SystemShipDestroyPacket)
// Packets sent system client -> system server // Packets sent system client -> system server
LEGACY.skip("SystemObjectSpawn") LEGACY.skip("SystemObjectSpawn")

View File

@ -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<Either<ChunkData, SystemData>>) : 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<List<Pair<Vector2i, Vector2i>>>,
val constellations: List<Pair<Vector2i, Vector2i>>,
val systemParameters: Map<Vector3i, CelestialParameters>,
val systemObjects: Map<Vector3i, Map<Int, PlanetData>>,
) {
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<Int, PlanetData>) {
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<Int, CelestialParameters>,
) {
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")
}
}

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -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<ByteArrayList>, val ships: Collection<ByteArrayList>, 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")
}
}

View File

@ -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<UUID, ByteArrayList>, val ships: Map<UUID, ByteArrayList>) : 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")
}
}

View File

@ -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)
}
}
}

View File

@ -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<Either<Vector2i, Vector3i>>) : 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<Either<CelestialResponsePacket.ChunkData, CelestialResponsePacket.SystemData>>()
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<Int, CelestialResponsePacket.PlanetData>()
for (planet in connection.server.universe.children(systemPos)) {
val planetData = connection.server.universe.parameters(planet) ?: continue
val children = HashMap<Int, CelestialParameters>()
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))
}
}
}

View File

@ -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")
}
}

View File

@ -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)
}
}

View File

@ -160,7 +160,7 @@ fun <E : Enum<E>> networkedEnum(value: E) = BasicNetworkedElement(value, StreamC
// networks enum as a signed variable length integer on legacy protocol // networks enum as a signed variable length integer on legacy protocol
fun <E : Enum<E>> networkedEnumStupid(value: E): BasicNetworkedElement<E, Int> { fun <E : Enum<E>> networkedEnumStupid(value: E): BasicNetworkedElement<E, Int> {
val codec = StreamCodec.Enum(value::class.java) 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 // networks enum as string on legacy protocol

View File

@ -45,6 +45,10 @@ class ServerChannels(val server: StarboundServer) : Closeable {
broadcast(ServerInfoPacket(new, server.settings.maxPlayers)) broadcast(ServerInfoPacket(new, server.settings.maxPlayers))
} }
fun connectionByID(id: Int): ServerConnection? {
return connections.firstOrNull { it.connectionID == id }
}
private fun cycleConnectionID(): Int { private fun cycleConnectionID(): Int {
val v = ++nextConnectionID and MAX_PLAYERS val v = ++nextConnectionID and MAX_PLAYERS

View File

@ -3,16 +3,20 @@ package ru.dbotthepony.kstarbound.server
import com.google.gson.JsonObject import com.google.gson.JsonObject
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.ByteKey import ru.dbotthepony.kommons.io.ByteKey
import ru.dbotthepony.kommons.util.KOptional 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.WarpAction
import ru.dbotthepony.kstarbound.defs.WarpAlias import ru.dbotthepony.kstarbound.defs.WarpAlias
import ru.dbotthepony.kstarbound.defs.WorldID 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.Connection
import ru.dbotthepony.kstarbound.network.ConnectionSide import ru.dbotthepony.kstarbound.network.ConnectionSide
import ru.dbotthepony.kstarbound.network.ConnectionType 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.ServerWorldTracker
import ru.dbotthepony.kstarbound.server.world.WorldStorage import ru.dbotthepony.kstarbound.server.world.WorldStorage
import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage 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.server.world.ServerWorld
import ru.dbotthepony.kstarbound.world.SystemWorldLocation
import ru.dbotthepony.kstarbound.world.UniversePos
import java.util.HashMap import java.util.HashMap
import java.util.UUID import java.util.UUID
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import kotlin.properties.Delegates import kotlin.properties.Delegates
// serverside part of connection // serverside part of connection
@ -35,13 +40,16 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
var tracker: ServerWorldTracker? = null var tracker: ServerWorldTracker? = null
var worldStartAcknowledged = false var worldStartAcknowledged = false
var returnWarp: WarpAction? = null var returnWarp: WarpAction? = null
var systemWorld: ServerSystemWorld? = null
val world: ServerWorld? val world: ServerWorld?
get() = tracker?.world get() = tracker?.world
// packets which interact with world must be // packets which interact with world must be
// executed on world's thread // 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 lateinit var shipWorld: ServerWorld
private set private set
@ -62,6 +70,10 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
return "ServerConnection[$nickname $uuid ID=$connectionID channel=$channel / $world]" return "ServerConnection[$nickname $uuid ID=$connectionID channel=$channel / $world]"
} }
fun alias(): String {
return "$nickname <$connectionID/$uuid>"
}
private val shipChunks = HashMap<ByteKey, KOptional<ByteArray>>() private val shipChunks = HashMap<ByteKey, KOptional<ByteArray>>()
private val modifiedShipChunks = ObjectOpenHashSet<ByteKey>() private val modifiedShipChunks = ObjectOpenHashSet<ByteKey>()
var shipChunkSource by Delegates.notNull<WorldStorage>() var shipChunkSource by Delegates.notNull<WorldStorage>()
@ -127,9 +139,12 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
private val warpQueue = Channel<Pair<WarpAction, Boolean>>(capacity = 10) private val warpQueue = Channel<Pair<WarpAction, Boolean>>(capacity = 10)
private suspend fun handleWarps() { private suspend fun warpEventLoop() {
while (true) { 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") LOGGER.info("Trying to warp $this to $request")
@ -163,6 +178,139 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
} }
} }
private val flyShipQueue = Channel<Pair<Vector3i, SystemWorldLocation>>(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<VisitableWorldParameters>()
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) { fun enqueueWarp(destination: WarpAction, deploy: Boolean = false) {
warpQueue.trySend(destination to deploy) warpQueue.trySend(destination to deploy)
} }
@ -242,7 +390,11 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
shipWorld = it shipWorld = it
shipWorld.thread.start() shipWorld.thread.start()
enqueueWarp(WarpAlias.OwnShip) 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) { if (server.channels.connections.size > 1) {
enqueueWarp(WarpAction.Player(server.channels.connections.first().uuid!!)) enqueueWarp(WarpAction.Player(server.channels.connections.first().uuid!!))

View File

@ -1,25 +1,36 @@
package ru.dbotthepony.kstarbound.server package ru.dbotthepony.kstarbound.server
import it.unimi.dsi.fastutil.objects.ObjectArraySet 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 org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.MailboxExecutorService import ru.dbotthepony.kommons.util.MailboxExecutorService
import ru.dbotthepony.kommons.vector.Vector3i
import ru.dbotthepony.kstarbound.GlobalDefaults import ru.dbotthepony.kstarbound.GlobalDefaults
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.WorldID import ru.dbotthepony.kstarbound.defs.WorldID
import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket
import ru.dbotthepony.kstarbound.server.world.ServerUniverse import ru.dbotthepony.kstarbound.server.world.ServerUniverse
import ru.dbotthepony.kstarbound.server.world.ServerWorld 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.Clock
import ru.dbotthepony.kstarbound.util.ExceptionLogger import ru.dbotthepony.kstarbound.util.ExceptionLogger
import ru.dbotthepony.kstarbound.util.ExecutionSpinner import ru.dbotthepony.kstarbound.util.ExecutionSpinner
import ru.dbotthepony.kstarbound.world.UniversePos
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import java.util.function.Supplier
sealed class StarboundServer(val root: File) : Closeable { sealed class StarboundServer(val root: File) : Closeable {
init { init {
@ -38,6 +49,25 @@ sealed class StarboundServer(val root: File) : Closeable {
val thread = Thread(spinner, "Server Thread") val thread = Thread(spinner, "Server Thread")
val universe = ServerUniverse() val universe = ServerUniverse()
val chat = ChatHandler(this) val chat = ChatHandler(this)
val context = CoroutineScope(Starbound.COROUTINE_EXECUTOR)
private val systemWorlds = HashMap<Vector3i, CompletableFuture<ServerSystemWorld>>()
private suspend fun loadSystemWorld0(location: Vector3i): ServerSystemWorld {
return ServerSystemWorld.create(this, location)
}
fun loadSystemWorld(location: Vector3i): CompletableFuture<ServerSystemWorld> {
return CompletableFuture.supplyAsync(Supplier {
systemWorlds.computeIfAbsent(location) {
context.async { loadSystemWorld0(location) }.asCompletableFuture()
}
}, mailbox).thenCompose { it }
}
fun loadSystemWorld(location: UniversePos): CompletableFuture<ServerSystemWorld> {
return loadSystemWorld(location.location)
}
val settings = ServerSettings() val settings = ServerSettings()
val channels = ServerChannels(this) 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() tick0()
return !isClosed return !isClosed
} }
@ -116,6 +169,7 @@ sealed class StarboundServer(val root: File) : Closeable {
if (isClosed) return if (isClosed) return
isClosed = true isClosed = true
context.cancel("Server shutting down")
channels.close() channels.close()
worlds.values.forEach { it.close() } worlds.values.forEach { it.close() }
limboWorlds.forEach { it.close() } limboWorlds.forEach { it.close() }

View File

@ -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<JsonObject> = ImmutableList.of(),
)
override val entities = HashMap<UUID, ServerEntity>()
override val ships = HashMap<UUID, ServerShip>()
private class Task<T>(val supplier: Supplier<T>) : Runnable {
val future = CompletableFuture<T>()
override fun run() {
try {
future.complete(supplier.get())
} catch (err: Throwable) {
future.completeExceptionally(err)
}
}
}
private val tasks = ConcurrentLinkedQueue<Task<*>>()
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<ServerShip> {
val task = Task { addClient0(client, shipSpeed, location) }
tasks.add(task)
return task.future
}
fun removeClient(client: ServerConnection): CompletableFuture<Unit> {
val task = Task { removeClient0(client) }
tasks.add(task)
return task.future
}
private fun flyShip0(client: ServerConnection, location: SystemWorldLocation, future: CompletableFuture<SystemWorldLocation>) {
val ship = ships[client.uuid] ?: throw IllegalStateException("No client $client in $this!")
ship.destination(location, future)
}
fun flyShip(client: ServerConnection, location: SystemWorldLocation): CompletableFuture<SystemWorldLocation> {
val future = CompletableFuture<SystemWorldLocation>()
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<Vector2d>()
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<UUID>()
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<UUID, ByteArrayList>()
val ships = HashMap<UUID, ByteArrayList>()
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<SystemWorldLocation>()
fun destination(destination: SystemWorldLocation, future: CompletableFuture<SystemWorldLocation>) {
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
}
}
}

View File

@ -21,6 +21,8 @@ import ru.dbotthepony.kstarbound.defs.world.CelestialNames
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import ru.dbotthepony.kstarbound.fromJson import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.io.BTreeDB5 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.util.random.staticRandom64
import ru.dbotthepony.kstarbound.world.Universe import ru.dbotthepony.kstarbound.world.Universe
import ru.dbotthepony.kstarbound.world.UniversePos import ru.dbotthepony.kstarbound.world.UniversePos
@ -94,7 +96,7 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab
return listOf() return listOf()
} }
override suspend fun scanSystems(region: AABBi, includedTypes: Set<String>?): List<UniversePos> { override suspend fun findSystems(region: AABBi, includedTypes: Set<String>?): List<UniversePos> {
val copy = if (includedTypes != null) ObjectOpenHashSet(includedTypes) else null val copy = if (includedTypes != null) ObjectOpenHashSet(includedTypes) else null
val futures = ArrayList<CompletableFuture<List<UniversePos>>>() val futures = ArrayList<CompletableFuture<List<UniversePos>>>()
@ -126,7 +128,64 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab
return futures.stream().flatMap { it.get().stream() }.toList() return futures.stream().flatMap { it.get().stream() }.toList()
} }
override suspend fun scanConstellationLines(region: AABBi): List<Pair<Vector2i, Vector2i>> { override suspend fun <T> scanSystems(region: AABBi, callback: suspend (UniversePos) -> KOptional<T>): KOptional<T> {
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<UniversePos>(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<Pair<Vector2i, Vector2i>> {
val futures = ArrayList<CompletableFuture<List<Pair<Vector2i, Vector2i>>>>() val futures = ArrayList<CompletableFuture<List<Pair<Vector2i, Vector2i>>>>()
for (pos in chunkPositions(region)) { for (pos in chunkPositions(region)) {

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.server.world package ru.dbotthepony.kstarbound.server.world
import com.google.gson.JsonElement
import com.google.gson.JsonObject import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.IntArraySet import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction 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.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.WarpAction import ru.dbotthepony.kstarbound.defs.WarpAction
import ru.dbotthepony.kstarbound.defs.WarpAlias
import ru.dbotthepony.kstarbound.defs.WorldID import ru.dbotthepony.kstarbound.defs.WorldID
import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult 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.IPacket
import ru.dbotthepony.kstarbound.network.packets.StepUpdatePacket import ru.dbotthepony.kstarbound.network.packets.StepUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPacket 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.StarboundServer
import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.util.AssetPathStack 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 { override fun chunkFactory(pos: ChunkPos): ServerChunk {
return ServerChunk(this, pos) return ServerChunk(this, pos)
} }
@ -521,6 +529,11 @@ class ServerWorld private constructor(
world.respawnInWorld = meta.respawnInWorld world.respawnInWorld = meta.respawnInWorld
world.adjustPlayerSpawn = meta.adjustPlayerStart world.adjustPlayerSpawn = meta.adjustPlayerStart
world.centralStructure = meta.centralStructure world.centralStructure = meta.centralStructure
for ((k, v) in meta.worldProperties.entrySet()) {
world.setProperty(k, v)
}
world.protectedDungeonIDs.addAll(meta.protectedDungeonIds) world.protectedDungeonIDs.addAll(meta.protectedDungeonIds)
world world
} }

View File

@ -49,7 +49,10 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
client.worldID = world.worldID 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 val isRemoved = AtomicBoolean()
private var isActuallyRemoved = false private var isActuallyRemoved = false
@ -99,7 +102,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
dungeonGravity = mapOf(), dungeonGravity = mapOf(),
dungeonBreathable = mapOf(), dungeonBreathable = mapOf(),
protectedDungeonIDs = world.protectedDungeonIDs, protectedDungeonIDs = world.protectedDungeonIDs,
worldProperties = world.properties.deepCopy(), worldProperties = world.copyProperties(),
connectionID = client.connectionID, connectionID = client.connectionID,
localInterpolationMode = false, localInterpolationMode = false,
)) ))
@ -143,9 +146,12 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
} }
run { run {
val (data, version) = world.sky.networkedGroup.write(skyVersion, isLegacy = client.isLegacy) if (skyUpdateWaitTicks++ >= 4) {
skyVersion = version val (data, version) = world.sky.networkedGroup.write(skyVersion, isLegacy = client.isLegacy)
send(EnvironmentUpdatePacket(data, ByteArrayList())) skyVersion = version
send(EnvironmentUpdatePacket(data, ByteArrayList()))
skyUpdateWaitTicks = 0
}
} }
run { run {

View File

@ -10,23 +10,26 @@ import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.ints.Int2ObjectMap import it.unimi.dsi.fastutil.ints.Int2ObjectMap
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.getArray import ru.dbotthepony.kommons.gson.getArray
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kommons.vector.Vector3i import ru.dbotthepony.kommons.vector.Vector3i
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters 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.pairAdapter
import ru.dbotthepony.kstarbound.json.pairListAdapter import ru.dbotthepony.kstarbound.json.pairListAdapter
import ru.dbotthepony.kstarbound.network.packets.clientbound.CelestialResponsePacket
import ru.dbotthepony.kstarbound.world.UniversePos import ru.dbotthepony.kstarbound.world.UniversePos
import java.util.HashMap import java.util.HashMap
import java.util.stream.Collectors
class UniverseChunk(var chunkPos: Vector2i = Vector2i.ZERO) { class UniverseChunk(var chunkPos: Vector2i = Vector2i.ZERO) {
data class System(val parameters: CelestialParameters, val planets: Int2ObjectMap<CelestialPlanet>) data class System(val parameters: CelestialParameters, val planets: Int2ObjectMap<Planet>)
@JsonFactory
data class Planet(val parameters: CelestialParameters, val satellites: Int2ObjectMap<CelestialParameters>)
val systems = HashMap<Vector3i, System>() val systems = HashMap<Vector3i, System>()
val constellations = ObjectOpenHashSet<Pair<Vector2i, Vector2i>>() val constellations = ObjectOpenHashSet<Pair<Vector2i, Vector2i>>()
@ -44,6 +47,15 @@ class UniverseChunk(var chunkPos: Vector2i = Vector2i.ZERO) {
throw RuntimeException("unreachable code") 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<UniverseChunk>() { class Adapter(gson: Gson) : TypeAdapter<UniverseChunk>() {
private val vectors = gson.getAdapter(Vector2i::class.java) private val vectors = gson.getAdapter(Vector2i::class.java)
private val vectors3 = gson.getAdapter(Vector3i::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 orbit = planetPair[0].asInt
val params = planetPair[1] as JsonObject 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 val satellitesPairs = params["satellites"] as JsonArray
for (satellitePair in satellitesPairs) { for (satellitePair in satellitesPairs) {

View File

@ -18,8 +18,6 @@ import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kommons.vector.Vector3i import ru.dbotthepony.kommons.vector.Vector3i
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters 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.io.BTreeDB5
import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.json.readJsonElement 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 systemParams.parameters["constellationCapable"] = system.constellationCapable
} }
val planets = Int2ObjectArrayMap<CelestialPlanet>() val planets = Int2ObjectArrayMap<UniverseChunk.Planet>()
for (planetOrbitIndex in 1 .. universe.baseInformation.planetOrbitalLevels) { for (planetOrbitIndex in 1 .. universe.baseInformation.planetOrbitalLevels) {
// this looks dumb at first, but then it makes sense // 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) return UniverseChunk.System(systemParams, planets)

View File

@ -21,8 +21,8 @@ val JsonElement.coalesceNull: JsonElement?
fun UUID.toStarboundString(): String { fun UUID.toStarboundString(): String {
val builder = StringBuilder(32) val builder = StringBuilder(32)
val a = mostSignificantBits.toString(16) val a = java.lang.Long.toUnsignedString(mostSignificantBits, 16)
val b = mostSignificantBits.toString(16) val b = java.lang.Long.toUnsignedString(leastSignificantBits, 16)
for (i in a.length until 16) for (i in a.length until 16)
builder.append("0") builder.append("0")
@ -37,6 +37,17 @@ fun UUID.toStarboundString(): String {
return builder.toString() 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 { fun paddedNumber(number: Int, digits: Int): String {
val str = number.toString() val str = number.toString()

View File

@ -3,15 +3,15 @@ package ru.dbotthepony.kstarbound.util.random
import ru.dbotthepony.kstarbound.getValue import ru.dbotthepony.kstarbound.getValue
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
// multiply with carry random number generator, as used by original Starbound // MWC Random (multiply with carry), exact replica from original code
// Game's code SHOULD NOT be tied to this generator, random() global function should be used // (for purpose of giving exactly the same results for same seed provided)
// What interesting though, is the size of cycle - 256 (8092 bits). Others use much bigger number - 4096 @OptIn(ExperimentalUnsignedTypes::class)
class MWCRandom(seed: Long = System.nanoTime(), cycle: Int = 256, windupIterations: Int = 32) : RandomGenerator { class MWCRandom(seed: ULong = System.nanoTime().toULong(), cycle: Int = 256, windupIterations: Int = 32) : RandomGenerator {
private val data = IntArray(cycle) private val data = UIntArray(cycle)
private var carry = 0 private var carry = 0u
private var dataIndex = 0 private var dataIndex = 0
var seed: Long = seed var seed: ULong = seed
private set private set
init { init {
@ -22,49 +22,72 @@ class MWCRandom(seed: Long = System.nanoTime(), cycle: Int = 256, windupIteratio
/** /**
* re-initialize this MWC generator * re-initialize this MWC generator
*/ */
fun init(seed: Long, windupIterations: Int = 0) { fun init(seed: ULong, windupIterations: Int = 0) {
this.seed = seed this.seed = seed
carry = (seed % MAGIC).toInt() carry = (seed % MAGIC).toUInt()
data[0] = seed.toInt() data[0] = seed.toUInt()
data[1] = seed.ushr(32).toInt() data[1] = seed.shr(32).toUInt()
for (i in 2 until data.size) { 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 dataIndex = data.size - 1
// initial windup // initial windup
for (i in 0 until windupIterations) { 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 // 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[0] = data[0].xor(seed.toUInt())
data[1] = data[1].xor(seed.ushr(32).xor(seed).toInt()) data[1] = data[1].xor(seed.shr(32).xor(seed).toUInt())
for (i in 2 until data.size) { 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 { override fun nextLong(): Long {
dataIndex = (dataIndex + 1) % data.size val a = nextInt().toLong() and 0xFFFFFFFFL
val t = MAGIC.toLong() * data[dataIndex].toLong() + carry.toLong() val b = nextInt().toLong() and 0xFFFFFFFFL
return a.shl(32) or b
}
carry = t.ushr(32).toInt() override fun nextFloat(): Float {
data[dataIndex] = t.toInt() 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 { companion object {
const val MAGIC = 809430660 const val MAGIC = 809430660u
val GLOBAL: MWCRandom by ThreadLocal.withInitial { MWCRandom() } val GLOBAL: MWCRandom by ThreadLocal.withInitial { MWCRandom() }
} }

View File

@ -21,7 +21,7 @@ import kotlin.math.sqrt
* Replacing generator returned here will affect all random generation code. * Replacing generator returned here will affect all random generation code.
*/ */
fun random(seed: Long = System.nanoTime()): RandomGenerator { fun random(seed: Long = System.nanoTime()): RandomGenerator {
return MWCRandom(seed) return MWCRandom(seed.toULong())
} }
private fun toBytes(accept: ByteConsumer, value: Short) { private fun toBytes(accept: ByteConsumer, value: Short) {

View File

@ -65,6 +65,9 @@ abstract class CoordinateMapper {
open fun isValidCellIndex(value: Int): Boolean = inBoundsCell(value) open fun isValidCellIndex(value: Int): Boolean = inBoundsCell(value)
open fun isValidChunkIndex(value: Int): Boolean = inBoundsChunk(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() { class Wrapper(private val cells: Int) : CoordinateMapper() {
override val chunks = divideUp(cells, CHUNK_SIZE) override val chunks = divideUp(cells, CHUNK_SIZE)
private val cellsD = cells.toDouble() private val cellsD = cells.toDouble()
@ -88,6 +91,36 @@ abstract class CoordinateMapper {
override fun isValidCellIndex(value: Int) = true override fun isValidCellIndex(value: Int) = true
override fun isValidChunkIndex(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 { override fun chunkFromCell(value: Int): Int {
return chunk(value shr CHUNK_SIZE_BITS) return chunk(value shr CHUNK_SIZE_BITS)
} }
@ -186,6 +219,14 @@ abstract class CoordinateMapper {
private var cellsEdgeFloat = cellsF 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 { init {
var power = -64f var power = -64f

View File

@ -34,6 +34,8 @@ class Sky() {
private val skyParametersNetState = networkedGroup.upstream.add(networkedJson(SkyParameters())) private val skyParametersNetState = networkedGroup.upstream.add(networkedJson(SkyParameters()))
var skyType by networkedGroup.upstream.add(networkedEnumStupid(SkyType.ORBITAL)) var skyType by networkedGroup.upstream.add(networkedEnumStupid(SkyType.ORBITAL))
private set
var time by networkedGroup.upstream.add(networkedDouble()) var time by networkedGroup.upstream.add(networkedDouble())
private set private set
var flyingType by networkedGroup.upstream.add(networkedEnum(FlyingType.NONE)) var flyingType by networkedGroup.upstream.add(networkedEnum(FlyingType.NONE))
@ -42,12 +44,12 @@ class Sky() {
private set private set
var startInWarp by networkedGroup.upstream.add(networkedBoolean()) var startInWarp by networkedGroup.upstream.add(networkedBoolean())
private set 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()) var worldMoveOffset by networkedGroup.upstream.add(networkedVec2f())
private set private set
var starMoveOffset by networkedGroup.upstream.add(networkedVec2f()) var starMoveOffset by networkedGroup.upstream.add(networkedVec2f())
private set 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()) var flyingTimer by networkedGroup.upstream.add(networkedFloat())
private set private set
@ -61,6 +63,8 @@ class Sky() {
var pathOffset = Vector2d.ZERO var pathOffset = Vector2d.ZERO
private set private set
var worldRotation: Double = 0.0
private set
var starRotation: Double = 0.0 var starRotation: Double = 0.0
private set private set
var pathRotation: Double = 0.0 var pathRotation: Double = 0.0
@ -98,14 +102,19 @@ class Sky() {
fun startFlying(enterHyperspace: Boolean, startInWarp: Boolean = false) { fun startFlying(enterHyperspace: Boolean, startInWarp: Boolean = false) {
if (startInWarp) if (startInWarp)
flyingType = FlyingType.WARP flyingType = FlyingType.WARP
else else if (flyingType == FlyingType.NONE)
flyingType = FlyingType.DISEMBARKING flyingType = FlyingType.DISEMBARKING
flyingTimer = 0.0 // flyingTimer = 0.0
this.enterHyperspace = enterHyperspace this.enterHyperspace = enterHyperspace
this.startInWarp = startInWarp this.startInWarp = startInWarp
} }
fun stopFlyingAt(destination: SkyParameters) {
this.destination = destination
skyType = SkyType.ORBITAL
}
private var lastFlyingType = FlyingType.NONE private var lastFlyingType = FlyingType.NONE
private var lastWarpPhase = WarpPhase.MAINTAIN private var lastWarpPhase = WarpPhase.MAINTAIN
private var sentSFX = false private var sentSFX = false
@ -141,11 +150,32 @@ class Sky() {
when (warpPhase) { when (warpPhase) {
WarpPhase.SLOWING_DOWN -> { 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 -> { WarpPhase.MAINTAIN -> {
flashTimer = GlobalDefaults.sky.flashTimer //flashTimer = GlobalDefaults.sky.flashTimer
skyType = SkyType.WARP skyType = SkyType.WARP
sentSFX = false sentSFX = false
} }
@ -157,6 +187,7 @@ class Sky() {
} }
lastFlyingType = flyingType lastFlyingType = flyingType
lastWarpPhase = warpPhase
} }

View File

@ -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<UUID, Entity>
abstract val ships: Map<UUID, Ship>
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
}
}
}

View File

@ -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<Pair<WarpAction, WarpMode>>
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<Pair<WarpAction, WarpMode>> {
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<Pair<WarpAction, WarpMode>> {
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<Pair<WarpAction, WarpMode>> {
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<Pair<WarpAction, WarpMode>> {
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<Pair<WarpAction, WarpMode>> {
// 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!")
}
}
}
}

View File

@ -23,9 +23,16 @@ abstract class Universe {
* are guaranteed to have unique x/y coordinates, and are meant to be viewed * 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 * from the top in 2d. The z-coordinate is there simpy as a validation
* parameter. * parameter.
*
* [callback] determines when to stop scanning (returning non empty KOptional will stop scanning)
*/ */
abstract suspend fun scanSystems(region: AABBi, includedTypes: Set<String>? = null): List<UniversePos> abstract suspend fun <T> scanSystems(region: AABBi, callback: suspend (UniversePos) -> KOptional<T>): KOptional<T>
abstract suspend fun scanConstellationLines(region: AABBi): List<Pair<Vector2i, Vector2i>> abstract suspend fun scanConstellationLines(region: AABBi, aggressive: Boolean = false): List<Pair<Vector2i, Vector2i>>
/**
* Similar to [scanSystems], but scans for ALL systems in given range, on multiple threads
*/
abstract suspend fun findSystems(region: AABBi, includedTypes: Set<String>? = null): List<UniversePos>
abstract suspend fun hasChildren(pos: UniversePos): Boolean abstract suspend fun hasChildren(pos: UniversePos): Boolean
abstract suspend fun children(pos: UniversePos): List<UniversePos> abstract suspend fun children(pos: UniversePos): List<UniversePos>

View File

@ -34,7 +34,7 @@ import java.io.DataOutputStream
* exists in a specific universe or not can be expressed. * exists in a specific universe or not can be expressed.
*/ */
@JsonAdapter(UniversePos.Adapter::class) @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<UniversePos> {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readVector3i(), if (isLegacy) stream.readInt() else stream.readVarInt(), if (isLegacy) stream.readInt() else stream.readVarInt()) constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readVector3i(), if (isLegacy) stream.readInt() else stream.readVarInt(), if (isLegacy) stream.readInt() else stream.readVarInt())
init { init {
@ -43,7 +43,21 @@ data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit:
} }
override fun toString(): String { 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 val isSystem: Boolean
@ -53,7 +67,7 @@ data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit:
get() = planetOrbit != 0 && satelliteOrbit == 0 get() = planetOrbit != 0 && satelliteOrbit == 0
val isSatellite: Boolean val isSatellite: Boolean
get() = planetOrbit != 0 && satelliteOrbit == 0 get() = planetOrbit != 0 && satelliteOrbit != 0
val orbitNumber: Int val orbitNumber: Int
get() = if (isSatellite) satelliteOrbit else if (isPlanet) planetOrbit else 0 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 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) { fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeStruct3i(location) stream.writeStruct3i(location)
@ -132,21 +155,7 @@ data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit:
return ZERO return ZERO
else { else {
try { try {
val split = read.split(splitter) return parse(read)
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)
} catch (err: Throwable) { } catch (err: Throwable) {
throw JsonSyntaxException("Error parsing UniversePos from string", err) 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("[ _:]") private val splitter = Regex("[ _:]")
val ZERO = UniversePos() 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)
}
} }
} }

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import com.google.gson.JsonElement
import com.google.gson.JsonObject import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.ints.IntArraySet 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 org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.collect.filterNotNull import ru.dbotthepony.kommons.collect.filterNotNull
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.util.IStruct2d import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.util.AABB 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.Starbound
import ru.dbotthepony.kstarbound.defs.world.WorldStructure import ru.dbotthepony.kstarbound.defs.world.WorldStructure
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.IPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket
@ -239,7 +242,26 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
var centralStructure: WorldStructure = WorldStructure() var centralStructure: WorldStructure = WorldStructure()
val protectedDungeonIDs = IntArraySet() val protectedDungeonIDs = IntArraySet()
val properties = JsonObject() protected val worldProperties = JsonObject()
fun copyProperties(): JsonObject = worldProperties.deepCopy()
fun updateProperties(properties: JsonObject) {
mergeJson(worldProperties, properties)
}
protected open fun setProperty0(key: String, value: JsonElement) {
}
fun setProperty(key: String, value: JsonElement) {
if (worldProperties[key] == value)
return
val copy = value.deepCopy()
worldProperties[key] = copy
setProperty0(key, copy)
}
open fun setPlayerSpawn(position: Vector2d, respawnInWorld: Boolean) { open fun setPlayerSpawn(position: Vector2d, respawnInWorld: Boolean) {
playerSpawnPosition = position playerSpawnPosition = position

View File

@ -213,4 +213,12 @@ data class WorldGeometry(val size: Vector2i, val loopX: Boolean, val loopY: Bool
return ObjectArraySet.ofUnchecked(*result.toTypedArray()) return ObjectArraySet.ofUnchecked(*result.toTypedArray())
} }
fun diff(a: Vector2i, b: Vector2i): Vector2i {
return Vector2i(x.diff(a.x, b.x), y.diff(a.y, b.y))
}
fun diff(a: Vector2d, b: Vector2d): Vector2d {
return Vector2d(x.diff(a.x, b.x), y.diff(a.y, b.y))
}
} }

View File

@ -13,6 +13,8 @@ import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.client.world.ClientWorld import ru.dbotthepony.kstarbound.client.world.ClientWorld
import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.InteractAction
import ru.dbotthepony.kstarbound.defs.InteractRequest
import ru.dbotthepony.kstarbound.defs.JsonDriven import ru.dbotthepony.kstarbound.defs.JsonDriven
import ru.dbotthepony.kstarbound.network.packets.EntityDestroyPacket import ru.dbotthepony.kstarbound.network.packets.EntityDestroyPacket
import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec
@ -141,6 +143,10 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
} }
} }
open fun interact(request: InteractRequest): InteractAction {
return InteractAction.NONE
}
var isRemote: Boolean = false var isRemote: Boolean = false
open fun tick() { open fun tick() {

View File

@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive import com.google.gson.JsonPrimitive
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
@ -34,6 +35,8 @@ import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.client.world.ClientWorld import ru.dbotthepony.kstarbound.client.world.ClientWorld
import ru.dbotthepony.kstarbound.defs.DamageSource import ru.dbotthepony.kstarbound.defs.DamageSource
import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.InteractAction
import ru.dbotthepony.kstarbound.defs.InteractRequest
import ru.dbotthepony.kstarbound.defs.`object`.ObjectType import ru.dbotthepony.kstarbound.defs.`object`.ObjectType
import ru.dbotthepony.kstarbound.defs.quest.QuestArcDescriptor import ru.dbotthepony.kstarbound.defs.quest.QuestArcDescriptor
import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamage
@ -265,6 +268,25 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : 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() { override fun invalidate() {
super.invalidate() super.invalidate()
drawablesCache.invalidate() drawablesCache.invalidate()