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 kotlinx.coroutines.future.await import ru.dbotthepony.kstarbound.io.StreamCodec import ru.dbotthepony.kommons.io.readUUID import ru.dbotthepony.kstarbound.io.readVector2d import ru.dbotthepony.kstarbound.io.readVector2f import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeStruct2d import ru.dbotthepony.kommons.io.writeStruct2f import ru.dbotthepony.kommons.io.writeUUID import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.network.syncher.legacyCodec import ru.dbotthepony.kstarbound.network.syncher.nativeCodec import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.server.world.ServerChunk import ru.dbotthepony.kstarbound.server.world.ServerWorld import java.io.DataInputStream import java.io.DataOutputStream import java.util.UUID import kotlin.math.PI import kotlin.math.roundToInt // original game has MVariant here // MVariant prepends InvalidValue to Variant<> template // Variant<> itself works with LookupTypeIndex // 0 is responsible for holding current template comparison check // typedef MVariant WarpAction; // -> Variant WarpAction // hence WarpToWorld has index 1, WarpToPlayer 2, WarpAlias 3 @JsonAdapter(SpawnTarget.Adapter::class) sealed class SpawnTarget { abstract fun write(stream: DataOutputStream, isLegacy: Boolean) abstract suspend fun resolve(world: ServerWorld): Vector2d? object Whatever : SpawnTarget() { override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeByte(0) } override suspend fun resolve(world: ServerWorld): Vector2d { return world.playerSpawnPosition } override fun toString(): String { return "Whatever" } } data class Entity(val id: String) : SpawnTarget() { override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeByte(1) stream.writeBinaryString(id) } override suspend fun resolve(world: ServerWorld): Vector2d? { return world.uniqueEntities[id]?.position } override fun toString(): String { return id } } data class Position(val position: Vector2d) : SpawnTarget() { override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeByte(2) if (isLegacy) { stream.writeStruct2f(position.toFloatVector()) } else { stream.writeStruct2d(position) } } override fun toString(): String { return "${position.x.roundToInt()}.${position.y.roundToInt()}" } override suspend fun resolve(world: ServerWorld): Vector2d { return position } } data class X(val position: Double) : SpawnTarget() { override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeByte(3) if (isLegacy) { stream.writeFloat(position.toFloat()) } else { stream.writeDouble(position) } } override fun toString(): String { return position.roundToInt().toString() } override suspend fun resolve(world: ServerWorld): Vector2d { val basePos = Vector2d(position, world.geometry.size.y * 0.5) val tickets = ArrayList() try { for (i in 0 until Globals.worldServer.playerSpaceStartMaximumTries) { val testPos = world.geometry.wrap(basePos + Vector2d.angle(world.random.nextDouble(PI * 2.0), i * Globals.worldServer.playerSpaceStartDistanceIncrement)) val testRect = AABB.withSide(testPos, Globals.worldServer.playerSpaceStartRegionSize.x, Globals.worldServer.playerSpaceStartRegionSize.y) tickets.addAll(world.permanentChunkTicket(testRect).await()) tickets.forEach { it.chunk.await() } if (!world.chunkMap.anyCellSatisfies(testRect) { x, y, cell -> cell.foreground.material.value.collisionKind.isSolidCollision }) return testPos } return basePos } finally { tickets.forEach { it.cancel() } } } } class Adapter : TypeAdapter() { override fun write(out: JsonWriter, value: SpawnTarget) { out.value(value.toString()) } override fun read(`in`: JsonReader): SpawnTarget { return parse(`in`.nextString()) } } companion object { private val position = Regex("\\d+.\\d+") private val positionX = Regex("\\d+") fun read(stream: DataInputStream, isLegacy: Boolean): SpawnTarget { return when (val type = stream.readUnsignedByte()) { 0 -> Whatever 1 -> Entity(stream.readInternedString()) 2 -> Position(if (isLegacy) stream.readVector2f().toDoubleVector() else stream.readVector2d()) 3 -> X(if (isLegacy) stream.readFloat().toDouble() else stream.readDouble()) else -> throw IllegalArgumentException("Unknown SpawnTarget type $type!") } } fun parse(value: String): SpawnTarget { val matchPos = position.matchEntire(value) if (matchPos != null) { val split = matchPos.groups[0]!!.value.split('.') return Position(Vector2d(split[0].toDouble(), split[1].toDouble())) } val matchX = positionX.matchEntire(value) if (matchX != null) { return X(matchX.groups[0]!!.value.toDouble()) } return Entity(value) } } } @JsonAdapter(WarpAction.Adapter::class) sealed class WarpAction { abstract fun write(stream: DataOutputStream, isLegacy: Boolean) abstract fun resolve(connection: ServerConnection): WorldID data class World(val worldID: WorldID, val target: SpawnTarget = SpawnTarget.Whatever) : WarpAction() { override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeByte(1) worldID.write(stream, isLegacy) target.write(stream, isLegacy) } override fun resolve(connection: ServerConnection): WorldID { return worldID } override fun toString(): String { if (target != SpawnTarget.Whatever) return "$worldID=$target" return "$worldID" } } data class Player(val uuid: UUID) : WarpAction() { override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeByte(2) stream.writeUUID(uuid) } override fun resolve(connection: ServerConnection): WorldID { if (connection.uuid == uuid) return connection.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() { override fun write(out: JsonWriter, value: WarpAction) { out.value(value.toString()) } override fun read(`in`: JsonReader): WarpAction { return parse(`in`.nextString()) } } companion object { fun read(stream: DataInputStream, isLegacy: Boolean): WarpAction { return when (val type = stream.readUnsignedByte()) { 1 -> World(WorldID.read(stream, isLegacy), SpawnTarget.read(stream, isLegacy)) 2 -> Player(stream.readUUID()) 3 -> { when (val type2 = stream.readInt()) { 0 -> WarpAlias.Return 1 -> WarpAlias.OrbitedWorld 2 -> WarpAlias.OwnShip else -> throw IllegalArgumentException("Unknown WarpAlias type $type2!") } } else -> throw IllegalArgumentException("Unknown WarpAction type $type!") } } fun parse(value: String): WarpAction { if (value.lowercase() == "return") { return WarpAlias.Return } else if (value.lowercase() == "orbitedworld") { return WarpAlias.OrbitedWorld } else if (value.lowercase().startsWith("player:")) { return Player(UUID.fromString(value.substring(7))) } else { val parts = value.split('=') val world = WorldID.parse(parts[0]) var spawnTarget: SpawnTarget = SpawnTarget.Whatever if (parts.size == 2) { spawnTarget = SpawnTarget.parse(parts[1]) } return World(world, spawnTarget) } } val CODEC = nativeCodec(::read, WarpAction::write) val LEGACY_CODEC = legacyCodec(::read, WarpAction::write) } } sealed class WarpAlias(val index: Int) : WarpAction() { final override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.write(3) // because it is defined as enum class WarpAlias, without specifying uint8_t as type stream.writeInt(index) } abstract fun remap(connection: ServerConnection): WarpAction final override fun resolve(connection: ServerConnection): WorldID { throw RuntimeException("Trying to use WarpAlias as regular warp action") } object Return : WarpAlias(0) { override fun remap(connection: ServerConnection): WarpAction { return connection.returnWarp ?: World(connection.shipWorld.worldID) } override fun toString(): String { return "Return" } } object OrbitedWorld : WarpAlias(1) { override fun remap(connection: ServerConnection): WarpAction { return connection.orbitalWarpAction.orNull()?.first ?: World(connection.shipWorld.worldID) } override fun toString(): String { return "OrbitedWorld" } } object OwnShip : WarpAlias(2) { override fun remap(connection: ServerConnection): WarpAction { return World(connection.shipWorld.worldID) } override fun toString(): String { return "OwnShip" } } } enum class WarpMode(override val jsonName: String) : IStringSerializable { NONE("None"), BEAM_ONLY("BeamOnly"), DEPLOY_ONLY("DeployOnly"), BEAM_OR_DEPLOY("BeamOrDeploy"); companion object { val CODEC = StreamCodec.Enum(WarpMode::class.java) } }