diff --git a/SECURITY.md b/SECURITY.md index 11c0833c..62220234 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -53,3 +53,16 @@ So client can forge custom shipworld, with 2^31 x 2^31 dimensions, which will in server to consume at least 128 GiB of RAM when client connects. This attack does not require modified game client. + +----------- + +## Exploits in original engine + +These kind of bugs don't directly compromise security of server, but may degrade its performance. + +----------- + +### Client context window size + +Window size as reported by client is not checked for insane values, allowing to greatly slowdown the server if client +is residing in large world, and reporting to be tracking entire world. diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt index 74ab5bc8..c57c94c0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt @@ -157,7 +157,6 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : val playerID = networkedSignedInt() var playerEntity: PlayerEntity? = null - protected set // holy shit val clientSpectatingEntities = BasicNetworkedElement(IntAVLTreeSet(), StreamCodec.Collection(VarIntValueCodec) { IntAVLTreeSet() }) @@ -167,8 +166,23 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : fun trackingTileRegions(): List { val result = ArrayList() - val mins = Vector2i(windowXMin.get() - GlobalDefaults.client.windowMonitoringBorder, windowYMin.get() - GlobalDefaults.client.windowMonitoringBorder) - val maxs = Vector2i(windowWidth.get() + GlobalDefaults.client.windowMonitoringBorder, windowHeight.get() + GlobalDefaults.client.windowMonitoringBorder) + var mins = Vector2i(windowXMin.get() - GlobalDefaults.client.windowMonitoringBorder, windowYMin.get() - GlobalDefaults.client.windowMonitoringBorder) + var maxs = Vector2i(windowWidth.get() + GlobalDefaults.client.windowMonitoringBorder, windowHeight.get() + GlobalDefaults.client.windowMonitoringBorder) + + if (maxs.x - mins.x > 1000) { + // holy shit + val middle = (maxs.x - mins.x) / 2 + mins = mins.copy(x = middle - 500) + maxs = maxs.copy(x = middle + 500) + } + + if (maxs.y - mins.y > 1000) { + // holy shit + val middle = (maxs.y - mins.y) / 2 + mins = mins.copy(y = middle - 500) + maxs = maxs.copy(y = middle + 500) + } + val window = AABBi(mins, maxs + mins) if (window.mins != Vector2i.ZERO && window.maxs != Vector2i.ZERO) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt index 5906112a..a683fcc6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt @@ -3,62 +3,38 @@ package ru.dbotthepony.kstarbound.server import com.google.gson.JsonObject import io.netty.channel.ChannelHandlerContext import it.unimi.dsi.fastutil.bytes.ByteArrayList -import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap -import it.unimi.dsi.fastutil.ints.Int2ObjectMaps -import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream -import it.unimi.dsi.fastutil.objects.ObjectArraySet -import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet import 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.vector.Vector2i -import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket 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.EntityCreatePacket -import ru.dbotthepony.kstarbound.network.packets.EntityUpdateSetPacket -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.TileDamageUpdatePacket +import ru.dbotthepony.kstarbound.server.world.ServerWorldTracker import ru.dbotthepony.kstarbound.server.world.WorldStorage import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage import ru.dbotthepony.kstarbound.server.world.ServerWorld -import ru.dbotthepony.kstarbound.world.ChunkPos -import ru.dbotthepony.kstarbound.world.IChunkListener -import ru.dbotthepony.kstarbound.world.TileHealth -import ru.dbotthepony.kstarbound.world.api.ImmutableCell -import ru.dbotthepony.kstarbound.world.entities.AbstractEntity -import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity -import java.io.DataOutputStream import java.util.HashMap -import java.util.concurrent.ConcurrentLinkedQueue import kotlin.properties.Delegates // serverside part of connection class ServerConnection(val server: StarboundServer, type: ConnectionType) : Connection(ConnectionSide.SERVER, type) { var world: ServerWorld? = null + var tracker: ServerWorldTracker? = null var worldStartAcknowledged = false - private val tasks = ConcurrentLinkedQueue Unit>() - // packets which interact with world must be // executed on world's thread - fun enqueue(task: ServerWorld.() -> Unit) { - tasks.add(task) - } + fun enqueue(task: ServerWorld.() -> Unit) = tracker?.enqueue(task) lateinit var shipWorld: ServerWorld private set - var skyVersion = 0L - init { connectionID = server.channels.nextConnectionID() @@ -93,25 +69,6 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn shipChunks.putAll(chunks) } - private val tickets = HashMap() - private val pendingSend = ObjectLinkedOpenHashSet() - - private inner class Ticket(val ticket: ServerWorld.ITicket, val pos: ChunkPos) : IChunkListener { - override fun onEntityAdded(entity: AbstractEntity) {} - override fun onEntityRemoved(entity: AbstractEntity) {} - - override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) { - if (pos !in pendingSend) { - send(LegacyTileUpdatePacket(pos.tile + Vector2i(x, y), cell.toLegacyNet())) - } - } - - override fun onTileHealthUpdate(x: Int, y: Int, isBackground: Boolean, health: TileHealth) { - // let's hope nothing bad happens from referencing live data - send(TileDamageUpdatePacket(pos.tileX + x, pos.tileY + y, isBackground, health)) - } - } - override fun flush() { if (isConnected) { val entries = rpc.write() @@ -129,14 +86,6 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn super.flush() } - fun onLeaveWorld() { - tasks.clear() - tickets.values.forEach { it.ticket.cancel() } - tickets.clear() - pendingSend.clear() - playerEntity = null - } - override fun onChannelClosed() { playerEntity = null @@ -179,12 +128,8 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn flush() } - world?.removePlayer(this) - world = null - tickets.values.forEach { it.ticket.cancel() } - tickets.clear() - tasks.clear() - pendingSend.clear() + tracker?.remove() + tracker = null if (::shipWorld.isInitialized) { shipWorld.close() @@ -198,100 +143,6 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn } } - private val entityVersions = Int2LongOpenHashMap() - - init { - entityVersions.defaultReturnValue(-1L) - } - - fun isTracking(pos: ChunkPos): Boolean { - return pos in tickets - } - - fun tickWorld() { - val world = world!! - - run { - var next = tasks.poll() - - while (next != null) { - next.invoke(world) - next = tasks.poll() - } - } - - playerEntity = world.entities[playerID.get()] as? PlayerEntity - - run { - val newTrackedChunks = ObjectArraySet() - - for (region in trackingTileRegions()) { - newTrackedChunks.addAll(world.geometry.tileRegion2Chunks(region)) - } - - val itr = tickets.entries.iterator() - - for ((pos, ticket) in itr) { - if (pos !in newTrackedChunks) { - pendingSend.remove(pos) - ticket.ticket.cancel() - itr.remove() - } - } - - for (pos in newTrackedChunks) { - if (pos !in tickets) { - val ticket = world.permanentChunkTicket(pos) - val thisTicket = Ticket(ticket, pos) - tickets[pos] = thisTicket - ticket.listener = thisTicket - pendingSend.add(pos) - } - } - } - - run { - val itr = pendingSend.iterator() - - for (pos in itr) { - val chunk = world.chunkMap[pos] ?: continue - - if (isLegacy) { - send(LegacyTileArrayUpdatePacket(chunk)) - chunk.tileDamagePackets().forEach { send(it) } - } else { - send(ChunkCellsPacket(chunk)) - } - - itr.remove() - } - } - - for ((id, entity) in world.entities) { - if (entity.connectionID != connectionID && entity is PlayerEntity) { - if (entityVersions.get(id) == -1L) { - // never networked - val initial = FastByteArrayOutputStream() - entity.writeNetwork(DataOutputStream(initial), isLegacy) - val (data, version) = entity.networkGroup.write(isLegacy = isLegacy) - - entityVersions.put(id, version) - - send(EntityCreatePacket( - entity.type, - ByteArrayList.wrap(initial.array, initial.length), - data, - entity.entityID - )) - } else { - val (data, version) = entity.networkGroup.write(remoteVersion = entityVersions.get(id), isLegacy = isLegacy) - entityVersions.put(id, version) - send(EntityUpdateSetPacket(entity.connectionID, Int2ObjectMaps.singleton(entity.entityID, data))) - } - } - } - } - override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { if (msg is IServerPacket) { try { @@ -323,22 +174,12 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn shipWorld.thread.start() send(PlayerWarpResultPacket(true, WarpAlias.OwnShip, false)) - server.worlds.first().acceptPlayer(this) + //server.worlds.first().acceptPlayer(this) - /*shipWorld.acceptPlayer(this).thenAccept { - for (conn in server.channels.connections) { - if (conn.isLegacy && conn !== this) { - conn.shipWorld.acceptPlayer(this) - break - } - } - - server.worlds.first().acceptPlayer(this) - }.exceptionally { + shipWorld.acceptClient(this).exceptionally { LOGGER.error("Shipworld of $this rejected to accept its owner", it) disconnect("Shipworld rejected player warp request: $it") - null - }*/ + } }.exceptionally { LOGGER.error("Error while initializing shipworld for $this", it) disconnect("Error while initializing shipworld for player: $it") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt index 9cccef8a..bc7a8e2f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt @@ -13,8 +13,8 @@ import ru.dbotthepony.kstarbound.util.ExceptionLogger import ru.dbotthepony.kstarbound.util.ExecutionSpinner import java.io.Closeable import java.io.File -import java.util.Collections import java.util.UUID +import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock @@ -28,8 +28,7 @@ sealed class StarboundServer(val root: File) : Closeable { } } - val worlds: MutableList = Collections.synchronizedList(ArrayList()) - + val worlds = CopyOnWriteArrayList() val serverID = threadCounter.getAndIncrement() val mailbox = MailboxExecutorService().also { it.exceptionHandler = ExceptionLogger(LOGGER) } val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TIMESTEP_NANOS) @@ -89,7 +88,7 @@ sealed class StarboundServer(val root: File) : Closeable { fun playerInGame(player: ServerConnection) { val world = worlds.first() - world.acceptPlayer(player) + world.acceptClient(player) } protected abstract fun close0() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index 3605b29a..fad28244 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -9,7 +9,6 @@ import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kstarbound.Starbound -import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult import ru.dbotthepony.kstarbound.defs.world.WorldStructure @@ -17,8 +16,6 @@ import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.packets.StepUpdatePacket -import ru.dbotthepony.kstarbound.network.packets.clientbound.CentralStructureUpdatePacket -import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket import ru.dbotthepony.kstarbound.server.StarboundServer import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.util.AssetPathStack @@ -30,7 +27,6 @@ import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.WorldGeometry import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.entities.AbstractEntity -import java.util.Collections import java.util.concurrent.CompletableFuture import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.RejectedExecutionException @@ -54,52 +50,22 @@ class ServerWorld private constructor( server.worlds.add(this) } - private val internalPlayers = CopyOnWriteArrayList() - val players: List = Collections.unmodifiableList(internalPlayers) + val players = CopyOnWriteArrayList() - private fun doAcceptPlayer(player: ServerConnection) { - if (player in internalPlayers) - throw IllegalStateException("$player is already in $this") + private fun doAcceptClient(client: ServerConnection) { + if (players.any { it.client == client }) + throw IllegalStateException("$client is already in $this") - internalPlayers.add(player) - player.onLeaveWorld() - player.world?.removePlayer(player) - player.world = this - player.worldStartAcknowledged = false - - if (player.isLegacy) { - val (skyData, skyVersion) = sky.networkedGroup.write(isLegacy = true) - player.skyVersion = skyVersion - - player.send(WorldStartPacket( - templateData = Starbound.writeLegacyJson { template.toJson() }, - 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, - )) - - Starbound.writeLegacyJson { - player.sendAndFlush(CentralStructureUpdatePacket(Starbound.gson.toJsonTree(centralStructure))) - } - } else { - player.sendAndFlush(JoinWorldPacket(this)) - } + client.tracker?.remove() + players.add(ServerWorldTracker(this, client)) } - fun acceptPlayer(player: ServerConnection): CompletableFuture { + fun acceptClient(player: ServerConnection): CompletableFuture { check(!isClosed.get()) { "$this is invalid" } unpause() try { - return CompletableFuture.supplyAsync(Supplier { doAcceptPlayer(player) }, mailbox).exceptionally { + return CompletableFuture.supplyAsync(Supplier { doAcceptClient(player) }, mailbox).exceptionally { LOGGER.error("Error while accepting new player into world", it) } } catch (err: RejectedExecutionException) { @@ -107,35 +73,6 @@ class ServerWorld private constructor( } } - private fun doRemovePlayer(player: ServerConnection): Boolean { - if (internalPlayers.remove(player)) { - val itr = entities.int2ObjectEntrySet().iterator() - - for ((id, entity) in itr) { - if (id in player.entityIDRange) { - entity.remove() - } - } - - return true - } - - return false - } - - fun removePlayer(player: ServerConnection): CompletableFuture { - check(!isClosed.get()) { "$this is invalid" } - - try { - return CompletableFuture.supplyAsync(Supplier { doRemovePlayer(player) }, mailbox).exceptionally { - LOGGER.error("Error while removing player from world", it) - null - } - } catch (err: RejectedExecutionException) { - return CompletableFuture.completedFuture(false) - } - } - val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TIMESTEP_NANOS) val thread = Thread(spinner, "Starbound Server World Thread") val ticketListLock = ReentrantLock() @@ -160,13 +97,7 @@ class ServerWorld private constructor( super.close() spinner.unpause() - - lock.withLock { - internalPlayers.forEach { - it.world = null - } - } - + players.forEach { it.remove() } server.worlds.remove(this) LockSupport.unpark(thread) } @@ -210,12 +141,12 @@ class ServerWorld private constructor( override fun tickInner() { val packet = StepUpdatePacket(ticks) - internalPlayers.forEach { - if (!isClosed.get() && it.worldStartAcknowledged && it.channel.isOpen) { + players.forEach { + if (!isClosed.get()) { it.send(packet) try { - it.tickWorld() + it.tick() } catch (err: Throwable) { LOGGER.error("Exception while ticking player $it", err) //it.disconnect("Exception while ticking player: $err") @@ -253,7 +184,7 @@ class ServerWorld private constructor( } override fun broadcast(packet: IPacket) { - internalPlayers.forEach { + players.forEach { it.send(packet) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt new file mode 100644 index 00000000..476e05fa --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt @@ -0,0 +1,214 @@ +package ru.dbotthepony.kstarbound.server.world + +import it.unimi.dsi.fastutil.bytes.ByteArrayList +import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap +import it.unimi.dsi.fastutil.ints.Int2ObjectMaps +import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream +import it.unimi.dsi.fastutil.objects.ObjectArraySet +import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket +import ru.dbotthepony.kstarbound.network.IPacket +import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket +import ru.dbotthepony.kstarbound.network.packets.EntityUpdateSetPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.CentralStructureUpdatePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileUpdatePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket +import ru.dbotthepony.kstarbound.server.ServerConnection +import ru.dbotthepony.kstarbound.world.ChunkPos +import ru.dbotthepony.kstarbound.world.IChunkListener +import ru.dbotthepony.kstarbound.world.TileHealth +import ru.dbotthepony.kstarbound.world.api.ImmutableCell +import ru.dbotthepony.kstarbound.world.entities.AbstractEntity +import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity +import java.io.DataOutputStream +import java.util.HashMap +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.atomic.AtomicBoolean + +// couples ServerWorld and ServerConnection together, +// allowing ServerConnection client to track ServerWorld state +class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection) { + init { + client.worldStartAcknowledged = false + client.tracker = this + } + + var skyVersion = 0L + + private val isRemoved = AtomicBoolean() + private val tickets = HashMap() + private val pendingSend = ObjectLinkedOpenHashSet() + private val tasks = ConcurrentLinkedQueue Unit>() + private val entityVersions = Int2LongOpenHashMap() + + init { + entityVersions.defaultReturnValue(-1L) + } + + fun send(packet: IPacket) = client.send(packet) + + // packets which interact with world must be + // executed on world's thread + fun enqueue(task: ServerWorld.() -> Unit) { + tasks.add(task) + } + + private inner class Ticket(val ticket: ServerWorld.ITicket, val pos: ChunkPos) : IChunkListener { + override fun onEntityAdded(entity: AbstractEntity) {} + override fun onEntityRemoved(entity: AbstractEntity) {} + + override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) { + if (pos !in pendingSend) { + send(LegacyTileUpdatePacket(pos.tile + Vector2i(x, y), cell.toLegacyNet())) + } + } + + override fun onTileHealthUpdate(x: Int, y: Int, isBackground: Boolean, health: TileHealth) { + // let's hope nothing bad happens from referencing live data + send(TileDamageUpdatePacket(pos.tileX + x, pos.tileY + y, isBackground, health)) + } + } + + init { + val (skyData, skyVersion) = world.sky.networkedGroup.write(isLegacy = client.isLegacy) + this.skyVersion = skyVersion + + if (client.isLegacy) { + client.send(WorldStartPacket( + templateData = Starbound.writeLegacyJson { world.template.toJson() }, + skyData = skyData.toByteArray(), + weatherData = ByteArray(0), + playerStart = world.playerSpawnPosition, + playerRespawn = world.playerSpawnPosition, + respawnInWorld = world.respawnInWorld, + dungeonGravity = mapOf(), + dungeonBreathable = mapOf(), + protectedDungeonIDs = world.protectedDungeonIDs, + worldProperties = world.properties.deepCopy(), + connectionID = client.connectionID, + localInterpolationMode = false, + )) + + Starbound.writeLegacyJson { + client.sendAndFlush(CentralStructureUpdatePacket(Starbound.gson.toJsonTree(world.centralStructure))) + } + } + } + + fun isTracking(pos: ChunkPos): Boolean { + return pos in tickets + } + + fun tick() { + if (!client.worldStartAcknowledged) + return + + if (!client.channel.isOpen) { + remove() // ??? + return + } + + run { + var next = tasks.poll() + + while (next != null) { + next.invoke(world) + next = tasks.poll() + } + } + + client.playerEntity = world.entities[client.playerID.get()] as? PlayerEntity + + run { + val newTrackedChunks = ObjectArraySet() + + for (region in client.trackingTileRegions()) { + newTrackedChunks.addAll(world.geometry.tileRegion2Chunks(region)) + } + + val itr = tickets.entries.iterator() + + for ((pos, ticket) in itr) { + if (pos !in newTrackedChunks) { + pendingSend.remove(pos) + ticket.ticket.cancel() + itr.remove() + } + } + + for (pos in newTrackedChunks) { + if (pos !in tickets) { + val ticket = world.permanentChunkTicket(pos) + val thisTicket = Ticket(ticket, pos) + tickets[pos] = thisTicket + ticket.listener = thisTicket + pendingSend.add(pos) + } + } + } + + run { + val itr = pendingSend.iterator() + + for (pos in itr) { + val chunk = world.chunkMap[pos] ?: continue + + if (client.isLegacy) { + send(LegacyTileArrayUpdatePacket(chunk)) + chunk.tileDamagePackets().forEach { send(it) } + } else { + send(ChunkCellsPacket(chunk)) + } + + itr.remove() + } + } + + for ((id, entity) in world.entities) { + if (entity.connectionID != client.connectionID && entity is PlayerEntity) { + if (entityVersions.get(id) == -1L) { + // never networked + val initial = FastByteArrayOutputStream() + entity.writeNetwork(DataOutputStream(initial), client.isLegacy) + val (data, version) = entity.networkGroup.write(isLegacy = client.isLegacy) + + entityVersions.put(id, version) + + send(EntityCreatePacket( + entity.type, + ByteArrayList.wrap(initial.array, initial.length), + data, + entity.entityID + )) + } else { + val (data, version) = entity.networkGroup.write(remoteVersion = entityVersions.get(id), isLegacy = client.isLegacy) + entityVersions.put(id, version) + send(EntityUpdateSetPacket(entity.connectionID, Int2ObjectMaps.singleton(entity.entityID, data))) + } + } + } + } + + fun remove() { + if (isRemoved.compareAndSet(false, true)) { + client.tracker = null + client.playerEntity = null + world.players.remove(this) + tickets.values.forEach { it.ticket.cancel() } + + world.mailbox.execute { + val itr = world.entities.int2ObjectEntrySet().iterator() + + for ((id, entity) in itr) { + if (id in client.entityIDRange) { + entity.remove() + } + } + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 3cc1dc49..dabedc8a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -20,7 +20,6 @@ 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.server.StarboundServer import ru.dbotthepony.kstarbound.util.ExceptionLogger import ru.dbotthepony.kstarbound.util.ParallelPerform import ru.dbotthepony.kstarbound.world.api.ICellAccess @@ -35,7 +34,6 @@ import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.getBlockPlatforms import ru.dbotthepony.kstarbound.world.physics.getBlocksMarchingSquares import java.io.Closeable -import java.util.concurrent.ForkJoinPool import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock import java.util.function.Predicate @@ -276,7 +274,7 @@ abstract class World, ChunkType : Chunk