diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 95e0e378..73fad6a4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -103,7 +103,7 @@ fun main() { Starbound.initializeGame() Starbound.mailboxInitialized.submit { - val server = IntegratedStarboundServer(File("./")) + val server = IntegratedStarboundServer(client, File("./")) val world = ServerWorld.load(server, LegacyWorldStorage.file(db)).get() world.thread.start() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt index 0cbb5122..ff6dfc85 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt @@ -19,7 +19,6 @@ import ru.dbotthepony.kstarbound.network.packets.ProtocolRequestPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientDisconnectRequestPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldClientStateUpdatePacket import java.net.SocketAddress -import java.util.* // clientside part of connection class ClientConnection(val client: StarboundClient, type: ConnectionType) : Connection(ConnectionSide.CLIENT, type) { @@ -71,7 +70,7 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType) : Conn channel.write(ClientContextUpdatePacket(entries, KOptional(), KOptional())) } - val (data, new) = clientStateGroup.write(clientStateNetVersion) + val (data, new) = client2serverGroup.write(clientStateNetVersion) if (data.isNotEmpty()) channel.write(WorldClientStateUpdatePacket(data)) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt index 8095d51a..71c7039f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt @@ -1,7 +1,23 @@ package ru.dbotthepony.kstarbound.defs +import ru.dbotthepony.kommons.io.StreamCodec +import ru.dbotthepony.kommons.io.readUUID +import ru.dbotthepony.kommons.io.readVector2d +import ru.dbotthepony.kommons.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.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +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.ServerWorld import java.io.DataInputStream import java.io.DataOutputStream +import java.util.UUID // original game has MVariant here // MVariant prepends InvalidValue to Variant<> template @@ -11,35 +27,190 @@ import java.io.DataOutputStream // -> Variant WarpAction // hence WarpToWorld has index 1, WarpToPlayer 2, WarpAlias 3 -sealed class AbstractWarpTarget { +sealed class SpawnTarget { abstract fun write(stream: DataOutputStream, isLegacy: Boolean) + abstract fun resolve(world: ServerWorld): Vector2d? + + object Whatever : SpawnTarget() { + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(0) + } + + override fun resolve(world: ServerWorld): Vector2d { + return world.playerSpawnPosition + } + + override fun toString(): String { + return "SpawnTarget.SpawnTarget" + } + } + + data class Entity(val id: String) : SpawnTarget() { + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(1) + stream.writeBinaryString(id) + } + + override fun resolve(world: ServerWorld): Vector2d? { + return world.entities.values.firstOrNull { it.uniqueID == id }?.position + } + + override fun toString(): String { + return "SpawnTarget.Entity[$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 "SpawnTarget.Position[$position]" + } + + override 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 "SpawnTarget.X[$position]" + } + + override fun resolve(world: ServerWorld): Vector2d { + TODO("Not yet implemented") + } + } companion object { - fun read(stream: DataInputStream, isLegacy: Boolean): AbstractWarpTarget { - return when (stream.readUnsignedByte()) { - 3 -> { - when (stream.readInt()) { - 0 -> WarpAlias.Return - 1 -> WarpAlias.OrbitedWorld - 2 -> WarpAlias.OwnShip - else -> throw IllegalArgumentException() - } - } - - else -> throw IllegalArgumentException() + 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!") } } } } -sealed class WarpAlias(val index: Int) : AbstractWarpTarget() { +sealed class WarpAction { + abstract fun write(stream: DataOutputStream, isLegacy: Boolean) + abstract fun resolve(connection: ServerConnection): WorldID + + data class World(val worldID: WorldID, val target: SpawnTarget) : WarpAction() { + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(1) + worldID.write(stream, isLegacy) + target.write(stream, isLegacy) + } + + override fun resolve(connection: ServerConnection): WorldID { + TODO("Not yet implemented") + } + } + + 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 + } + } + + 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!") + } + } + + val CODEC = nativeCodec(::read, WarpAction::write) + val LEGACY_CODEC = legacyCodec(::read, WarpAction::write) + } +} + +sealed class WarpAlias(val index: Int) : WarpAction() { override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.write(3) // because it is defined as enum class WarpAlias, without specifying uint8_t as type stream.writeInt(index) } - object Return : WarpAlias(0) - object OrbitedWorld : WarpAlias(1) - object OwnShip : WarpAlias(2) + object Return : WarpAlias(0) { + override fun resolve(connection: ServerConnection): WorldID { + TODO("Not yet implemented") + } + + override fun toString(): String { + return "WarpAlias.Return" + } + } + + object OrbitedWorld : WarpAlias(1) { + override fun resolve(connection: ServerConnection): WorldID { + TODO("Not yet implemented") + } + + override fun toString(): String { + return "WarpAlias.OrbitedWorld" + } + } + + object OwnShip : WarpAlias(2) { + override fun resolve(connection: ServerConnection): WorldID { + return connection.shipWorld.worldID + } + + override fun toString(): String { + return "WarpAlias.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) + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldID.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldID.kt new file mode 100644 index 00000000..81d69f2f --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldID.kt @@ -0,0 +1,103 @@ +package ru.dbotthepony.kstarbound.defs + +import ru.dbotthepony.kommons.io.readUUID +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeUUID +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.network.syncher.legacyCodec +import ru.dbotthepony.kstarbound.network.syncher.nativeCodec +import ru.dbotthepony.kstarbound.world.UniversePos +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.UUID + +sealed class WorldID { + abstract fun write(stream: DataOutputStream, isLegacy: Boolean) + val isLimbo: Boolean get() = this is Limbo + + object Limbo : WorldID() { + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(0) + } + + override fun toString(): String { + return "WorldID.Limbo" + } + } + + data class Celestial(val pos: UniversePos) : WorldID() { + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(1) + pos.write(stream, isLegacy) + } + + override fun toString(): String { + return "WorldID.Celestial[$pos]" + } + } + + data class ShipWorld(val uuid: UUID) : WorldID() { + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(2) + stream.writeUUID(uuid) + } + + override fun toString(): String { + return "WorldID.ShipWorld[$uuid]" + } + } + + data class Instance(val name: String, val uuid: UUID? = null, val threatLevel: Double? = null) : WorldID() { + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(3) + stream.writeBinaryString(name) + + stream.writeBoolean(uuid != null) + if (uuid != null) stream.writeUUID(uuid) + + stream.writeBoolean(threatLevel != null) + if (threatLevel != null) { + if (isLegacy) { + stream.writeFloat(threatLevel.toFloat()) + } else { + stream.writeDouble(threatLevel) + } + } + } + + override fun toString(): String { + return "WorldID.Instance[$name, uuid=$uuid, threat level=$threatLevel]" + } + } + + companion object { + val CODEC = nativeCodec(::read, WorldID::write) + val LEGACY_CODEC = legacyCodec(::read, WorldID::write) + + fun read(stream: DataInputStream, isLegacy: Boolean): WorldID { + return when (val type = stream.readUnsignedByte()) { + 0 -> Limbo + 1 -> Celestial(UniversePos(stream, isLegacy)) + 2 -> ShipWorld(stream.readUUID()) + 3 -> { + val name = stream.readInternedString() + val uuid = if (stream.readBoolean()) stream.readUUID() else null + val level: Double? + + if (stream.readBoolean()) { + if (isLegacy) { + level = stream.readFloat().toDouble() + } else { + level = stream.readDouble() + } + } else { + level = null + } + + Instance(name, uuid, level) + } + else -> throw IllegalArgumentException("Unknown WorldID type $type!") + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/ShipUpgrades.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/ShipUpgrades.kt index b3e23563..7700d2ee 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/ShipUpgrades.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/ShipUpgrades.kt @@ -6,7 +6,10 @@ import ru.dbotthepony.kommons.io.readBinaryString import ru.dbotthepony.kommons.io.readCollection import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeCollection +import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.network.syncher.legacyCodec +import ru.dbotthepony.kstarbound.network.syncher.nativeCodec import java.io.DataInputStream import java.io.DataOutputStream @@ -25,7 +28,7 @@ data class ShipUpgrades( stream.readInt(), if (isLegacy) stream.readFloat().toDouble() else stream.readDouble(), stream.readInt(), - ImmutableSet.copyOf(stream.readCollection { readBinaryString() }) + ImmutableSet.copyOf(stream.readCollection { readInternedString() }) ) fun apply(upgrades: ShipUpgrades): ShipUpgrades { @@ -52,4 +55,9 @@ data class ShipUpgrades( stream.writeInt(shipSpeed) stream.writeCollection(capabilities) { writeBinaryString(it) } } + + companion object { + val CODEC = nativeCodec(::ShipUpgrades, ShipUpgrades::write) + val LEGACY_CODEC = legacyCodec(::ShipUpgrades, ShipUpgrades::write) + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt index c57c94c0..cae3d4d6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt @@ -10,16 +10,28 @@ import it.unimi.dsi.fastutil.ints.IntAVLTreeSet import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.io.StreamCodec import ru.dbotthepony.kommons.io.VarIntValueCodec +import ru.dbotthepony.kommons.io.koptional import ru.dbotthepony.kommons.util.AABBi +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.Vector2i import ru.dbotthepony.kstarbound.GlobalDefaults +import ru.dbotthepony.kstarbound.defs.WarpAction +import ru.dbotthepony.kstarbound.defs.EntityDamageTeam +import ru.dbotthepony.kstarbound.defs.WarpMode +import ru.dbotthepony.kstarbound.defs.WorldID +import ru.dbotthepony.kstarbound.defs.actor.player.ShipUpgrades import ru.dbotthepony.kstarbound.network.syncher.BasicNetworkedElement import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.MasterElement +import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean +import ru.dbotthepony.kstarbound.network.syncher.networkedData import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt import ru.dbotthepony.kstarbound.player.Avatar import ru.dbotthepony.kstarbound.server.ServerChannels +import ru.dbotthepony.kstarbound.world.UniversePos import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity import java.io.Closeable import kotlin.math.roundToInt @@ -150,24 +162,35 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : channel.close() } - val windowXMin = networkedSignedInt() - val windowYMin = networkedSignedInt() - val windowWidth = networkedSignedInt() - val windowHeight = networkedSignedInt() - val playerID = networkedSignedInt() + // global variables (per connection) + // clientside variables + val client2serverGroup = MasterElement(NetworkedGroup()) + var windowXMin by client2serverGroup.upstream.add(networkedSignedInt()) + var windowYMin by client2serverGroup.upstream.add(networkedSignedInt()) + var windowWidth by client2serverGroup.upstream.add(networkedSignedInt()) + var windowHeight by client2serverGroup.upstream.add(networkedSignedInt()) + var playerID by client2serverGroup.upstream.add(networkedSignedInt()) + + // serverside variables + val server2clientGroup = MasterElement(NetworkedGroup()) + var orbitalWarpAction by server2clientGroup.upstream.add(networkedData(KOptional(), warpActionCodec, legacyWarpActionCodec)) + var worldID by server2clientGroup.upstream.add(networkedData(WorldID.Limbo, WorldID.CODEC, WorldID.LEGACY_CODEC)) + var isAdmin by server2clientGroup.upstream.add(networkedBoolean()) + var team by server2clientGroup.upstream.add(networkedData(EntityDamageTeam(), EntityDamageTeam.CODEC, EntityDamageTeam.LEGACY_CODEC)) + var shipUpgrades by server2clientGroup.upstream.add(networkedData(ShipUpgrades(), ShipUpgrades.CODEC, ShipUpgrades.LEGACY_CODEC)) + var shipCoordinate by server2clientGroup.upstream.add(networkedData(UniversePos(), UniversePos.CODEC, UniversePos.LEGACY_CODEC)) var playerEntity: PlayerEntity? = null // holy shit val clientSpectatingEntities = BasicNetworkedElement(IntAVLTreeSet(), StreamCodec.Collection(VarIntValueCodec) { IntAVLTreeSet() }) - val clientStateGroup = MasterElement(NetworkedGroup(windowXMin, windowYMin, windowWidth, windowHeight, playerID, clientSpectatingEntities)) // in tiles fun trackingTileRegions(): List { val result = ArrayList() - var mins = Vector2i(windowXMin.get() - GlobalDefaults.client.windowMonitoringBorder, windowYMin.get() - GlobalDefaults.client.windowMonitoringBorder) - var maxs = Vector2i(windowWidth.get() + GlobalDefaults.client.windowMonitoringBorder, windowHeight.get() + GlobalDefaults.client.windowMonitoringBorder) + var mins = Vector2i(windowXMin - GlobalDefaults.client.windowMonitoringBorder, windowYMin - GlobalDefaults.client.windowMonitoringBorder) + var maxs = Vector2i(windowWidth + GlobalDefaults.client.windowMonitoringBorder, windowHeight + GlobalDefaults.client.windowMonitoringBorder) if (maxs.x - mins.x > 1000) { // holy shit @@ -215,6 +238,9 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : companion object { private val LOGGER = LogManager.getLogger() + private val warpActionCodec = StreamCodec.Pair(WarpAction.CODEC, WarpMode.CODEC).koptional() + private val legacyWarpActionCodec = StreamCodec.Pair(WarpAction.LEGACY_CODEC, WarpMode.CODEC).koptional() + val NIO_POOL by lazy { NioEventLoopGroup(1, ThreadFactoryBuilder().setDaemon(true).setNameFormat("Starbound Network IO %d").build()) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt index 3083252a..ecaf8b95 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt @@ -33,6 +33,7 @@ import ru.dbotthepony.kstarbound.network.packets.ProtocolResponsePacket import ru.dbotthepony.kstarbound.network.packets.StepUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.CentralStructureUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ChatReceivePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectFailurePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.FindUniqueEntityResponsePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileUpdatePacket @@ -48,6 +49,7 @@ import ru.dbotthepony.kstarbound.network.packets.serverbound.ChatSendPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientDisconnectRequestPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.DamageTileGroupPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.FindUniqueEntityPacket +import ru.dbotthepony.kstarbound.network.packets.serverbound.PlayerWarpPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldClientStateUpdatePacket import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldStartAcknowledgePacket import java.io.BufferedInputStream @@ -382,7 +384,7 @@ class PacketRegistry(val isLegacy: Boolean) { // Packets sent universe server -> universe client LEGACY.add(::ServerDisconnectPacket) // ServerDisconnect LEGACY.add(::ConnectSuccessPacket) // ConnectSuccess - LEGACY.skip("ConnectFailure") + LEGACY.add(::ConnectFailurePacket) LEGACY.add(::HandshakeChallengePacket) // HandshakeChallenge LEGACY.add(::ChatReceivePacket) LEGACY.add(::UniverseTimeUpdatePacket) @@ -396,7 +398,7 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.add(::ClientConnectPacket) // ClientConnect LEGACY.add(ClientDisconnectRequestPacket::read) LEGACY.add(::HandshakeResponsePacket) // HandshakeResponse - LEGACY.skip("PlayerWarp") + LEGACY.add(::PlayerWarpPacket) LEGACY.skip("FlyShip") LEGACY.add(::ChatSendPacket) LEGACY.skip("CelestialRequest") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityCreatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityCreatePacket.kt index ece54dc8..c4ffb34c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityCreatePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityCreatePacket.kt @@ -7,6 +7,7 @@ import ru.dbotthepony.kommons.io.readByteArray import ru.dbotthepony.kommons.io.readSignedVarInt import ru.dbotthepony.kommons.io.writeByteArray import ru.dbotthepony.kommons.io.writeSignedVarInt +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.ClientConnection import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.network.IClientPacket diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityUpdateSetPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityUpdateSetPacket.kt index 83c13e3a..645ebe8d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityUpdateSetPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityUpdateSetPacket.kt @@ -37,7 +37,7 @@ class EntityUpdateSetPacket(val forConnection: Int, val deltas: Int2ObjectMap Unit) = tracker?.enqueue(task) @@ -35,6 +41,8 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn lateinit var shipWorld: ServerWorld private set + var uuid: UUID? = null + init { connectionID = server.channels.nextConnectionID() @@ -69,15 +77,20 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn shipChunks.putAll(chunks) } + private var remoteVersion = 0L + override fun flush() { if (isConnected) { val entries = rpc.write() - if (entries != null || modifiedShipChunks.isNotEmpty()) { + if (entries != null || modifiedShipChunks.isNotEmpty() || server2clientGroup.upstream.hasChangedSince(remoteVersion)) { + val (data, version) = server2clientGroup.write(remoteVersion, isLegacy) + remoteVersion = version + channel.write(ClientContextUpdatePacket( entries ?: listOf(), KOptional(modifiedShipChunks.associateWith { shipChunks[it]!! }), - KOptional(ByteArrayList()))) + KOptional(data))) modifiedShipChunks.clear() } @@ -106,7 +119,59 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn } } - private var announcedDisconnect = false + private var warpingAllowed = false + private var pendingWarp: Pair? = null + private var currentWarpStatus: CompletableFuture<*>? = null + + fun enqueueWarp(destination: WarpAction, deploy: Boolean = false) { + pendingWarp = destination to deploy + } + + fun tick() { + if (!isConnected || !channel.isOpen) + return + + flush() + + if (currentWarpStatus?.isDone == true) + currentWarpStatus = null + + if (currentWarpStatus == null && warpingAllowed) { + val pendingWarp = pendingWarp + this.pendingWarp = null + + if (pendingWarp != null) { + val (request, deploy) = pendingWarp + + val resolve = request.resolve(this) + + if (resolve.isLimbo) { + send(PlayerWarpResultPacket(false, request, true)) + } else if (tracker?.world?.worldID == resolve) { + LOGGER.info("$this tried to warp into world they are already in.") + send(PlayerWarpResultPacket(true, request, false)) + } else { + val world = server.worlds[resolve] + + if (world == null) { + send(PlayerWarpResultPacket(false, request, false)) + } else { + currentWarpStatus = world.acceptClient(this).exceptionally { + send(PlayerWarpResultPacket(false, request, false)) + + if (world == shipWorld) { + disconnect("ShipWorld refused to accept its owner: $it") + } else { + enqueueWarp(WarpAlias.OwnShip) + } + } + } + } + } + } + } + + private var announcedDisconnect = true private fun announceDisconnect(reason: String) { if (!announcedDisconnect && nickname.isNotBlank()) { @@ -160,26 +225,19 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn private var countedTowardsPlayerCount = false override fun inGame() { + announcedDisconnect = false server.chat.systemMessage("Player '$nickname' connected") countedTowardsPlayerCount = true server.channels.incrementPlayerCount() - if (!isLegacy) { - server.playerInGame(this) - } else { + if (isLegacy) { LOGGER.info("Initializing ship world for $this") - ServerWorld.load(server, shipChunkSource).thenAccept { + ServerWorld.load(server, shipChunkSource, WorldID.ShipWorld(uuid!!)).thenAccept { shipWorld = it shipWorld.thread.start() - send(PlayerWarpResultPacket(true, WarpAlias.OwnShip, false)) - - //server.worlds.first().acceptPlayer(this) - - shipWorld.acceptClient(this).exceptionally { - LOGGER.error("Shipworld of $this rejected to accept its owner", it) - disconnect("Shipworld rejected player warp request: $it") - } + enqueueWarp(WarpAlias.OwnShip) + warpingAllowed = true }.exceptionally { LOGGER.error("Error while initializing shipworld for $this", it) disconnect("Error while initializing shipworld for player: $it") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt index bc7a8e2f..39656e4f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt @@ -5,6 +5,7 @@ import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.MailboxExecutorService import ru.dbotthepony.kstarbound.GlobalDefaults import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.WorldID import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket import ru.dbotthepony.kstarbound.server.world.ServerUniverse import ru.dbotthepony.kstarbound.server.world.ServerWorld @@ -14,6 +15,7 @@ import ru.dbotthepony.kstarbound.util.ExecutionSpinner import java.io.Closeable import java.io.File import java.util.UUID +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger @@ -28,11 +30,11 @@ sealed class StarboundServer(val root: File) : Closeable { } } - val worlds = CopyOnWriteArrayList() + val worlds = ConcurrentHashMap() val serverID = threadCounter.getAndIncrement() val mailbox = MailboxExecutorService().also { it.exceptionHandler = ExceptionLogger(LOGGER) } - val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TIMESTEP_NANOS) - val thread = Thread(spinner, "Starbound Server $serverID") + val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::tick, Starbound.TIMESTEP_NANOS) + val thread = Thread(spinner, "Server $serverID Thread") val universe = ServerUniverse() val chat = ChatHandler(this) @@ -57,7 +59,7 @@ sealed class StarboundServer(val root: File) : Closeable { actuallyClose() } - thread.isDaemon = this is IntegratedStarboundServer + // thread.isDaemon = this is IntegratedStarboundServer thread.start() } @@ -86,16 +88,26 @@ sealed class StarboundServer(val root: File) : Closeable { } } - fun playerInGame(player: ServerConnection) { - val world = worlds.first() - world.acceptClient(player) + fun clientByUUID(uuid: UUID): ServerConnection? { + return channels.connections.firstOrNull { it.uuid == uuid } } protected abstract fun close0() + protected abstract fun tick0() - private fun spin(): Boolean { + private fun tick(): Boolean { if (isClosed) return false - channels.connections.forEach { if (it.channel.isOpen) it.flush() } + + channels.connections.forEach { + try { + it.tick() + } catch (err: Throwable) { + LOGGER.error("Exception while ticking client connection", err) + it.disconnect("Exception while ticking client connection: $err") + } + } + + tick0() return !isClosed } @@ -104,7 +116,7 @@ sealed class StarboundServer(val root: File) : Closeable { isClosed = true channels.close() - worlds.forEach { it.close() } + worlds.values.forEach { it.close() } universe.close() close0() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index fad28244..c09c6461 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -9,6 +9,8 @@ import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.WarpAction +import ru.dbotthepony.kstarbound.defs.WorldID import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult import ru.dbotthepony.kstarbound.defs.world.WorldStructure @@ -16,6 +18,7 @@ import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.packets.StepUpdatePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPacket import ru.dbotthepony.kstarbound.server.StarboundServer import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.util.AssetPathStack @@ -42,30 +45,49 @@ class ServerWorld private constructor( val server: StarboundServer, template: WorldTemplate, val storage: WorldStorage, + val worldID: WorldID, ) : World(template) { init { if (server.isClosed) throw RuntimeException() - server.worlds.add(this) + if (server.worlds.containsKey(worldID)) + throw IllegalStateException("Duplicate world ID: $worldID") + + server.worlds[worldID] = this } val players = CopyOnWriteArrayList() - private fun doAcceptClient(client: ServerConnection) { + private fun doAcceptClient(client: ServerConnection, action: WarpAction?) { if (players.any { it.client == client }) throw IllegalStateException("$client is already in $this") + val start = if (action is WarpAction.Player) + players.firstOrNull { it.client.uuid == action.uuid }?.client?.playerEntity?.position + else if (action is WarpAction.World) + action.target.resolve(this) + else + playerSpawnPosition + + if (start == null) { + client.send(PlayerWarpResultPacket(false, action!!, true)) + throw IllegalStateException("Not a valid spawn target: $action") + } + + if (action != null) + client.send(PlayerWarpResultPacket(true, action, false)) + client.tracker?.remove() - players.add(ServerWorldTracker(this, client)) + players.add(ServerWorldTracker(this, client, start)) } - fun acceptClient(player: ServerConnection): CompletableFuture { + fun acceptClient(player: ServerConnection, action: WarpAction? = null): CompletableFuture { check(!isClosed.get()) { "$this is invalid" } unpause() try { - return CompletableFuture.supplyAsync(Supplier { doAcceptClient(player) }, mailbox).exceptionally { + return CompletableFuture.supplyAsync(Supplier { doAcceptClient(player, action) }, mailbox).exceptionally { LOGGER.error("Error while accepting new player into world", it) } } catch (err: RejectedExecutionException) { @@ -74,7 +96,7 @@ class ServerWorld private constructor( } val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TIMESTEP_NANOS) - val thread = Thread(spinner, "Starbound Server World Thread") + val thread = Thread(spinner, "Server World $worldID") val ticketListLock = ReentrantLock() private val isClosed = AtomicBoolean() @@ -91,6 +113,13 @@ class ServerWorld private constructor( if (!isClosed.get()) spinner.unpause() } + override fun toString(): String { + if (isClosed.get()) + return "NULL ServerWorld at $worldID" + else + return "ServerWorld at $worldID" + } + override fun close() { if (isClosed.compareAndSet(false, true)) { LOGGER.info("Shutting down $this") @@ -98,7 +127,7 @@ class ServerWorld private constructor( super.close() spinner.unpause() players.forEach { it.remove() } - server.worlds.remove(this) + server.worlds.remove(worldID) LockSupport.unpark(thread) } } @@ -412,20 +441,20 @@ class ServerWorld private constructor( companion object { private val LOGGER = LogManager.getLogger() - fun create(server: StarboundServer, template: WorldTemplate, storage: WorldStorage): ServerWorld { - return ServerWorld(server, template, storage) + fun create(server: StarboundServer, template: WorldTemplate, storage: WorldStorage, worldID: WorldID = WorldID.Limbo): ServerWorld { + return ServerWorld(server, template, storage, worldID) } - fun create(server: StarboundServer, geometry: WorldGeometry, storage: WorldStorage): ServerWorld { - return create(server, WorldTemplate(geometry), storage) + fun create(server: StarboundServer, geometry: WorldGeometry, storage: WorldStorage, worldID: WorldID = WorldID.Limbo): ServerWorld { + return ServerWorld(server, WorldTemplate(geometry), storage, worldID) } - fun load(server: StarboundServer, storage: WorldStorage): CompletableFuture { + fun load(server: StarboundServer, storage: WorldStorage, worldID: WorldID = WorldID.Limbo): CompletableFuture { return storage.loadMetadata().thenApply { AssetPathStack("/") { _ -> val meta = it.map { Starbound.gson.fromJson(it.data.content, MetadataJson::class.java) }.orThrow { NoSuchElementException("No world metadata is present") } - val world = create(server, WorldTemplate.fromJson(meta.worldTemplate), storage) + val world = ServerWorld(server, WorldTemplate.fromJson(meta.worldTemplate), storage, worldID) world.playerSpawnPosition = meta.playerStart world.respawnInWorld = meta.respawnInWorld world.adjustPlayerSpawn = meta.adjustPlayerStart diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt index 476e05fa..2bf85ba4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt @@ -6,6 +6,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMaps import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet +import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket @@ -31,7 +32,7 @@ import java.util.concurrent.atomic.AtomicBoolean // couples ServerWorld and ServerConnection together, // allowing ServerConnection client to track ServerWorld state -class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection) { +class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, playerStart: Vector2d) { init { client.worldStartAcknowledged = false client.tracker = this @@ -82,7 +83,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection) { templateData = Starbound.writeLegacyJson { world.template.toJson() }, skyData = skyData.toByteArray(), weatherData = ByteArray(0), - playerStart = world.playerSpawnPosition, + playerStart = playerStart, playerRespawn = world.playerSpawnPosition, respawnInWorld = world.respawnInWorld, dungeonGravity = mapOf(), @@ -121,7 +122,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection) { } } - client.playerEntity = world.entities[client.playerID.get()] as? PlayerEntity + client.playerEntity = world.entities[client.playerID] as? PlayerEntity run { val newTrackedChunks = ObjectArraySet() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt index 5a13962e..be435890 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt @@ -12,8 +12,38 @@ import com.google.gson.stream.JsonWriter import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.vector.Vector3i import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.io.readVarInt +import ru.dbotthepony.kommons.io.readVector3i +import ru.dbotthepony.kommons.io.writeSignedVarInt +import ru.dbotthepony.kommons.io.writeStruct3i +import ru.dbotthepony.kommons.io.writeVarInt +import ru.dbotthepony.kstarbound.network.syncher.legacyCodec +import ru.dbotthepony.kstarbound.network.syncher.nativeCodec +import java.io.DataInputStream +import java.io.DataOutputStream +/** + * Specifies coordinates to either a planetary system, a planetary body, or a + * satellite around such a planetary body. The terms here are meant to be very + * generic, a "planetary body" could be an asteroid field, or a ship, or + * anything in orbit around the center of mass of a specific planetary system. + * The terms are really simply meant as a hierarchy of orbits. + * + * No validity checking is done here, any coordinate to any body whether it + * exists in a specific universe or not can be expressed. + */ data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit: Int = 0, val satelliteOrbit: Int = 0) { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readVector3i(), if (isLegacy) stream.readInt() else stream.readVarInt(), if (isLegacy) stream.readInt() else stream.readVarInt()) + + init { + require(planetOrbit >= 0) { "Negative planetOrbit: $planetOrbit" } + require(satelliteOrbit >= 0) { "Negative satelliteOrbit: $satelliteOrbit" } + } + + override fun toString(): String { + return "UniversePos[$location, planetOrbit=$planetOrbit, satelliteOrbit=$satelliteOrbit]" + } + val isSystem: Boolean get() = planetOrbit == 0 @@ -53,7 +83,22 @@ data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit: return this } + fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeStruct3i(location) + + if (isLegacy) { + stream.writeInt(planetOrbit) + stream.writeInt(satelliteOrbit) + } else { + stream.writeVarInt(planetOrbit) + stream.writeVarInt(satelliteOrbit) + } + } + companion object : TypeAdapterFactory { + val CODEC = nativeCodec(::UniversePos, UniversePos::write) + val LEGACY_CODEC = legacyCodec(::UniversePos, UniversePos::write) + private val splitter = Regex("[ _:]") val ZERO = UniversePos()