From afc45aac92616308ba980b7a19662305dbff210c Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Wed, 20 Mar 2024 12:39:21 +0700 Subject: [PATCH] More legacy protocol implementation --- .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 6 +- .../kstarbound/client/ClientConnection.kt | 55 +++-- .../kstarbound/client/StarboundClient.kt | 12 +- .../network/packets/SpawnWorldObjectPacket.kt | 2 +- .../dbotthepony/kstarbound/defs/EntityType.kt | 14 +- .../kstarbound/defs/PlayerWarping.kt | 45 ++++ .../kstarbound/defs/world/WorldStructure.kt | 40 ++++ .../kstarbound/network/Connection.kt | 29 ++- .../kstarbound/network/PacketRegistry.kt | 211 ++++++++++-------- .../packets/ClientContextUpdatePacket.kt | 8 +- .../network/packets/EntityCreatePacket.kt | 14 ++ .../network/packets/ProtocolRequestPacket.kt | 15 +- .../network/packets/ProtocolResponsePacket.kt | 3 + .../CentralStructureUpdatePacket.kt | 28 +++ .../clientbound/ConnectFailurePacket.kt | 2 +- .../FindUniqueEntityResponsePacket.kt | 55 +++++ .../clientbound/PlayerWarpResultPacket.kt | 21 ++ .../packets/clientbound/ServerInfoPacket.kt | 19 ++ .../clientbound/SetPlayerStartPacket.kt | 30 +++ .../serverbound/FindUniqueEntityPacket.kt | 24 ++ .../WorldStartAcknowledgePacket.kt | 21 ++ .../kstarbound/server/ServerChannels.kt | 20 +- .../kstarbound/server/ServerConnection.kt | 59 ++++- .../kstarbound/server/ServerSettings.kt | 2 +- .../kstarbound/server/world/ServerWorld.kt | 85 +++---- .../dbotthepony/kstarbound/world/Direction.kt | 22 +- .../ru/dbotthepony/kstarbound/world/World.kt | 17 +- .../world/entities/AbstractEntity.kt | 6 +- .../world/entities/DynamicEntity.kt | 2 +- .../kstarbound/world/entities/TileEntity.kt | 2 +- 30 files changed, 672 insertions(+), 197 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldStructure.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/CentralStructureUpdatePacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/FindUniqueEntityResponsePacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/PlayerWarpResultPacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/ServerInfoPacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SetPlayerStartPacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/FindUniqueEntityPacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/WorldStartAcknowledgePacket.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index ab31722c..2b8df664 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -122,15 +122,15 @@ fun main() { val item = ItemEntity(Registries.items.keys.values.random().value) item.position = Vector2d(225.0 - i, 785.0) - item.spawn(world) + item.joinWorld(world) item.movement.velocity = Vector2d(rand.nextDouble() * 32.0 - 16.0, rand.nextDouble() * 32.0 - 16.0) item.mailbox.scheduleAtFixedRate({ item.movement.velocity += Vector2d(rand.nextDouble() * 32.0 - 16.0, rand.nextDouble() * 32.0 - 16.0) }, 1000 + rand.nextLong(-100, 100), 1000 + rand.nextLong(-100, 100), TimeUnit.MILLISECONDS) //item.movement.applyVelocity(Vector2d(rand.nextDouble() * 1000.0 - 500.0, rand.nextDouble() * 1000.0 - 500.0)) } - client.connectToLocalServer(server.channels.createLocalChannel(), UUID.randomUUID()) - //client.connectToRemoteServer(InetSocketAddress("127.0.0.1", 21025), UUID.randomUUID()) + client.connectToLocalServer(server.channels.createLocalChannel()) + //client.connectToRemoteServer(InetSocketAddress("127.0.0.1", 21025)) //client2.connectToLocalServer(server.channels.createLocalChannel(), UUID.randomUUID()) server.channels.createChannel(InetSocketAddress(21060)) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt index 45a40c02..0cbb5122 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt @@ -23,10 +23,20 @@ import java.util.* // clientside part of connection class ClientConnection(val client: StarboundClient, type: ConnectionType) : Connection(ConnectionSide.CLIENT, type) { - private fun sendHello() { - isLegacy = false - //sendAndFlush(ProtocolRequestPacket(Starbound.LEGACY_PROTOCOL_VERSION)) - sendAndFlush(ProtocolRequestPacket(Starbound.NATIVE_PROTOCOL_VERSION)) + private fun sendHello(asLegacy: Boolean = false) { + isLegacy = asLegacy + + if (asLegacy) { + channel.write(ProtocolRequestPacket(Starbound.LEGACY_PROTOCOL_VERSION)) + } else { + channel.write(ProtocolRequestPacket(Starbound.NATIVE_PROTOCOL_VERSION)) + } + + channel.flush() + } + + fun enqueue(task: StarboundClient.() -> Unit) { + client.mailbox.execute { task.invoke(client) } } override fun inGame() { @@ -54,7 +64,7 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType) : Conn private var clientStateNetVersion = 0L override fun flush() { - if (!pendingDisconnect) { + if (!pendingDisconnect && isConnected) { val entries = rpc.write() if (entries != null) { @@ -102,11 +112,24 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType) : Conn } } + fun bootstrap(address: SocketAddress = channel.remoteAddress(), asLegacy: Boolean = false) { + LOGGER.info("Trying to connect to remote server at $address with ${if (asLegacy) "legacy" else "native"} protocol") + + Bootstrap() + .group(NIO_POOL) + .channel(NioSocketChannel::class.java) + .handler(object : ChannelInitializer() { override fun initChannel(ch: Channel) { bind(ch) } }) + .connect(address) + .syncUninterruptibly() + + sendHello(asLegacy) + } + companion object { private val LOGGER = LogManager.getLogger() - fun connectToLocalServer(client: StarboundClient, address: LocalAddress, uuid: UUID): ClientConnection { - LOGGER.info("Trying to connect to local server at $address with Client UUID $uuid") + fun connectToLocalServer(client: StarboundClient, address: LocalAddress): ClientConnection { + LOGGER.info("Trying to connect to local server at $address") val connection = ClientConnection(client, ConnectionType.MEMORY) Bootstrap() @@ -121,23 +144,13 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType) : Conn return connection } - fun connectToLocalServer(client: StarboundClient, address: Channel, uuid: UUID): ClientConnection { - return connectToLocalServer(client, address.localAddress() as LocalAddress, uuid) + fun connectToLocalServer(client: StarboundClient, address: Channel): ClientConnection { + return connectToLocalServer(client, address.localAddress() as LocalAddress) } - fun connectToRemoteServer(client: StarboundClient, address: SocketAddress, uuid: UUID): ClientConnection { - LOGGER.info("Trying to connect to remote server at $address with Client UUID $uuid") + fun connectToRemoteServer(client: StarboundClient, address: SocketAddress): ClientConnection { val connection = ClientConnection(client, ConnectionType.NETWORK) - - Bootstrap() - .group(NIO_POOL) - .channel(NioSocketChannel::class.java) - .handler(object : ChannelInitializer() { override fun initChannel(ch: Channel) { connection.bind(ch) } }) - .connect(address) - .syncUninterruptibly() - - connection.sendHello() - + connection.bootstrap(address) return connection } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index ff7b36cb..ee862f97 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -166,19 +166,19 @@ class StarboundClient private constructor(val clientID: Int) : Closeable { var activeConnection: ClientConnection? = null private set - fun connectToLocalServer(client: StarboundClient, address: LocalAddress, uuid: UUID) { + fun connectToLocalServer(client: StarboundClient, address: LocalAddress) { check(activeConnection == null) { "Already having active connection to server: $activeConnection" } - activeConnection = ClientConnection.connectToLocalServer(client, address, uuid) + activeConnection = ClientConnection.connectToLocalServer(client, address) } - fun connectToLocalServer(address: Channel, uuid: UUID) { + fun connectToLocalServer(address: Channel) { check(activeConnection == null) { "Already having active connection to server: $activeConnection" } - activeConnection = ClientConnection.connectToLocalServer(this, address, uuid) + activeConnection = ClientConnection.connectToLocalServer(this, address) } - fun connectToRemoteServer(address: SocketAddress, uuid: UUID) { + fun connectToRemoteServer(address: SocketAddress) { check(activeConnection == null) { "Already having active connection to server: $activeConnection" } - activeConnection = ClientConnection.connectToRemoteServer(this, address, uuid) + activeConnection = ClientConnection.connectToRemoteServer(this, address) } private val scissorStack = LinkedList() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/SpawnWorldObjectPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/SpawnWorldObjectPacket.kt index edd7c16a..e37be13f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/SpawnWorldObjectPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/SpawnWorldObjectPacket.kt @@ -25,7 +25,7 @@ class SpawnWorldObjectPacket(val uuid: UUID, val data: JsonObject) : IClientPack val world = connection.client.world ?: return@execute val obj = WorldObject.fromJson(data) obj.uuid = uuid - obj.spawn(world) + obj.joinWorld(world) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt index 30471685..44d6ab78 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt @@ -4,16 +4,16 @@ import com.google.gson.stream.JsonWriter import ru.dbotthepony.kstarbound.json.builder.IStringSerializable enum class EntityType(val jsonName: String) : IStringSerializable { - PLAYER("PlayerEntity"), - MONSTER("MonsterEntity"), - OBJECT("ObjectEntity"), - ITEM_DROP("ItemDropEntity"), - PROJECTILE("ProjectileEntity"), PLANT("PlantEntity"), + OBJECT("ObjectEntity"), + VEHICLE("VehicleEntity"), + ITEM_DROP("ItemDropEntity"), PLANT_DROP("PlantDropEntity"), // wat - NPC("NpcEntity"), + PROJECTILE("ProjectileEntity"), STAGEHAND("StagehandEntity"), - VEHICLE("VehicleEntity"); + MONSTER("MonsterEntity"), + NPC("NpcEntity"), + PLAYER("PlayerEntity"); override fun match(name: String): Boolean { return name == jsonName diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt new file mode 100644 index 00000000..913a06bd --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt @@ -0,0 +1,45 @@ +package ru.dbotthepony.kstarbound.defs + +import java.io.DataInputStream +import java.io.DataOutputStream + +// 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 + +sealed class AbstractWarpTarget { + abstract fun write(stream: DataOutputStream, isLegacy: Boolean) + + 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() + } + } + } +} + +sealed class WarpAlias(val index: Int) : AbstractWarpTarget() { + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.write(3) + // because it is defined as enum class WarpAlias, which defaults to int32_t for some reason. + stream.writeInt(index) + } + + object Return : WarpAlias(0) + object OrbitedWorld : WarpAlias(1) + object OwnShip : WarpAlias(2) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldStructure.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldStructure.kt new file mode 100644 index 00000000..bb9f2afa --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldStructure.kt @@ -0,0 +1,40 @@ +package ru.dbotthepony.kstarbound.defs.world + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import ru.dbotthepony.kommons.util.AABBi +import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.world.Direction + +@JsonFactory +data class WorldStructure( + val region: AABBi = AABBi(Vector2i.ZERO, Vector2i.ZERO), + val anchorPosition: Vector2i = Vector2i.ZERO, + val config: JsonElement = JsonNull.INSTANCE, + val backgroundOverlays: ImmutableList = ImmutableList.of(), + val foregroundOverlays: ImmutableList = ImmutableList.of(), + val backgroundBlocks: ImmutableList = ImmutableList.of(), + val foregroundBlocks: ImmutableList = ImmutableList.of(), + val objects: ImmutableList = ImmutableList.of(), + val flaggedBlocks: ImmutableMap> = ImmutableMap.of(), +) { + @JsonFactory + data class Overlay(val min: Vector2d, val image: String, val fullbright: Boolean) + + @JsonFactory + data class Block(val position: Vector2i, val materialId: Either, val residual: Boolean) + + @JsonFactory + data class Obj( + val position: Vector2i, + val name: String, + val direction: Direction, + val parameters: JsonElement, + val residual: Boolean = false, + ) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt index 6af7aa04..b7ed0205 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt @@ -15,9 +15,11 @@ import ru.dbotthepony.kstarbound.network.syncher.GroupElement import ru.dbotthepony.kstarbound.network.syncher.MasterElement import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt import ru.dbotthepony.kstarbound.player.Avatar +import ru.dbotthepony.kstarbound.server.ServerChannels import ru.dbotthepony.kstarbound.world.entities.PlayerEntity import java.io.Closeable import java.util.* +import kotlin.properties.Delegates abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : ChannelInboundHandlerAdapter(), Closeable { abstract override fun channelRead(ctx: ChannelHandlerContext, msg: Any) @@ -26,7 +28,16 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : var character: PlayerEntity? = null val rpc = JsonRPC() + var entityIDRange: IntRange by Delegates.notNull() + private set + var connectionID: Int = -1 + set(value) { + require(value in 1 .. ServerChannels.MAX_PLAYERS) { "Connection ID is out of range: $value" } + field = value + entityIDRange = value * -65536 .. 65535 + } + var nickname: String = "" val hasChannel get() = ::channel.isInitialized @@ -45,7 +56,11 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : private val legacyValidator = PacketRegistry.LEGACY.Validator(side) private val legacySerializer = PacketRegistry.LEGACY.Serializer(side) + var isConnected = false + private set + open fun setupLegacy() { + isConnected = true LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using legacy protocol") if (type == ConnectionType.MEMORY) { @@ -60,6 +75,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : } open fun setupNative() { + isConnected = true LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using native protocol") if (type == ConnectionType.MEMORY) { @@ -95,16 +111,25 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : } } + // one connection can be reused for second channel (while first is being closed) + override fun isSharable(): Boolean { + return true + } + + override fun ensureNotSharable() { + + } + abstract fun inGame() fun send(packet: IPacket) { - if (channel.isOpen) { + if (channel.isOpen && isConnected) { channel.write(packet) } } fun sendAndFlush(packet: IPacket) { - if (channel.isOpen) { + if (channel.isOpen && isConnected) { channel.write(packet) flush() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt index f0de6459..84b16985 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt @@ -31,21 +31,29 @@ import ru.dbotthepony.kstarbound.network.packets.serverbound.HandshakeResponsePa import ru.dbotthepony.kstarbound.network.packets.ProtocolRequestPacket 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.FindUniqueEntityResponsePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileUpdatePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerInfoPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStopPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.ChatSendPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientDisconnectRequestPacket +import ru.dbotthepony.kstarbound.network.packets.serverbound.FindUniqueEntityPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldClientStateUpdatePacket +import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldStartAcknowledgePacket import ru.dbotthepony.kstarbound.server.network.packets.TrackedPositionPacket import ru.dbotthepony.kstarbound.server.network.packets.TrackedSizePacket import java.io.BufferedInputStream import java.io.DataInputStream import java.io.DataOutputStream +import java.io.EOFException import java.io.FilterInputStream import java.io.InputStream import java.util.zip.Deflater @@ -132,89 +140,116 @@ class PacketRegistry(val isLegacy: Boolean) { } inner class Serializer(val side: ConnectionSide) : ChannelDuplexHandler() { - private val backlog = ByteArrayList() + private val packetReadBuffer = ByteArrayList() private var discardBytes = 0 private var readableBytes = 0 private var isCompressed = false private var readingType: Type<*>? = null + private val networkReadBuffer = ByteArrayList() + + override fun isSharable(): Boolean { + return true + } + + override fun ensureNotSharable() {} + + private fun drainNetworkBuffer(ctx: ChannelHandlerContext) { + while (networkReadBuffer.isNotEmpty()) { + if (discardBytes > 0) { + val toSkip = discardBytes.coerceAtMost(networkReadBuffer.size) + discardBytes -= toSkip + networkReadBuffer.removeElements(0, toSkip) + continue + } + + if (readingType != null) { + while (readableBytes > 0 && networkReadBuffer.isNotEmpty()) { + val toRead = readableBytes.coerceAtMost(networkReadBuffer.size) + packetReadBuffer.addElements(packetReadBuffer.size, networkReadBuffer.elements(), 0, toRead) + readableBytes -= toRead + networkReadBuffer.removeElements(0, toRead) + } + + if (readableBytes == 0) { + val stream: InputStream + + if (isCompressed) { + stream = BufferedInputStream(LimitingInputStream(InflaterInputStream(FastByteArrayInputStream(packetReadBuffer.elements(), 0, packetReadBuffer.size)))) + } else { + stream = FastByteArrayInputStream(packetReadBuffer.elements(), 0, packetReadBuffer.size) + } + + // legacy protocol allows to stitch multiple packets of same type together without + // separate headers for each + // Due to nature of netty pipeline, we can't do the same on native protocol; + // so don't do that when on native protocol + while (stream.available() > 0 || !isLegacy) { + try { + ctx.fireChannelRead(readingType!!.factory.read(DataInputStream(stream), isLegacy, side)) + } catch (err: Throwable) { + LOGGER.error("Error while reading incoming packet from network (type ${readingType!!.id}; ${readingType!!.type})", err) + } + + if (!isLegacy) break + } + + stream.close() + + packetReadBuffer.clear() + readingType = null + isCompressed = false + } + + continue + } + + val reader = FastByteArrayInputStream(networkReadBuffer.elements(), 0, networkReadBuffer.size) + + try { + val stream = DataInputStream(reader) + val packetType = stream.readUnsignedByte() + val dataLength = stream.readSignedVarInt() + + val type = packets.getOrNull(packetType) + + if (type == null) { + val name = missingNames[packetType] + + if (name != null) + LOGGER.error("Unknown packet type $packetType ($name)! Discarding ${dataLength.absoluteValue} bytes") + else + LOGGER.error("Unknown packet type $packetType! Discarding ${dataLength.absoluteValue} bytes") + + discardBytes = dataLength.absoluteValue + } else if (!type.direction.acceptedOn(side)) { + LOGGER.error("Packet type $packetType (${type.type}) can not be accepted on side $side! Discarding ${dataLength.absoluteValue} bytes") + discardBytes = dataLength.absoluteValue + } else if (dataLength.absoluteValue >= MAX_PACKET_SIZE) { + LOGGER.error("Packet ($packetType/${type.type}) of ${dataLength.absoluteValue} bytes is bigger than maximum allowed $MAX_PACKET_SIZE bytes") + discardBytes = dataLength.absoluteValue + } else { + if (LOG_PACKETS) LOGGER.debug("Packet type {} ({}) received on {} (size {} bytes)", packetType, type.type, side, dataLength.absoluteValue) + readingType = type + readableBytes = dataLength.absoluteValue + isCompressed = dataLength < 0 + } + + networkReadBuffer.removeElements(0, reader.position().toInt()) + } catch (err: EOFException) { + // Ignore EOF, since it is caused by segmented nature of TCP + } + } + } + override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { if (msg is ByteBuf) { try { - while (msg.readableBytes() > 0) { - if (discardBytes > 0) { - val toSkip = discardBytes.coerceAtMost(msg.readableBytes()) - discardBytes -= toSkip - msg.skipBytes(toSkip) - continue - } - - if (readingType != null) { - while (readableBytes > 0 && msg.readableBytes() > 0) { - backlog.add(msg.readByte()) - readableBytes-- - } - - if (readableBytes == 0) { - val stream: InputStream - - if (isCompressed) { - stream = BufferedInputStream(LimitingInputStream(InflaterInputStream(FastByteArrayInputStream(backlog.elements(), 0, backlog.size)))) - } else { - stream = FastByteArrayInputStream(backlog.elements(), 0, backlog.size) - } - - // legacy protocol allows to stitch multiple packets of same type together without - // separate headers for each - // Due to nature of netty pipeline, we can't do the same on native protocol; - // so don't do that when on native protocol - while (stream.available() > 0 || !isLegacy) { - try { - ctx.fireChannelRead(readingType!!.factory.read(DataInputStream(stream), isLegacy, side)) - } catch (err: Throwable) { - LOGGER.error("Error while reading incoming packet from network (type ${readingType!!.id}; ${readingType!!.type})", err) - } - - if (!isLegacy) break - } - - stream.close() - - backlog.clear() - readingType = null - isCompressed = false - } - - continue - } - - val stream = DataInputStream(ByteBufInputStream(msg)) - val packetType = stream.readUnsignedByte() - val dataLength = stream.readSignedVarInt() - - val type = packets.getOrNull(packetType) - - if (type == null) { - val name = missingNames[packetType] - - if (name != null) - LOGGER.error("Unknown packet type $packetType ($name)! Discarding ${dataLength.absoluteValue} bytes") - else - LOGGER.error("Unknown packet type $packetType! Discarding ${dataLength.absoluteValue} bytes") - - discardBytes = dataLength.absoluteValue - } else if (!type.direction.acceptedOn(side)) { - LOGGER.error("Packet type $packetType (${type.type}) can not be accepted on side $side! Discarding ${dataLength.absoluteValue} bytes") - discardBytes = dataLength.absoluteValue - } else if (dataLength.absoluteValue >= MAX_PACKET_SIZE) { - LOGGER.error("Packet ($packetType/${type.type}) of ${dataLength.absoluteValue} bytes is bigger than maximum allowed $MAX_PACKET_SIZE bytes") - discardBytes = dataLength.absoluteValue - } else { - // LOGGER.debug("Packet type {} ({}) received on {} (size {} bytes)", packetType, type.type, side, dataLength.absoluteValue) - readingType = type - readableBytes = dataLength.absoluteValue - isCompressed = dataLength < 0 - } + if (msg.readableBytes() > 0) { + val index = networkReadBuffer.size + networkReadBuffer.size(index + msg.readableBytes()) + msg.readBytes(networkReadBuffer.elements(), index, msg.readableBytes()) + drainNetworkBuffer(ctx) } } finally { msg.release() @@ -241,12 +276,12 @@ class PacketRegistry(val isLegacy: Boolean) { if (stream.length >= 512) { // compress val deflater = Deflater(3) - val buffers = ByteArrayList(1024) - val buffer = ByteArray(1024) + val buffers = ByteArrayList(4096) + val buffer = ByteArray(4096) deflater.setInput(stream.array, 0, stream.length) deflater.finish() - while (!deflater.needsInput()) { + while (!deflater.finished()) { val deflated = deflater.deflate(buffer) if (deflated > 0) @@ -260,7 +295,7 @@ class PacketRegistry(val isLegacy: Boolean) { stream2.writeByte(type.id) stream2.writeSignedVarInt(-buffers.size) stream2.write(buffers.elements(), 0, buffers.size) - // LOGGER.debug("Packet type {} ({}) sent from {} (size {} bytes / COMPRESSED size {} bytes)", type.id, type.type, side, stream.length, buffers.size) + if (LOG_PACKETS) LOGGER.debug("Packet type {} ({}) sent from {} (size {} bytes / COMPRESSED size {} bytes)", type.id, type.type, side, stream.length, buffers.size) ctx.write(buff, promise) } else { // send as-is @@ -269,7 +304,7 @@ class PacketRegistry(val isLegacy: Boolean) { stream2.writeByte(type.id) stream2.writeSignedVarInt(stream.length) stream2.write(stream.array, 0, stream.length) - // LOGGER.debug("Packet type {} ({}) sent from {} (size {} bytes)", type.id, type.type, side, stream.length) + if (LOG_PACKETS) LOGGER.debug("Packet type {} ({}) sent from {} (size {} bytes)", type.id, type.type, side, stream.length) ctx.write(buff, promise) } } @@ -307,6 +342,7 @@ class PacketRegistry(val isLegacy: Boolean) { } companion object { + const val LOG_PACKETS = false const val MAX_PACKET_SIZE = 64L * 1024L * 1024L // 64 MiB // this includes both compressed and uncompressed // Original game allows 16 mebibyte packets @@ -328,6 +364,7 @@ class PacketRegistry(val isLegacy: Boolean) { NATIVE.add(::SpawnWorldObjectPacket) NATIVE.add(::ForgetEntityPacket) NATIVE.add(::UniverseTimeUpdatePacket) + NATIVE.add(::StepUpdatePacket) HANDSHAKE.add(::ProtocolRequestPacket) HANDSHAKE.add(::ProtocolResponsePacket) @@ -353,10 +390,10 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.add(::ChatReceivePacket) LEGACY.add(::UniverseTimeUpdatePacket) LEGACY.skip("CelestialResponse") - LEGACY.skip("PlayerWarpResult") + LEGACY.add(::PlayerWarpResultPacket) LEGACY.skip("PlanetTypeUpdate") LEGACY.skip("Pause") - LEGACY.skip("ServerInfo") + LEGACY.add(::ServerInfoPacket) // Packets sent universe client -> universe server LEGACY.add(::ClientConnectPacket) // ClientConnect @@ -376,7 +413,7 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.add(::WorldStopPacket) LEGACY.skip("WorldLayoutUpdate") LEGACY.skip("WorldParametersUpdate") - LEGACY.skip("CentralStructureUpdate") + LEGACY.add(::CentralStructureUpdatePacket) LEGACY.add(LegacyTileArrayUpdatePacket::read) LEGACY.add(LegacyTileUpdatePacket::read) LEGACY.skip("TileLiquidUpdate") @@ -387,8 +424,8 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.skip("UpdateTileProtection") LEGACY.skip("SetDungeonGravity") LEGACY.skip("SetDungeonBreathable") - LEGACY.skip("SetPlayerStart") - LEGACY.skip("FindUniqueEntityResponse") + LEGACY.add(::SetPlayerStartPacket) + LEGACY.add(FindUniqueEntityResponsePacket::read) LEGACY.add(PongPacket::read) // Packets sent world client -> world server @@ -400,8 +437,8 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.skip("ConnectWire") LEGACY.skip("DisconnectAllWires") LEGACY.add(::WorldClientStateUpdatePacket) - LEGACY.skip("FindUniqueEntity") - LEGACY.skip("WorldStartAcknowledge") + LEGACY.add(::FindUniqueEntityPacket) + LEGACY.add(WorldStartAcknowledgePacket::read) LEGACY.add(PingPacket::read) // Packets sent bidirectionally between world client and world server diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ClientContextUpdatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ClientContextUpdatePacket.kt index 3fea8ba6..68d9a737 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ClientContextUpdatePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ClientContextUpdatePacket.kt @@ -97,11 +97,13 @@ class ClientContextUpdatePacket( } else { val wrap = DataInputStream(FastByteArrayInputStream(stream.readByteArray())) val rpc = wrap.readByteArray() + val shipChunks = wrap.readByteArray() + val networkedVars = wrap.readByteArray() return ClientContextUpdatePacket( - DataInputStream(FastByteArrayInputStream(rpc)).readCollection { JsonRPC.Entry.legacy(this) }, - if (wrap.available() > 0) KOptional(wrap.readMap({ readByteKey() }, { readKOptional { readByteArray() } })) else KOptional(), - if (wrap.available() > 0) KOptional(ByteArrayList.wrap(wrap.readByteArray())) else KOptional(), + if (rpc.isEmpty()) listOf() else DataInputStream(FastByteArrayInputStream(rpc)).readCollection { JsonRPC.Entry.legacy(this) }, + KOptional(if (shipChunks.isEmpty()) mapOf() else DataInputStream(FastByteArrayInputStream(shipChunks)).readMap({ readByteKey() }, { readKOptional { readByteArray() } })), + KOptional(ByteArrayList.wrap(networkedVars)), ) } } else { 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 8a28297b..a0d4848a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityCreatePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityCreatePacket.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.network.packets +import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.io.readByteArray import ru.dbotthepony.kommons.io.readSignedVarInt import ru.dbotthepony.kommons.io.writeByteArray @@ -28,10 +29,23 @@ class EntityCreatePacket(val entityType: EntityType, val storeData: ByteArray, v } override fun play(connection: ServerConnection) { + if (entityID !in connection.entityIDRange) { + LOGGER.error("Player $connection tried to create entity $entityType with ID $entityID, but that's outside of allowed range ${connection.entityIDRange}!") + } else { + connection.enqueue { + } + } + + println(entityType) + println(entityID) } override fun play(connection: ClientConnection) { TODO("Not yet implemented") } + + companion object { + private val LOGGER = LogManager.getLogger() + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ProtocolRequestPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ProtocolRequestPacket.kt index 5e967c78..6eb8b9a0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ProtocolRequestPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ProtocolRequestPacket.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.network.packets +import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.network.IServerPacket import ru.dbotthepony.kstarbound.server.ServerConnection @@ -15,14 +16,22 @@ data class ProtocolRequestPacket(val version: Int) : IServerPacket { override fun play(connection: ServerConnection) { if (version == Starbound.NATIVE_PROTOCOL_VERSION) { - connection.sendAndFlush(ProtocolResponsePacket(true)) + connection.channel.write(ProtocolResponsePacket(true)) + connection.channel.flush() connection.setupNative() } else if (version == Starbound.LEGACY_PROTOCOL_VERSION) { - connection.sendAndFlush(ProtocolResponsePacket(true)) + connection.channel.write(ProtocolResponsePacket(true)) + connection.channel.flush() connection.setupLegacy() } else { - connection.sendAndFlush(ProtocolResponsePacket(false)) + LOGGER.info("Unsupported protocol version on $connection, closing.") + connection.channel.write(ProtocolResponsePacket(false)) + connection.channel.flush() connection.close() } } + + companion object { + private val LOGGER = LogManager.getLogger() + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ProtocolResponsePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ProtocolResponsePacket.kt index d6b418d5..85548ecc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ProtocolResponsePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ProtocolResponsePacket.kt @@ -36,6 +36,9 @@ data class ProtocolResponsePacket(val allowed: Boolean) : IClientPacket { } else { connection.setupNative() } + } else if (!connection.isLegacy) { + connection.channel.close() + connection.bootstrap(asLegacy = true) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/CentralStructureUpdatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/CentralStructureUpdatePacket.kt new file mode 100644 index 00000000..e6a7a47d --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/CentralStructureUpdatePacket.kt @@ -0,0 +1,28 @@ +package ru.dbotthepony.kstarbound.network.packets.clientbound + +import com.google.gson.JsonElement +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.defs.world.WorldStructure +import ru.dbotthepony.kstarbound.fromJson +import ru.dbotthepony.kstarbound.json.readJsonElement +import ru.dbotthepony.kstarbound.json.writeJsonElement +import ru.dbotthepony.kstarbound.network.IClientPacket +import java.io.DataInputStream +import java.io.DataOutputStream + +class CentralStructureUpdatePacket(val data: JsonElement) : IClientPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readJsonElement()) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeJsonElement(data) + } + + override fun play(connection: ClientConnection) { + val read = Starbound.gson.fromJson(data, WorldStructure::class.java) + + connection.enqueue { + world?.centralStructure = read + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/ConnectFailurePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/ConnectFailurePacket.kt index f0ea0985..be4251db 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/ConnectFailurePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/ConnectFailurePacket.kt @@ -15,6 +15,6 @@ class ConnectFailurePacket(val reason: String = "") : IClientPacket { } override fun play(connection: ClientConnection) { - TODO("Not yet implemented") + connection.disconnectNow() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/FindUniqueEntityResponsePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/FindUniqueEntityResponsePacket.kt new file mode 100644 index 00000000..427c6602 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/FindUniqueEntityResponsePacket.kt @@ -0,0 +1,55 @@ +package ru.dbotthepony.kstarbound.network.packets.clientbound + +import ru.dbotthepony.kommons.io.readBinaryString +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.vector.Vector2d +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.network.IClientPacket +import java.io.DataInputStream +import java.io.DataOutputStream + +class FindUniqueEntityResponsePacket(val name: String, val position: Vector2d?) : IClientPacket { + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeBinaryString(name) + stream.writeBoolean(position != null) + + if (isLegacy) { + if (position != null) + stream.writeStruct2f(position.toFloatVector()) + } else { + if (position != null) + stream.writeStruct2d(position) + } + } + + override fun play(connection: ClientConnection) { + TODO("Not yet implemented") + } + + companion object { + fun read(stream: DataInputStream, isLegacy: Boolean): FindUniqueEntityResponsePacket { + val name = stream.readBinaryString() + val position: Vector2d? + + if (isLegacy) { + if (stream.readBoolean()) { + position = stream.readVector2f().toDoubleVector() + } else { + position = null + } + } else { + if (stream.readBoolean()) { + position = stream.readVector2d() + } else { + position = null + } + } + + return FindUniqueEntityResponsePacket(name, position) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/PlayerWarpResultPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/PlayerWarpResultPacket.kt new file mode 100644 index 00000000..8e8be23f --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/PlayerWarpResultPacket.kt @@ -0,0 +1,21 @@ +package ru.dbotthepony.kstarbound.network.packets.clientbound + +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.defs.AbstractWarpTarget +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()) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeBoolean(success) + target.write(stream, isLegacy) + stream.writeBoolean(warpActionInvalid) + } + + override fun play(connection: ClientConnection) { + TODO("Not yet implemented") + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/ServerInfoPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/ServerInfoPacket.kt new file mode 100644 index 00000000..f8bf530f --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/ServerInfoPacket.kt @@ -0,0 +1,19 @@ +package ru.dbotthepony.kstarbound.network.packets.clientbound + +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.network.IClientPacket +import java.io.DataInputStream +import java.io.DataOutputStream + +class ServerInfoPacket(val players: Int, val maxPlayers: Int) : IClientPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readUnsignedShort(), stream.readUnsignedShort()) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeShort(players) + stream.writeShort(maxPlayers) + } + + override fun play(connection: ClientConnection) { + TODO("Not yet implemented") + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SetPlayerStartPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SetPlayerStartPacket.kt new file mode 100644 index 00000000..941d85f3 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/SetPlayerStartPacket.kt @@ -0,0 +1,30 @@ +package ru.dbotthepony.kstarbound.network.packets.clientbound + +import ru.dbotthepony.kommons.io.readVector2d +import ru.dbotthepony.kommons.io.readVector2f +import ru.dbotthepony.kommons.io.writeStruct2d +import ru.dbotthepony.kommons.io.writeStruct2f +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.network.IClientPacket +import java.io.DataInputStream +import java.io.DataOutputStream + +class SetPlayerStartPacket(val position: Vector2d, val respawnInWorld: Boolean) : IClientPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(if (isLegacy) stream.readVector2f().toDoubleVector() else stream.readVector2d(), stream.readBoolean()) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + if (isLegacy) + stream.writeStruct2f(position.toFloatVector()) + else + stream.writeStruct2d(position) + + stream.writeBoolean(respawnInWorld) + } + + override fun play(connection: ClientConnection) { + connection.enqueue { + world?.setPlayerSpawn(position, respawnInWorld) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/FindUniqueEntityPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/FindUniqueEntityPacket.kt new file mode 100644 index 00000000..f0f8eff4 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/FindUniqueEntityPacket.kt @@ -0,0 +1,24 @@ +package ru.dbotthepony.kstarbound.network.packets.serverbound + +import ru.dbotthepony.kommons.io.readBinaryString +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kstarbound.network.IServerPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.FindUniqueEntityResponsePacket +import ru.dbotthepony.kstarbound.server.ServerConnection +import java.io.DataInputStream +import java.io.DataOutputStream + +class FindUniqueEntityPacket(val name: String) : IServerPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readBinaryString()) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeBinaryString(name) + } + + override fun play(connection: ServerConnection) { + connection.enqueue { + // Do something + connection.send(FindUniqueEntityResponsePacket(name, null)) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/WorldStartAcknowledgePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/WorldStartAcknowledgePacket.kt new file mode 100644 index 00000000..8c99016c --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/WorldStartAcknowledgePacket.kt @@ -0,0 +1,21 @@ +package ru.dbotthepony.kstarbound.network.packets.serverbound + +import ru.dbotthepony.kstarbound.network.IServerPacket +import ru.dbotthepony.kstarbound.server.ServerConnection +import java.io.DataInputStream +import java.io.DataOutputStream + +object WorldStartAcknowledgePacket : IServerPacket { + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + if (isLegacy) stream.writeBoolean(false) + } + + override fun play(connection: ServerConnection) { + connection.worldStartAcknowledged = true + } + + fun read(stream: DataInputStream, isLegacy: Boolean): WorldStartAcknowledgePacket { + if (isLegacy) stream.readBoolean() + return WorldStartAcknowledgePacket + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerChannels.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerChannels.kt index 10ed265c..e0d84419 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerChannels.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerChannels.kt @@ -12,10 +12,12 @@ import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.network.Connection import ru.dbotthepony.kstarbound.network.ConnectionType import ru.dbotthepony.kstarbound.network.IPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerInfoPacket import java.io.Closeable import java.net.SocketAddress import java.util.* import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -30,8 +32,21 @@ class ServerChannels(val server: StarboundServer) : Closeable { private val occupiedConnectionIDs = IntAVLTreeSet() private val connectionIDLock = Any() + private val playerCount = AtomicInteger() + + fun incrementPlayerCount() { + val new = playerCount.incrementAndGet() + broadcast(ServerInfoPacket(new, server.settings.maxPlayers)) + } + + fun decrementPlayerCount() { + val new = playerCount.decrementAndGet() + check(new >= 0) { "Player count turned negative" } + broadcast(ServerInfoPacket(new, server.settings.maxPlayers)) + } + private fun cycleConnectionID(): Int { - val v = ++nextConnectionID and 32767 + val v = ++nextConnectionID and MAX_PLAYERS if (v == 0) { nextConnectionID++ @@ -45,7 +60,7 @@ class ServerChannels(val server: StarboundServer) : Closeable { synchronized(connectionIDLock) { var i = 0 - while (i++ <= 32767) { // 32767 is the maximum + while (i++ <= MAX_PLAYERS) { // 32767 is the maximum val get = cycleConnectionID() if (!occupiedConnectionIDs.contains(get)) { @@ -153,5 +168,6 @@ class ServerChannels(val server: StarboundServer) : Closeable { companion object { private val LOGGER = LogManager.getLogger() + const val MAX_PLAYERS = 32767 } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt index 8ea3687a..7ebfff0c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt @@ -9,17 +9,20 @@ 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.kommons.util.MailboxExecutorService import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket import ru.dbotthepony.kstarbound.client.network.packets.ForgetEntityPacket import ru.dbotthepony.kstarbound.client.network.packets.SpawnWorldObjectPacket +import ru.dbotthepony.kstarbound.defs.WarpAlias import ru.dbotthepony.kstarbound.network.Connection import ru.dbotthepony.kstarbound.network.ConnectionSide import ru.dbotthepony.kstarbound.network.ConnectionType import ru.dbotthepony.kstarbound.network.IServerPacket import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket import ru.dbotthepony.kstarbound.server.world.WorldStorage import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage @@ -29,11 +32,22 @@ import ru.dbotthepony.kstarbound.world.IChunkListener import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.WorldObject +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 worldStartAcknowledged = false + + private val tasks = ConcurrentLinkedQueue Unit>() + + // packets which interact with world must be + // executed on world's thread + fun enqueue(task: ServerWorld.() -> Unit) { + tasks.add(task) + } + lateinit var shipWorld: ServerWorld private set @@ -107,7 +121,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn private inner class ChunkListener(val pos: ChunkPos) : IChunkListener { override fun onEntityAdded(entity: AbstractEntity) { - if (entity is WorldObject) + if (entity is WorldObject && !isLegacy) send(SpawnWorldObjectPacket(entity.uuid, entity.serialize())) } @@ -121,21 +135,24 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn } override fun flush() { - val entries = rpc.write() + if (isConnected) { + val entries = rpc.write() - if (entries != null || modifiedShipChunks.isNotEmpty()) { - channel.write(ClientContextUpdatePacket( - entries ?: listOf(), - KOptional(modifiedShipChunks.associateWith { shipChunks[it]!! }), - KOptional(ByteArrayList()))) + if (entries != null || modifiedShipChunks.isNotEmpty()) { + channel.write(ClientContextUpdatePacket( + entries ?: listOf(), + KOptional(modifiedShipChunks.associateWith { shipChunks[it]!! }), + KOptional(ByteArrayList()))) - modifiedShipChunks.clear() + modifiedShipChunks.clear() + } } super.flush() } fun onLeaveWorld() { + tasks.clear() tickets.values.forEach { it.cancel() } tickets.clear() pendingSend.clear() @@ -152,6 +169,11 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn if (::shipWorld.isInitialized) { shipWorld.close() } + + if (countedTowardsPlayerCount) { + countedTowardsPlayerCount = false + server.channels.decrementPlayerCount() + } } private var announcedDisconnect = false @@ -227,7 +249,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn } } - fun tick() { + fun tickWorld() { val world = world if (world == null) { @@ -235,6 +257,15 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn return } + run { + var next = tasks.poll() + + while (next != null) { + next.invoke(world) + next = tasks.poll() + } + } + if (needsToRecomputeTrackedChunks) { recomputeTrackedChunks() } @@ -268,8 +299,12 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn } } + private var countedTowardsPlayerCount = false + override fun inGame() { server.chat.systemMessage("Player '$nickname' connected") + countedTowardsPlayerCount = true + server.channels.incrementPlayerCount() if (!isLegacy) { server.playerInGame(this) @@ -279,7 +314,11 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn ServerWorld.load(server, shipChunkSource).thenAccept { shipWorld = it shipWorld.thread.start() - shipWorld.acceptPlayer(this) + send(PlayerWarpResultPacket(true, WarpAlias.OwnShip, false)) + shipWorld.acceptPlayer(this).exceptionally { + LOGGER.error("Shipworld of $this rejected to accept its owner", it) + disconnect("Shipworld rejected player warp request: $it") + } }.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/ServerSettings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerSettings.kt index 58b073e7..4c791c02 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerSettings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerSettings.kt @@ -5,7 +5,7 @@ import java.util.UUID @JsonBuilder class ServerSettings { - var maxPlayers = 8 + var maxPlayers = ServerChannels.MAX_PLAYERS var listenPort = 21025 fun from(other: ServerSettings) { 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 35b79ca7..d2942367 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -1,6 +1,5 @@ package ru.dbotthepony.kstarbound.server.world -import com.google.gson.JsonElement import com.google.gson.JsonObject import it.unimi.dsi.fastutil.ints.IntArraySet import it.unimi.dsi.fastutil.longs.Long2ObjectFunction @@ -10,10 +9,12 @@ import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket +import ru.dbotthepony.kstarbound.defs.world.WorldStructure import ru.dbotthepony.kstarbound.defs.world.WorldTemplate -import ru.dbotthepony.kstarbound.fromJson 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.CentralStructureUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket import ru.dbotthepony.kstarbound.server.StarboundServer import ru.dbotthepony.kstarbound.server.ServerConnection @@ -51,50 +52,50 @@ class ServerWorld private constructor( private val internalPlayers = CopyOnWriteArrayList() val players: List = Collections.unmodifiableList(internalPlayers) - private fun doAcceptPlayer(player: ServerConnection): Boolean { - if (player !in internalPlayers) { - internalPlayers.add(player) - player.onLeaveWorld() - player.world?.removePlayer(player) - player.world = this - player.trackedPosition = playerSpawnPosition + private fun doAcceptPlayer(player: ServerConnection) { + if (player in internalPlayers) + throw IllegalStateException("$player is already in $this") - if (player.isLegacy) { - val (skyData, skyVersion) = sky.networkedGroup.write(isLegacy = true) - player.skyVersion = skyVersion + internalPlayers.add(player) + player.onLeaveWorld() + player.world?.removePlayer(player) + player.world = this + player.worldStartAcknowledged = false + player.trackedPosition = playerSpawnPosition - player.sendAndFlush(WorldStartPacket( - templateData = template.toJson(true), - skyData = skyData.toByteArray(), - weatherData = ByteArray(0), - playerStart = playerSpawnPosition, - playerRespawn = playerSpawnPosition, - respawnInWorld = respawnInWorld, - dungeonGravity = mapOf(), - dungeonBreathable = mapOf(), - protectedDungeonIDs = protectedDungeonIDs, - worldProperties = properties.deepCopy(), - connectionID = player.connectionID, - localInterpolationMode = false, - )) - } else { - player.sendAndFlush(JoinWorldPacket(this)) - } + if (player.isLegacy) { + val (skyData, skyVersion) = sky.networkedGroup.write(isLegacy = true) + player.skyVersion = skyVersion - return true + player.send(WorldStartPacket( + templateData = template.toJson(true), + skyData = skyData.toByteArray(), + weatherData = ByteArray(0), + playerStart = playerSpawnPosition, + playerRespawn = playerSpawnPosition, + respawnInWorld = respawnInWorld, + dungeonGravity = mapOf(), + dungeonBreathable = mapOf(), + protectedDungeonIDs = protectedDungeonIDs, + worldProperties = properties.deepCopy(), + connectionID = player.connectionID, + localInterpolationMode = false, + )) + + player.sendAndFlush(CentralStructureUpdatePacket(Starbound.gson.toJsonTree(centralStructure))) + } else { + player.sendAndFlush(JoinWorldPacket(this)) } - - return false } - fun acceptPlayer(player: ServerConnection): CompletableFuture { + fun acceptPlayer(player: ServerConnection): CompletableFuture { check(!isClosed.get()) { "$this is invalid" } unpause() try { return CompletableFuture.supplyAsync(Supplier { doAcceptPlayer(player) }, mailbox) } catch (err: RejectedExecutionException) { - return CompletableFuture.completedFuture(false) + return CompletableFuture.failedFuture(err) } } @@ -169,7 +170,14 @@ class ServerWorld private constructor( } override fun thinkInner() { - internalPlayers.forEach { if (!isClosed.get()) it.tick() } + val packet = StepUpdatePacket(ticks) + + internalPlayers.forEach { + if (!isClosed.get() && it.worldStartAcknowledged && it.channel.isOpen) { + it.send(packet) + it.tickWorld() + } + } ticketListLock.withLock { ticketLists.removeIf { @@ -200,7 +208,7 @@ class ServerWorld private constructor( } } - fun broadcast(packet: IPacket) { + override fun broadcast(packet: IPacket) { internalPlayers.forEach { it.send(packet) } @@ -327,7 +335,7 @@ class ServerWorld private constructor( ents.ifPresent { for (obj in it) { - obj.spawn(this@ServerWorld) + obj.joinWorld(this@ServerWorld) } } }, mailbox) @@ -414,7 +422,7 @@ class ServerWorld private constructor( val respawnInWorld: Boolean, val adjustPlayerStart: Boolean, val worldTemplate: JsonObject, - val centralStructure: JsonElement, + val centralStructure: WorldStructure, val protectedDungeonIds: IntArraySet, val worldProperties: JsonObject, val spawningEnabled: Boolean @@ -439,6 +447,7 @@ class ServerWorld private constructor( world.playerSpawnPosition = meta.playerStart world.respawnInWorld = meta.respawnInWorld world.adjustPlayerSpawn = meta.adjustPlayerStart + world.centralStructure = meta.centralStructure world.protectedDungeonIDs.addAll(meta.protectedDungeonIds) world } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Direction.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Direction.kt index c16a2269..8325c102 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Direction.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Direction.kt @@ -1,11 +1,21 @@ package ru.dbotthepony.kstarbound.world +import com.google.gson.stream.JsonWriter import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable -enum class Direction(val normal: Vector2d) { - UP(Vector2d.POSITIVE_Y), - RIGHT(Vector2d.POSITIVE_X), - DOWN(Vector2d.NEGATIVE_Y), - LEFT(Vector2d.NEGATIVE_X), - NONE(Vector2d.ZERO); +enum class Direction(val normal: Vector2d, val jsonName: String) : IStringSerializable { + UP(Vector2d.POSITIVE_Y, "up"), + RIGHT(Vector2d.POSITIVE_X, "right"), + DOWN(Vector2d.NEGATIVE_Y, "down"), + LEFT(Vector2d.NEGATIVE_X, "left"), + NONE(Vector2d.ZERO, "any"); + + override fun match(name: String): Boolean { + return name.lowercase() == jsonName + } + + override fun write(out: JsonWriter) { + out.value(jsonName) + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index b46a12ff..4a779ce2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -12,8 +12,11 @@ import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.MailboxExecutorService import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.defs.world.WorldStructure import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.math.* +import ru.dbotthepony.kstarbound.network.IPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket import ru.dbotthepony.kstarbound.util.ParallelPerform import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.AbstractCell @@ -227,10 +230,20 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk, ChunkType : Chunk) { } + protected open fun onJoinWorld(world: World<*, *>) { } protected open fun onRemove(world: World<*, *>) { } /** * MUST be called by [World] itself */ - fun spawn(world: World<*, *>) { + fun joinWorld(world: World<*, *>) { if (innerWorld != null) throw IllegalStateException("Already spawned (in world $innerWorld)") @@ -82,7 +82,7 @@ abstract class AbstractEntity(path: String) : JsonDriven(path) { innerWorld = world world.entities.add(this) world.orphanedEntities.add(this) - onSpawn(world) + onJoinWorld(world) } fun remove() { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt index 1a813710..902c70d0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt @@ -40,7 +40,7 @@ abstract class DynamicEntity(path: String) : AbstractEntity(path) { final override var chunkPos: ChunkPos = ChunkPos.ZERO private set - override fun onSpawn(world: World<*, *>) { + override fun onJoinWorld(world: World<*, *>) { world.dynamicEntities.add(this) forceChunkRepos = true position = position diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/TileEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/TileEntity.kt index 1c77c2b6..d2e9cd93 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/TileEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/TileEntity.kt @@ -35,7 +35,7 @@ abstract class TileEntity(path: String) : AbstractEntity(path) { final override var chunkPos: ChunkPos = ChunkPos.ZERO private set - override fun onSpawn(world: World<*, *>) { + override fun onJoinWorld(world: World<*, *>) { world.tileEntities.add(this) forceChunkRepos = true position = position