KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt
2024-08-10 23:40:26 +07:00

324 lines
9.5 KiB
Kotlin

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<MatchType, 0, FirstType, RestTypes...>
// 0 is responsible for holding current template comparison check
// typedef MVariant<WarpToWorld, WarpToPlayer, WarpAlias> WarpAction;
// -> Variant<InvalidType, WarpToWorld, WarpToPlayer, WarpAlias> 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<ServerChunk.ITicket>()
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<SpawnTarget>() {
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<WarpAction>() {
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)
}
}