Start implementing legacy protocol

This commit is contained in:
DBotThePony 2024-02-10 22:55:24 +07:00
parent 075b8259be
commit c2dc7c2e11
Signed by: DBot
GPG Key ID: DCC23B5715498507
29 changed files with 684 additions and 234 deletions

View File

@ -2,7 +2,7 @@ kotlin.code.style=official
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
kotlinVersion=1.9.0
kommonsVersion=2.1.1
kommonsVersion=2.1.5
ffiVersion=2.2.13
lwjglVersion=3.3.0

View File

@ -16,6 +16,8 @@ import java.io.BufferedInputStream
import java.io.ByteArrayInputStream
import java.io.DataInputStream
import java.io.File
import java.net.InetSocketAddress
import java.net.SocketAddress
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.zip.Inflater
@ -83,6 +85,7 @@ fun main() {
client.connectToLocalServer(server.channels.createLocalChannel(), UUID.randomUUID())
//client2.connectToLocalServer(server.channels.createLocalChannel(), UUID.randomUUID())
server.channels.createChannel(InetSocketAddress(21060))
}
//ent.position += Vector2d(y = 14.0, x = -10.0)

View File

@ -8,22 +8,20 @@ import io.netty.channel.local.LocalAddress
import io.netty.channel.local.LocalChannel
import io.netty.channel.socket.nio.NioSocketChannel
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.ConnectionSide
import ru.dbotthepony.kstarbound.network.ConnectionType
import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.packets.HelloListener
import ru.dbotthepony.kstarbound.network.packets.HelloPacket
import ru.dbotthepony.kstarbound.network.packets.ProtocolRequestPacket
import java.net.SocketAddress
import java.util.*
// client -> server
class ClientConnection(val client: StarboundClient, type: ConnectionType, uuid: UUID) : Connection(ConnectionSide.CLIENT, type, uuid) {
private fun sendHello() {
helloListener = HelloListener(this, channel!!).sendHello(localUUID)
}
override fun onHelloReceived(helloPacket: HelloPacket) {
isLegacy = true
sendAndFlush(ProtocolRequestPacket(Starbound.LEGACY_PROTOCOL_VERSION))
}
override fun inGame() {

View File

@ -945,7 +945,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
val activeConnection = activeConnection
activeConnection?.send(TrackedPositionPacket(camera.pos))
//activeConnection?.send(TrackedPositionPacket(camera.pos))
uberShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen }
fontShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen }

View File

