More legacy protocol implementation

This commit is contained in:
DBotThePony 2024-03-20 12:39:21 +07:00
parent 94cc53b176
commit afc45aac92
Signed by: DBot
GPG Key ID: DCC23B5715498507
30 changed files with 672 additions and 197 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,6 @@ class ConnectFailurePacket(val reason: String = "") : IClientPacket {
}
override fun play(connection: ClientConnection) {
TODO("Not yet implemented")
connection.disconnectNow()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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