Player warping test code

This commit is contained in:
DBotThePony 2024-03-29 17:03:39 +07:00
parent f2c2db2e9d
commit 21a13134a4
Signed by: DBot
GPG Key ID: DCC23B5715498507
20 changed files with 580 additions and 89 deletions

View File

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

View File

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

View File

@ -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<InvalidType, WarpToWorld, WarpToPlayer, WarpAlias> 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)
}
}

View File

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

View File

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

View File

@ -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<AABBi> {
val result = ArrayList<AABBi>()
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())
}

View File

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

View File

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

View File

@ -37,7 +37,7 @@ class EntityUpdateSetPacket(val forConnection: Int, val deltas: Int2ObjectMap<By
connection.disconnect("Updating entity with ID $id outside of allowed range ${connection.entityIDRange}")
break
} else {
entities[id]?.networkGroup?.read(delta, Starbound.TIMESTEP, connection.isLegacy)
entities[id]?.networkGroup?.read(delta, Starbound.TIMESTEP * 3.0, connection.isLegacy)
}
}
}

View File

@ -28,8 +28,7 @@ object PingPacket : IServerPacket {
}
override fun play(connection: ServerConnection) {
// immediately respond to ping packets
connection.sendAndFlush(PongPacket)
connection.send(PongPacket)
}
fun read(stream: DataInputStream, isLegacy: Boolean): PingPacket {

View File

@ -1,13 +1,13 @@
package ru.dbotthepony.kstarbound.network.packets.clientbound
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.defs.AbstractWarpTarget
import ru.dbotthepony.kstarbound.defs.WarpAction
import ru.dbotthepony.kstarbound.network.IClientPacket
import java.io.DataInputStream
import java.io.DataOutputStream
class PlayerWarpResultPacket(val success: Boolean, val target: AbstractWarpTarget, val warpActionInvalid: Boolean) : IClientPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readBoolean(), AbstractWarpTarget.read(stream, isLegacy), stream.readBoolean())
class PlayerWarpResultPacket(val success: Boolean, val target: WarpAction, val warpActionInvalid: Boolean) : IClientPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readBoolean(), WarpAction.read(stream, isLegacy), stream.readBoolean())
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeBoolean(success)

View File

@ -17,6 +17,7 @@ import ru.dbotthepony.kommons.io.writeUUID
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.defs.actor.player.ShipUpgrades
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectFailurePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectSuccessPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket
import ru.dbotthepony.kstarbound.server.ServerConnection
@ -57,15 +58,24 @@ data class ClientConnectPacket(
}
override fun play(connection: ServerConnection) {
LOGGER.info("Client connection request received from ${connection.channel.remoteAddress()}, Player $playerName/$playerUuid (account '$account')")
if (connection.server.clientByUUID(playerUuid) != null) {
connection.send(ConnectFailurePacket("Duplicate player UUID $playerUuid"))
LOGGER.warn("Unable to accept player $playerName/$playerUuid because such UUID is already taken")
connection.channel.flush()
connection.close()
} else {
LOGGER.info("Client connection request received from ${connection.channel.remoteAddress()}, Player $playerName/$playerUuid (account '$account')")
connection.nickname = connection.server.reserveNickname(playerName, "Player_${connection.connectionID}")
connection.nickname = connection.server.reserveNickname(playerName, "Player_${connection.connectionID}")
connection.shipUpgrades = shipUpgrades
connection.uuid = playerUuid
connection.receiveShipChunks(shipChunks)
connection.send(ConnectSuccessPacket(connection.connectionID, connection.server.serverUUID, connection.server.universe.baseInformation))
connection.send(UniverseTimeUpdatePacket(connection.server.universeClock.seconds))
connection.channel.flush()
connection.inGame()
connection.receiveShipChunks(shipChunks)
connection.send(ConnectSuccessPacket(connection.connectionID, connection.server.serverUUID, connection.server.universe.baseInformation))
connection.send(UniverseTimeUpdatePacket(connection.server.universeClock.seconds))
connection.channel.flush()
connection.inGame()
}
}
companion object {

View File

@ -0,0 +1,20 @@
package ru.dbotthepony.kstarbound.network.packets.serverbound
import ru.dbotthepony.kstarbound.defs.WarpAction
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import java.io.DataInputStream
import java.io.DataOutputStream
class PlayerWarpPacket(val action: WarpAction, val deploy: Boolean) : IServerPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(WarpAction.read(stream, isLegacy), stream.readBoolean())
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
action.write(stream, isLegacy)
stream.writeBoolean(deploy)
}
override fun play(connection: ServerConnection) {
connection.enqueueWarp(action, deploy)
}
}

View File

@ -18,6 +18,6 @@ class WorldClientStateUpdatePacket(val deltas: ByteArrayList) : IServerPacket {
}
override fun play(connection: ServerConnection) {
connection.clientStateGroup.read(deltas.elements(), 0, deltas.size)
connection.client2serverGroup.read(deltas.elements(), 0, deltas.size)
}
}

View File

@ -1,13 +1,20 @@
package ru.dbotthepony.kstarbound.server
import ru.dbotthepony.kstarbound.client.StarboundClient
import java.io.Closeable
import java.io.File
class IntegratedStarboundServer(root: File) : StarboundServer(root), Closeable {
class IntegratedStarboundServer(val client: StarboundClient, root: File) : StarboundServer(root), Closeable {
init {
channels.createLocalChannel()
}
override fun tick0() {
if (client.shouldTerminate) {
close()
}
}
override fun close0() {
}

View File

@ -2,12 +2,13 @@ package ru.dbotthepony.kstarbound.server
import com.google.gson.JsonObject
import io.netty.channel.ChannelHandlerContext
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.ByteKey
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.defs.WarpAction
import ru.dbotthepony.kstarbound.defs.WarpAlias
import ru.dbotthepony.kstarbound.defs.WorldID
import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.ConnectionSide
import ru.dbotthepony.kstarbound.network.ConnectionType
@ -20,14 +21,19 @@ import ru.dbotthepony.kstarbound.server.world.WorldStorage
import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import java.util.HashMap
import java.util.UUID
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentLinkedQueue
import kotlin.properties.Delegates
// serverside part of connection
class ServerConnection(val server: StarboundServer, type: ConnectionType) : Connection(ConnectionSide.SERVER, type) {
var world: ServerWorld? = null
var tracker: ServerWorldTracker? = null
var worldStartAcknowledged = false
val world: ServerWorld?
get() = tracker?.world
// packets which interact with world must be
// executed on world's thread
fun enqueue(task: ServerWorld.() -> Unit) = tracker?.enqueue(task)
@ -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<WarpAction, Boolean>? = 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")

View File

@ -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<ServerWorld>()
val worlds = ConcurrentHashMap<WorldID, ServerWorld>()
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()
}

View File

@ -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<ServerWorld, ServerChunk>(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<ServerWorldTracker>()
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<Unit> {
fun acceptClient(player: ServerConnection, action: WarpAction? = null): CompletableFuture<Unit> {
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<ServerWorld> {
fun load(server: StarboundServer, storage: WorldStorage, worldID: WorldID = WorldID.Limbo): CompletableFuture<ServerWorld> {
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

View File

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

View File

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