@ -15,7 +15,7 @@ import java.io.DataInputStream
import java.io.DataOutputStream
class ChunkCellsPacket(val pos: ChunkPos, val data: List<ImmutableCell>) : IClientPacket {
constructor(stream: DataInputStream) : this(stream.readChunkPos(), stream.readCollection { MutableCell().read(stream).immutable() })
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readChunkPos(), stream.readCollection { MutableCell().read(stream).immutable() })
constructor(chunk: Chunk<*, *>) : this(chunk.pos, ArrayList<ImmutableCell>(CHUNK_SIZE * CHUNK_SIZE).also {
for (x in 0 until CHUNK_SIZE) {
for (y in 0 until CHUNK_SIZE) {
@ -24,7 +24,7 @@ class ChunkCellsPacket(val pos: ChunkPos, val data: List<ImmutableCell>) : IClie
}
})
override fun write(stream: DataOutputStream) {
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeVec2i(pos)
stream.writeCollection(data) { it.write(stream) }
}

View File

@ -9,9 +9,9 @@ import java.io.DataInputStream
import java.io.DataOutputStream
class ForgetChunkPacket(val pos: ChunkPos) : IClientPacket {
constructor(stream: DataInputStream) : this(stream.readChunkPos())
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readChunkPos())
override fun write(stream: DataOutputStream) {
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeVec2i(pos)
}

View File

@ -9,9 +9,9 @@ import java.io.DataOutputStream
import java.util.UUID
class ForgetEntityPacket(val uuid: UUID) : IClientPacket {
constructor(buff: DataInputStream) : this(buff.readUUID())
constructor(buff: DataInputStream, isLegacy: Boolean) : this(buff.readUUID())
override fun write(stream: DataOutputStream) {
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeUUID(uuid)
}

View File

@ -12,10 +12,10 @@ import java.io.DataOutputStream
import java.util.*
data class JoinWorldPacket(val uuid: UUID, val seed: Long, val geometry: WorldGeometry) : IClientPacket {
constructor(buff: DataInputStream) : this(buff.readUUID(), buff.readLong(), WorldGeometry(buff))
constructor(buff: DataInputStream, isLegacy: Boolean) : this(buff.readUUID(), buff.readLong(), WorldGeometry(buff))
constructor(world: World<*, *>) : this(UUID(0L, 0L), world.seed, world.geometry)
override fun write(stream: DataOutputStream) {
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeUUID(uuid)
stream.writeLong(seed)
geometry.write(stream)

View File

@ -5,7 +5,7 @@ import ru.dbotthepony.kstarbound.network.IClientPacket
import java.io.DataOutputStream
object LeaveWorldPacket : IClientPacket {
override fun write(stream: DataOutputStream) {
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
}

View File

@ -13,9 +13,9 @@ import java.io.DataOutputStream
import java.util.UUID
class SpawnWorldObjectPacket(val uuid: UUID, val data: JsonObject) : IClientPacket {
constructor(stream: DataInputStream) : this(stream.readUUID(), stream.readJsonObject())
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readUUID(), stream.readJsonObject())
override fun write(stream: DataOutputStream) {
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeUUID(uuid)
stream.writeJsonObject(data)
}

View File

@ -0,0 +1,33 @@
package ru.dbotthepony.kstarbound.defs
import ru.dbotthepony.kommons.io.writeStruct2i
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.io.readVec2i
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import java.io.DataInputStream
import java.io.DataOutputStream
@JsonFactory
data class CelestialBaseInformation(
val planetOrbitalLevels: Int = 1,
val satelliteOrbitalLevels: Int = 1,
val chunkSize: Int = 1,
val xyCoordRange: Vector2i = Vector2i.ZERO,
val zCoordRange: Vector2i = Vector2i.ZERO,
) {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(
stream.readInt(),
stream.readInt(),
stream.readInt(),
stream.readVec2i(),
stream.readVec2i(),
)
fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeInt(planetOrbitalLevels)
stream.writeInt(satelliteOrbitalLevels)
stream.writeInt(chunkSize)
stream.writeStruct2i(xyCoordRange)
stream.writeStruct2i(zCoordRange)
}
}

View File

@ -0,0 +1,59 @@
package ru.dbotthepony.kstarbound.defs.player
import com.google.common.collect.ImmutableSet
import ru.dbotthepony.kommons.collect.immutableSet
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.json.builder.JsonFactory
import java.io.DataInputStream
import java.io.DataOutputStream
@JsonFactory
data class ShipUpgrades(
val shipLevel: Int = 0,
val maxFuel: Int = 0,
val crewSize: Int = 0,
val fuelEfficiency: Double = 1.0,
val shipSpeed: Int = 0,
val capabilities: ImmutableSet<String> = ImmutableSet.of()
) {
fun apply(upgrades: ShipUpgrades): ShipUpgrades {
return ShipUpgrades(
shipLevel = shipLevel.coerceAtLeast(upgrades.shipLevel),
maxFuel = maxFuel.coerceAtLeast(upgrades.maxFuel),
crewSize = crewSize.coerceAtLeast(upgrades.crewSize),
fuelEfficiency = fuelEfficiency.coerceAtLeast(upgrades.fuelEfficiency),
shipSpeed = shipSpeed.coerceAtLeast(upgrades.shipSpeed),
capabilities = immutableSet { capabilities.forEach(::accept); upgrades.capabilities.forEach(::accept) }
)
}
fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeInt(shipLevel)
stream.writeInt(maxFuel)
stream.writeInt(crewSize)
if (isLegacy)
stream.writeFloat(fuelEfficiency.toFloat())
else
stream.writeDouble(fuelEfficiency)
stream.writeInt(shipSpeed)
stream.writeCollection(capabilities) { writeBinaryString(it) }
}
companion object {
fun read(stream: DataInputStream, isLegacy: Boolean): ShipUpgrades {
return ShipUpgrades(
stream.readInt(),
stream.readInt(),
stream.readInt(),
if (isLegacy) stream.readFloat().toDouble() else stream.readDouble(),
stream.readInt(),
ImmutableSet.copyOf(stream.readCollection { readBinaryString() })
)
}
}
}

View File

@ -0,0 +1,33 @@
package ru.dbotthepony.kstarbound.io
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.writeVarInt
import java.io.InputStream
import java.io.OutputStream
class ByteKey(private vararg val bytes: Byte) {
override fun equals(other: Any?): Boolean {
return this === other || other is ByteKey && other.bytes.contentEquals(bytes)
}
fun write(stream: OutputStream) {
stream.writeVarInt(bytes.size)
stream.write(bytes)
}
override fun hashCode(): Int {
return bytes.contentHashCode()
}
override fun toString(): String {
return "ByteKey[${bytes.joinToString(", ")}]"
}
}
fun InputStream.readByteKey(): ByteKey {
return ByteKey(*ByteArray(readVarInt()).also { read(it) })
}
fun OutputStream.writeByteKey(key: ByteKey) {
key.write(this)
}

View File

@ -76,11 +76,11 @@ enum class ConnectionType {
}
fun interface IPacketReader<T : IPacket> {
fun read(stream: DataInputStream): T
fun read(stream: DataInputStream, isLegacy: Boolean): T
}
interface IPacket {
fun write(stream: DataOutputStream)
fun write(stream: DataOutputStream, isLegacy: Boolean)
}
interface IServerPacket : IPacket {

View File

@ -7,111 +7,108 @@ import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.channel.ChannelOption
import io.netty.channel.nio.NioEventLoopGroup
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.network.packets.DisconnectPacket
import ru.dbotthepony.kstarbound.network.packets.HelloListener
import ru.dbotthepony.kstarbound.network.packets.HelloPacket
import ru.dbotthepony.kstarbound.player.Avatar
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, val localUUID: UUID) : ChannelInboundHandlerAdapter(), IConnectionDetails {
abstract class Connection(val side: ConnectionSide, val type: ConnectionType, val localUUID: UUID) : ChannelInboundHandlerAdapter(), Closeable {
abstract override fun channelRead(ctx: ChannelHandlerContext, msg: Any)
var avatar: Avatar? = null
var character: PlayerEntity? = null
protected var channel: Channel? = null
protected var otherSide: HelloPacket? = null
protected var helloListener: HelloListener? = null
var channel: Channel by Delegates.notNull()
protected set
override val protocolVersion: Int
get() = otherSide?.protocolVersion ?: 0
override val engineVersion: String
get() = otherSide?.engineVersion ?: "0.0.0"
override val username: String
get() = otherSide?.username ?: ""
override val password: String
get() = otherSide?.password ?: ""
override val uuid: UUID
get() = otherSide?.uuid ?: EMPTY_UUID
var isLegacy: Boolean = true
protected set
private val handshakeValidator = PacketRegistry.HANDSHAKE.Validator(side)
private val handshakeSerializer = PacketRegistry.HANDSHAKE.Serializer(side)
private val nativeValidator = PacketRegistry.NATIVE.Validator(side)
private val nativeSerializer = PacketRegistry.NATIVE.Serializer(side)
private val legacyValidator = PacketRegistry.LEGACY.Validator(side)
private val legacySerializer = PacketRegistry.LEGACY.Serializer(side)
fun setupLegacy() {
LOGGER.info("Handshake successful from ${channel.remoteAddress()}, channel is using legacy protocol")
if (type == ConnectionType.MEMORY) {
channel.pipeline().remove(handshakeValidator)
channel.pipeline().addFirst(legacyValidator)
} else {
channel.pipeline().remove(handshakeSerializer)
channel.pipeline().addFirst(legacySerializer)
}
isLegacy = true
}
fun setupNative() {
LOGGER.info("Handshake successful from ${channel.remoteAddress()}, channel is using native protocol")
if (type == ConnectionType.MEMORY) {
channel.pipeline().remove(handshakeValidator)
channel.pipeline().addFirst(nativeValidator)
} else {
channel.pipeline().remove(handshakeSerializer)
channel.pipeline().addFirst(nativeSerializer)
}
isLegacy = false
}
var disconnectionReason: String? = null
private set
fun bind(channel: Channel) {
check(this.channel == null) { "Already having channel bound" }
channel.config().setOption(ChannelOption.TCP_NODELAY, true)
this.channel = channel
if (side == ConnectionSide.SERVER)
helloListener = HelloListener(this, channel)
}
protected abstract fun onHelloReceived(helloPacket: HelloPacket)
protected abstract fun inGame()
fun helloReceived(helloPacket: HelloPacket) {
LOGGER.info("Handshake received on $side from ${channel?.remoteAddress()}: $helloPacket")
otherSide = helloPacket
if (side == ConnectionSide.SERVER) {
helloListener!!.sendHello(localUUID)
}
onHelloReceived(helloPacket)
initializeHandlers()
}
fun helloFailed() {
channel!!.close()
}
fun initializeHandlers() {
val channel = channel ?: throw IllegalStateException("No network channel is bound")
if (type == ConnectionType.NETWORK) {
channel.pipeline().addLast(PacketRegistry.NATIVE.Inbound(side))
} else {
channel.pipeline().addLast(PacketRegistry.NATIVE.InboundValidator(side))
}
if (type == ConnectionType.MEMORY)
channel.pipeline().addFirst(handshakeValidator)
else
channel.pipeline().addFirst(handshakeSerializer)
channel.pipeline().addLast(this)
if (type == ConnectionType.NETWORK) {
channel.pipeline().addLast(PacketRegistry.NATIVE.Outbound(side))
channel.pipeline().addFirst(DatagramEncoder)
channel.pipeline().addFirst(DatagramDecoder())
} else {
channel.pipeline().addLast(PacketRegistry.NATIVE.OutboundValidator(side))
channel.closeFuture().addListener {
LOGGER.info("Connection to ${channel.remoteAddress()} is closed")
}
inGame()
}
protected abstract fun inGame()
fun send(packet: IPacket) {
val channel = channel ?: throw IllegalStateException("No network channel is bound")
channel.write(packet)
}
fun sendAndFlush(packet: IPacket) {
val channel = channel ?: throw IllegalStateException("No network channel is bound")
channel.write(packet)
channel.flush()
}
fun flush() {
val channel = channel ?: throw IllegalStateException("No network channel is bound")
channel.flush()
}
fun disconnect(reason: String) {
if (side == ConnectionSide.CLIENT) {
channel!!.close()
channel.close()
} else {
channel!!.write(DisconnectPacket(reason))
channel!!.flush()
channel!!.close()
channel.flush()
channel.close()
}
}
override fun close() {
channel.close()
}
companion object {
private val EMPTY_UUID = UUID(0L, 0L)
private val LOGGER = LogManager.getLogger()

View File

@ -3,26 +3,40 @@ package ru.dbotthepony.kstarbound.network
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufInputStream
import io.netty.buffer.ByteBufOutputStream
import io.netty.channel.ChannelDuplexHandler
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.channel.ChannelOutboundHandlerAdapter
import io.netty.channel.ChannelPromise
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.readSignedVarInt
import ru.dbotthepony.kommons.io.writeSignedVarInt
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.JoinWorldPacket
import ru.dbotthepony.kstarbound.client.network.packets.SpawnWorldObjectPacket
import ru.dbotthepony.kstarbound.network.packets.DisconnectPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientConnectPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectSuccessPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.HandshakeChallengePacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.HandshakeResponsePacket
import ru.dbotthepony.kstarbound.network.packets.ProtocolRequestPacket
import ru.dbotthepony.kstarbound.network.packets.ProtocolResponsePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket
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.InputStream
import java.util.zip.InflaterInputStream
import kotlin.math.absoluteValue
import kotlin.reflect.KClass
class PacketRegistry {
private val packets = ArrayList<Type<*>>()
class PacketRegistry(val isLegacy: Boolean) {
private val packets = ArrayList<Type<*>?>()
private val clazz2Type = Reference2ObjectOpenHashMap<KClass<*>, Type<*>>()
private data class Type<T : IPacket>(val id: Int, val type: KClass<T>, val factory: IPacketReader<T>, val direction: PacketDirection)
@ -47,34 +61,110 @@ class PacketRegistry {
return add(T::class, reader, direction)
}
inner class Inbound(val side: ConnectionSide) : ChannelInboundHandlerAdapter() {
private fun skip(amount: Int = 1) {
for (i in 0 until amount) {
packets.add(null)
}
}
inner class Serializer(val side: ConnectionSide) : ChannelDuplexHandler() {
private val backlog = ByteArrayList()
private var discardBytes = 0
private var readableBytes = 0
private var isCompressed = false
private var readingType: Type<*>? = null
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if (msg is ByteBuf) {
val packetType = msg.readUnsignedByte().toInt()
val type = packets.getOrNull(packetType)
try {
while (msg.readableBytes() > 0) {
if (discardBytes > 0) {
val toSkip = discardBytes.coerceAtMost(msg.readableBytes())
discardBytes -= toSkip
msg.skipBytes(toSkip)
continue
}
if (type == null) {
LOGGER.error("Unknown packet type $packetType!")
msg.release()
} else if (!type.direction.acceptedOn(side)) {
LOGGER.error("Packet ${type.type} can not be accepted on side $side!")
msg.release()
} else {
try {
ctx.fireChannelRead(type.factory.read(DataInputStream(ByteBufInputStream(msg))))
} catch (err: Throwable) {
LOGGER.error("Error while reading incoming packet from network", err)
} finally {
msg.release()
if (readingType != null) {
while (readableBytes > 0 && msg.readableBytes() > 0) {
backlog.add(msg.readByte())
readableBytes--
}
if (readableBytes == 0) {
val stream: InputStream
if (isCompressed) {
stream = BufferedInputStream(InflaterInputStream(FastByteArrayInputStream(backlog.elements(), 0, backlog.size)))
} else {
stream = FastByteArrayInputStream(backlog.elements(), 0, backlog.size)
}
try {
ctx.fireChannelRead(readingType!!.factory.read(DataInputStream(stream), isLegacy))
} catch (err: Throwable) {
LOGGER.error("Error while reading incoming packet from network (type ${readingType!!.id}; ${readingType!!.type})", err)
}
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) {
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 {
readingType = type
readableBytes = dataLength.absoluteValue
isCompressed = dataLength < 0
}
}
} finally {
msg.release()
}
} else {
super.channelRead(ctx, msg)
}
}
override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise) {
val type = clazz2Type[msg::class]
if (type == null) {
LOGGER.error("Unknown outgoing message type ${msg::class}, it will not reach the other side.")
} else if (!type.direction.acceptedOn(side.opposite)) {
LOGGER.error("Packet ${type.type} can not be accepted on side ${side.opposite}, refusing to send it!")
} else {
val stream = FastByteArrayOutputStream()
(msg as IPacket).write(DataOutputStream(stream), isLegacy)
if (isLegacy)
check(stream.length > 0) { "Packet $msg didn't write any data to network, this is not allowed by legacy protocol" }
val buff = ctx.alloc().buffer(stream.length + 5)
val stream2 = ByteBufOutputStream(buff)
stream2.writeByte(type.id)
stream2.writeSignedVarInt(stream.length)
stream2.write(stream.array, 0, stream.length)
ctx.write(buff, promise)
}
}
}
inner class InboundValidator(val side: ConnectionSide) : ChannelInboundHandlerAdapter() {
inner class Validator(val side: ConnectionSide) : ChannelDuplexHandler() {
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
val type = clazz2Type[msg::class]
@ -90,26 +180,7 @@ class PacketRegistry {
}
}
}
}
inner class Outbound(val side: ConnectionSide) : ChannelOutboundHandlerAdapter() {
override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise) {
val type = clazz2Type[msg::class]
if (type == null) {
LOGGER.error("Unknown outgoing message type ${msg::class}, it will not reach the other side.")
} else if (!type.direction.acceptedOn(side.opposite)) {
LOGGER.error("Packet ${type.type} can not be accepted on side ${side.opposite}, refusing to send it!")
} else {
val buff = ctx.alloc().buffer(2048)
buff.writeByte(type.id)
(msg as IPacket).write(DataOutputStream(ByteBufOutputStream(buff)))
ctx.write(buff, promise)
}
}
}
inner class OutboundValidator(val side: ConnectionSide) : ChannelOutboundHandlerAdapter() {
override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise) {
val type = clazz2Type[msg::class]
@ -126,12 +197,11 @@ class PacketRegistry {
companion object {
private val LOGGER = LogManager.getLogger()
val NATIVE = PacketRegistry()
val LEGACY = PacketRegistry()
val HANDSHAKE = PacketRegistry()
val NATIVE = PacketRegistry(false)
val LEGACY = PacketRegistry(true)
val HANDSHAKE = PacketRegistry(false)
init {
NATIVE.add(::DisconnectPacket)
NATIVE.add(::JoinWorldPacket)
NATIVE.add(::ChunkCellsPacket)
NATIVE.add(::ForgetChunkPacket)
@ -139,6 +209,96 @@ class PacketRegistry {
NATIVE.add(::TrackedSizePacket)
NATIVE.add(::SpawnWorldObjectPacket)
NATIVE.add(::ForgetEntityPacket)
HANDSHAKE.add(::ProtocolRequestPacket)
HANDSHAKE.add(::ProtocolResponsePacket)
LEGACY.skip() // ProtocolRequest
LEGACY.skip() // ProtocolResponse
// Packets sent universe server -> universe client
LEGACY.add(::ServerDisconnectPacket) // ServerDisconnect
LEGACY.add(::ConnectSuccessPacket) // ConnectSuccess
LEGACY.skip() // ConnectFailure
LEGACY.add(::HandshakeChallengePacket) // HandshakeChallenge
LEGACY.skip() // ChatReceive
LEGACY.skip() // UniverseTimeUpdate
LEGACY.skip() // CelestialResponse
LEGACY.skip() // PlayerWarpResult
LEGACY.skip() // PlanetTypeUpdate
LEGACY.skip() // Pause
LEGACY.skip() // ServerInfo
// Packets sent universe client -> universe server
LEGACY.add(::ClientConnectPacket) // ClientConnect
LEGACY.skip() // ClientDisconnectRequest
LEGACY.add(::HandshakeResponsePacket) // HandshakeResponse
LEGACY.skip() // PlayerWarp
LEGACY.skip() // FlyShip
LEGACY.skip() // ChatSend
LEGACY.skip() // CelestialRequest
// Packets sent bidirectionally between the universe client and the universe
// server
LEGACY.skip() // ClientContextUpdate
// Packets sent world server -> world client
LEGACY.skip() // WorldStart
LEGACY.skip() // WorldStop
LEGACY.skip() // WorldLayoutUpdate
LEGACY.skip() // WorldParametersUpdate
LEGACY.skip() // CentralStructureUpdate
LEGACY.skip() // TileArrayUpdate
LEGACY.skip() // TileUpdate
LEGACY.skip() // TileLiquidUpdate
LEGACY.skip() // TileDamageUpdate
LEGACY.skip() // TileModificationFailure
LEGACY.skip() // GiveItem
LEGACY.skip() // EnvironmentUpdate
LEGACY.skip() // UpdateTileProtection
LEGACY.skip() // SetDungeonGravity
LEGACY.skip() // SetDungeonBreathable
LEGACY.skip() // SetPlayerStart
LEGACY.skip() // FindUniqueEntityResponse
LEGACY.skip() // Pong
// Packets sent world client -> world server
LEGACY.skip() // ModifyTileList
LEGACY.skip() // DamageTileGroup
LEGACY.skip() // CollectLiquid
LEGACY.skip() // RequestDrop
LEGACY.skip() // SpawnEntity
LEGACY.skip() // ConnectWire
LEGACY.skip() // DisconnectAllWires
LEGACY.skip() // WorldClientStateUpdate
LEGACY.skip() // FindUniqueEntity
LEGACY.skip() // WorldStartAcknowledge
LEGACY.skip() // Ping
// Packets sent bidirectionally between world client and world server
LEGACY.skip() // EntityCreate
LEGACY.skip() // EntityUpdateSet
LEGACY.skip() // EntityDestroy
LEGACY.skip() // EntityInteract
LEGACY.skip() // EntityInteractResult
LEGACY.skip() // HitRequest
LEGACY.skip() // DamageRequest
LEGACY.skip() // DamageNotification
LEGACY.skip() // EntityMessage
LEGACY.skip() // EntityMessageResponse
LEGACY.skip() // UpdateWorldProperties
LEGACY.skip() // StepUpdate
// Packets sent system server -> system client
LEGACY.skip() // SystemWorldStart
LEGACY.skip() // SystemWorldUpdate
LEGACY.skip() // SystemObjectCreate
LEGACY.skip() // SystemObjectDestroy
LEGACY.skip() // SystemShipCreate
LEGACY.skip() // SystemShipDestroy
// Packets sent system client -> system server
LEGACY.skip() // SystemObjectSpawn
}
}
}

View File

@ -0,0 +1,30 @@
package ru.dbotthepony.kstarbound.network.packets
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.CelestialBaseInformation
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.UUID
data class ProtocolRequestPacket(val version: Int) : IServerPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInt())
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeInt(version)
}
override fun play(connection: ServerConnection) {
if (version == Starbound.NATIVE_PROTOCOL_VERSION) {
connection.sendAndFlush(ProtocolResponsePacket(true))
connection.setupNative()
} else if (version == Starbound.LEGACY_PROTOCOL_VERSION) {
connection.sendAndFlush(ProtocolResponsePacket(true))
connection.setupLegacy()
} else {
connection.sendAndFlush(ProtocolResponsePacket(false))
connection.close()
}
}
}

View File

@ -0,0 +1,41 @@
package ru.dbotthepony.kstarbound.network.packets
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.defs.player.ShipUpgrades
import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientConnectPacket
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.*
import kotlin.collections.HashMap
data class ProtocolResponsePacket(val allowed: Boolean) : IClientPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readBoolean())
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeBoolean(allowed)
}
override fun play(connection: ClientConnection) {
if (allowed) {
if (connection.isLegacy) {
connection.setupLegacy()
connection.sendAndFlush(
ClientConnectPacket(
ByteArray(ClientConnectPacket.DIGEST_SIZE),
allowAssetMismatch = true,
playerUuid = UUID(0L, 0L),
playerName = "Test Name",
playerSpecies = "hylotl",
shipChunks = HashMap(),
shipUpgrades = ShipUpgrades(),
introComplete = false,
account = ""
)
)
} else {
connection.setupNative()
}
}
}
}

View File

@ -1,88 +0,0 @@
package ru.dbotthepony.kstarbound.network.packets
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufInputStream
import io.netty.buffer.ByteBufOutputStream
import io.netty.channel.Channel
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter
import ru.dbotthepony.kommons.io.readUUID
import ru.dbotthepony.kommons.io.writeUUID
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.IConnectionDetails
import ru.dbotthepony.kstarbound.network.IPacket
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.network.readUTF
import ru.dbotthepony.kstarbound.network.writeUTF
import ru.dbotthepony.kstarbound.server.ServerConnection
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.UUID
data class HelloPacket(
override val protocolVersion: Int,
override val engineVersion: String,
override val username: String,
override val password: String,
override val uuid: UUID
) : IPacket, IConnectionDetails {
constructor(buff: DataInputStream) : this(buff.readInt(), buff.readUTF(), buff.readUTF(), buff.readUTF(), buff.readUUID())
override fun write(stream: DataOutputStream) {
stream.writeInt(protocolVersion)
stream.writeUTF(engineVersion)
stream.writeUTF(username)
stream.writeUTF(password)
stream.writeUUID(uuid)
}
}
data class DisconnectPacket(val reason: String) : IServerPacket, IClientPacket {
constructor(buff: DataInputStream) : this(buff.readUTF())
override fun write(stream: DataOutputStream) {
stream.writeUTF(reason)
}
override fun play(connection: ServerConnection) {
}
override fun play(connection: ClientConnection) {
}
}
class HelloListener(val connection: Connection, private val channel: Channel) : ChannelInboundHandlerAdapter() {
init {
channel.pipeline().addFirst(this)
}
fun sendHello(uuid: UUID, username: String = "", password: String = ""): HelloListener {
val buf = channel.config().allocator.buffer()
buf.writeUTF("KSTARBOUND HELLO")
HelloPacket(Starbound.NATIVE_PROTOCOL_VERSION, Starbound.ENGINE_VERSION, username, password, uuid).write(DataOutputStream(ByteBufOutputStream(buf)))
channel.write(buf)
channel.flush()
return this
}
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if (msg is ByteBuf) {
if (msg.readUTF() != "KSTARBOUND HELLO") {
connection.helloFailed()
} else {
connection.helloReceived(HelloPacket(DataInputStream(ByteBufInputStream(msg))))
}
msg.release()
} else {
connection.helloFailed()
}
channel.pipeline().remove(this)
}
}

View File

@ -0,0 +1,20 @@
package ru.dbotthepony.kstarbound.network.packets.clientbound
import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.network.IClientPacket
import java.io.DataInputStream
import java.io.DataOutputStream
class ConnectFailurePacket(val reason: String = "") : IClientPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readBinaryString())
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeBinaryString(reason)
}
override fun play(connection: ClientConnection) {
TODO("Not yet implemented")
}
}

View File

@ -0,0 +1,28 @@
package ru.dbotthepony.kstarbound.network.packets.clientbound
import ru.dbotthepony.kommons.io.readUUID
import ru.dbotthepony.kommons.io.writeUUID
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.defs.CelestialBaseInformation
import ru.dbotthepony.kstarbound.network.IClientPacket
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.UUID
class ConnectSuccessPacket(val connectionID: Int, val serverUUID: UUID, val celestialInformation: CelestialBaseInformation) : IClientPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(
stream.readUnsignedShort(),
stream.readUUID(),
CelestialBaseInformation(stream, isLegacy)
)
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeShort(connectionID)
stream.writeUUID(serverUUID)
celestialInformation.write(stream, isLegacy)
}
override fun play(connection: ClientConnection) {
TODO("Not yet implemented")
}
}

