More legacy protocol implementation
This commit is contained in:
parent
94cc53b176
commit
afc45aac92
@ -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))
|
||||
}
|
||||
|
@ -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<Channel>() { 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<Channel>() { override fun initChannel(ch: Channel) { connection.bind(ch) } })
|
||||
.connect(address)
|
||||
.syncUninterruptibly()
|
||||
|
||||
connection.sendHello()
|
||||
|
||||
connection.bootstrap(address)
|
||||
return connection
|
||||
}
|
||||
}
|
||||
|
@ -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<ScissorRect>()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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<MatchType, 0, FirstType, RestTypes...>
|
||||
// 0 is responsible for holding current template comparison check
|
||||
// typedef MVariant<WarpToWorld, WarpToPlayer, WarpAlias> WarpAction;
|
||||
// -> Variant<InvalidType, WarpToWorld, WarpToPlayer, WarpAlias> WarpAction
|
||||
// hence WarpToWorld has index 1, WarpToPlayer 2, WarpAlias 3
|
||||
|
||||
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)
|
||||
}
|
@ -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<Overlay> = ImmutableList.of(),
|
||||
val foregroundOverlays: ImmutableList<Overlay> = ImmutableList.of(),
|
||||
val backgroundBlocks: ImmutableList<Block> = ImmutableList.of(),
|
||||
val foregroundBlocks: ImmutableList<Block> = ImmutableList.of(),
|
||||
val objects: ImmutableList<Obj> = ImmutableList.of(),
|
||||
val flaggedBlocks: ImmutableMap<String, ImmutableList<Vector2i>> = 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<Int, String>, val residual: Boolean)
|
||||
|
||||
@JsonFactory
|
||||
data class Obj(
|
||||
val position: Vector2i,
|
||||
val name: String,
|
||||
val direction: Direction,
|
||||
val parameters: JsonElement,
|
||||
val residual: Boolean = false,
|
||||
)
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,6 @@ class ConnectFailurePacket(val reason: String = "") : IClientPacket {
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
connection.disconnectNow()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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<ServerWorld.() -> 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")
|
||||
|
@ -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) {
|
||||
|
@ -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<ServerConnection>()
|
||||
val players: List<ServerConnection> = 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<Boolean> {
|
||||
fun acceptPlayer(player: ServerConnection): CompletableFuture<Unit> {
|
||||
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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
||||
|
||||
var playerSpawnPosition = Vector2d.ZERO
|
||||
protected set
|
||||
|
||||
var respawnInWorld = false
|
||||
protected set
|
||||
|
||||
var adjustPlayerSpawn = false
|
||||
protected set
|
||||
|
||||
var ticks = 0L
|
||||
private set
|
||||
|
||||
open fun broadcast(packet: IPacket) {
|
||||
|
||||
}
|
||||
|
||||
var centralStructure: WorldStructure = WorldStructure()
|
||||
|
||||
val protectedDungeonIDs = IntArraySet()
|
||||
val properties = JsonObject()
|
||||
@ -238,6 +251,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
||||
open fun setPlayerSpawn(position: Vector2d, respawnInWorld: Boolean) {
|
||||
playerSpawnPosition = position
|
||||
this.respawnInWorld = respawnInWorld
|
||||
broadcast(SetPlayerStartPacket(position, respawnInWorld))
|
||||
}
|
||||
|
||||
abstract fun isSameThread(): Boolean
|
||||
@ -248,6 +262,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
||||
|
||||
fun think() {
|
||||
try {
|
||||
ticks++
|
||||
mailbox.executeQueuedTasks()
|
||||
|
||||
ForkJoinPool.commonPool().submit(ParallelPerform(dynamicEntities.spliterator(), { it.movement.move() })).join()
|
||||
|
@ -64,13 +64,13 @@ abstract class AbstractEntity(path: String) : JsonDriven(path) {
|
||||
open val isApplicableForUnloading: Boolean
|
||||
get() = true
|
||||
|
||||
protected open fun onSpawn(world: World<*, *>) { }
|
||||
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() {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user