View File

@ -0,0 +1,20 @@
package ru.dbotthepony.kstarbound.network.packets.clientbound
import ru.dbotthepony.kommons.io.readByteArray
import ru.dbotthepony.kommons.io.writeByteArray
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.network.IClientPacket
import java.io.DataInputStream
import java.io.DataOutputStream
class HandshakeChallengePacket(val passwordSalt: ByteArray) : IClientPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readByteArray())
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByteArray(passwordSalt)
}
override fun play(connection: ClientConnection) {
TODO("Not yet implemented")
}
}

View File

@ -0,0 +1,20 @@
package ru.dbotthepony.kstarbound.network.packets.clientbound
import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.network.IClientPacket
import java.io.DataInputStream
import java.io.DataOutputStream
class ServerDisconnectPacket(val reason: String = "") : IClientPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readBinaryString())
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeBinaryString(reason)
}
override fun play(connection: ClientConnection) {
TODO("Not yet implemented")
}
}

View File

@ -0,0 +1,68 @@
package ru.dbotthepony.kstarbound.network.packets.serverbound
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.io.readByteArray
import ru.dbotthepony.kommons.io.readKOptional
import ru.dbotthepony.kommons.io.readMap
import ru.dbotthepony.kommons.io.readUUID
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeByteArray
import ru.dbotthepony.kommons.io.writeKOptional
import ru.dbotthepony.kommons.io.writeMap
import ru.dbotthepony.kommons.io.writeUUID
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.defs.CelestialBaseInformation
import ru.dbotthepony.kstarbound.defs.player.ShipUpgrades
import ru.dbotthepony.kstarbound.io.ByteKey
import ru.dbotthepony.kstarbound.io.readByteKey
import ru.dbotthepony.kstarbound.io.writeByteKey
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectSuccessPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.InputStream
import java.util.*
import kotlin.collections.HashMap
data class ClientConnectPacket(
val assetDigest: ByteArray, val allowAssetMismatch: Boolean,
val playerUuid: UUID, val playerName: String, val playerSpecies: String,
val shipChunks: Map<ByteKey, KOptional<ByteArray>>, val shipUpgrades: ShipUpgrades, val introComplete: Boolean,
val account: String
) : IServerPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(
stream.readByteArray(),
stream.readBoolean(),
stream.readUUID(),
stream.readBinaryString(),
stream.readBinaryString(),
stream.readMap(InputStream::readByteKey, { readKOptional { readByteArray() } }, { HashMap(it) }),
ShipUpgrades.read(stream, isLegacy),
stream.readBoolean(),
stream.readBinaryString()
)
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByteArray(assetDigest)
stream.writeBoolean(allowAssetMismatch)
stream.writeUUID(playerUuid)
stream.writeBinaryString(playerName)
stream.writeBinaryString(playerSpecies)
stream.writeMap(shipChunks, { writeByteKey(it) }, { writeKOptional(it) { writeByteArray(it) } })
shipUpgrades.write(stream, isLegacy)
stream.writeBoolean(introComplete)
stream.writeBinaryString(account)
}
override fun play(connection: ServerConnection) {
LOGGER.info("Client connection request received from ${connection.channel.remoteAddress()}, Player $playerName/$playerUuid (account '$account')")
connection.sendAndFlush(ConnectSuccessPacket(4, UUID(4L, 4L), CelestialBaseInformation()))
}
companion object {
const val DIGEST_SIZE = 32
private val LOGGER = LogManager.getLogger()
}
}

View File

@ -0,0 +1,20 @@
package ru.dbotthepony.kstarbound.network.packets.serverbound
import ru.dbotthepony.kommons.io.readByteArray
import ru.dbotthepony.kommons.io.writeByteArray
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import java.io.DataInputStream
import java.io.DataOutputStream
class HandshakeResponsePacket(val passwordHash: ByteArray) : IServerPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readByteArray())
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByteArray(passwordHash)
}
override fun play(connection: ServerConnection) {
TODO("Not yet implemented")
}
}

View File

@ -41,9 +41,15 @@ class ServerChannels(val server: StarboundServer) : Closeable {
val channel = ServerBootstrap().channel(LocalServerChannel::class.java).group(Connection.NIO_POOL).childHandler(object : ChannelInitializer<Channel>() {
override fun initChannel(ch: Channel) {
LOGGER.info("Incoming connection from ${ch.remoteAddress()}")
val connection = ServerConnection(server, ConnectionType.MEMORY)
connections.add(connection)
connection.bind(ch)
try {
val connection = ServerConnection(server, ConnectionType.MEMORY)
connections.add(connection)
connection.bind(ch)
} catch (err: Throwable) {
LOGGER.error("Error while accepting new connection from ${ch.remoteAddress()}", err)
ch.close()
}
}
}).bind(LocalAddress.ANY).syncUninterruptibly()
@ -64,9 +70,15 @@ class ServerChannels(val server: StarboundServer) : Closeable {
val channel = ServerBootstrap().channel(NioServerSocketChannel::class.java).group(Connection.NIO_POOL).childHandler(object : ChannelInitializer<Channel>() {
override fun initChannel(ch: Channel) {
LOGGER.info("Incoming connection from ${ch.remoteAddress()}")
val connection = ServerConnection(server, ConnectionType.NETWORK)
connections.add(connection)
connection.bind(ch)
try {
val connection = ServerConnection(server, ConnectionType.NETWORK)
connections.add(connection)
connection.bind(ch)
} catch (err: Throwable) {
LOGGER.error("Error while accepting new connection from ${ch.remoteAddress()}", err)
ch.close()
}
}
}).bind(localAddress).syncUninterruptibly()

View File

@ -14,7 +14,6 @@ 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.HelloPacket
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.IChunkListener
@ -115,7 +114,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
}
fun tick() {
channel?.flush()
channel.flush()
val world = world
if (world == null) {
@ -154,9 +153,6 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
server.playerInGame(this)
}
override fun onHelloReceived(helloPacket: HelloPacket) {
}
companion object {
private val LOGGER = LogManager.getLogger()
}

View File

@ -9,9 +9,9 @@ import java.io.DataInputStream
import java.io.DataOutputStream
data class TrackedPositionPacket(val pos: Vector2d) : IServerPacket {
constructor(stream: DataInputStream) : this(stream.readVec2d())
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readVec2d())
override fun write(stream: DataOutputStream) {
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeVec2d(pos)
}

View File

@ -6,14 +6,14 @@ import java.io.DataInputStream
import java.io.DataOutputStream
data class TrackedSizePacket(val width: Int, val height: Int) : IServerPacket {
constructor(stream: DataInputStream) : this(stream.readUnsignedByte(), stream.readUnsignedByte())
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readUnsignedByte(), stream.readUnsignedByte())
init {
require(width in 1 .. 12) { "Bad chunk width to track: $width" }
require(height in 1 .. 12) { "Bad chunk height to track: $height" }
}
override fun write(stream: DataOutputStream) {
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(width)
stream.writeByte(height)
}