From ab516d91a959bbf8986baf66889fea5818504de0 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Mon, 22 Apr 2024 22:40:49 +0700 Subject: [PATCH] Responsive player tracking --- .../server/world/ServerWorldTracker.kt | 79 ++++++++++++++----- .../world/entities/AbstractEntity.kt | 2 + .../world/entities/tile/PlantEntity.kt | 3 +- 3 files changed, 61 insertions(+), 23 deletions(-) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt index 069aed7c..6dfb20ff 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt @@ -23,6 +23,7 @@ import ru.dbotthepony.kstarbound.defs.WarpAction import ru.dbotthepony.kstarbound.defs.WorldID import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.world.FlyingType +import ru.dbotthepony.kstarbound.math.AABBi import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket import ru.dbotthepony.kstarbound.network.packets.EntityDestroyPacket @@ -45,9 +46,11 @@ 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.* +import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic.AtomicBoolean +import kotlin.collections.ArrayList // couples ServerWorld and ServerConnection together, // allowing ServerConnection client to track ServerWorld state @@ -60,7 +63,10 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p private val isRemoved = AtomicBoolean() private var isActuallyRemoved = false private val tickets = HashMap() - private val tasks = ConcurrentLinkedQueue Unit>() + + private val tasksQueue = ConcurrentLinkedQueue>() + private val tasks = ArrayList>() + private val entityVersions = Int2LongOpenHashMap() private val scope = CoroutineScope(world.eventLoop.coroutines + SupervisorJob()) @@ -87,6 +93,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p world.damageTiles(positions, isBackground, sourcePosition, damage, source, tileModificationBudget) } catch (err: Throwable) { LOGGER.error("Exception in player damage tiles loop", err) + client.disconnect("Exception in player damage tiles loop: $err") } } } @@ -110,6 +117,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p } catch (err: Throwable) { client.send(TileModificationFailurePacket(modifications)) LOGGER.error("Exception in player modify tiles loop", err) + client.disconnect("Exception in player modify tiles loop: $err") } } } @@ -128,8 +136,18 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p // packets which interact with world must be // executed on world's thread fun enqueue(task: ServerWorld.() -> Unit) { - if (!isRemoved.get()) - tasks.add(task) + if (!isRemoved.get()) { + tasksQueue.add(world.eventLoop.supplyAsync { + if (!isRemoved.get()) { + try { + task(world) + } catch (err: Throwable) { + LOGGER.error("Exception executing queued player task", err) + client.disconnect("Exception executing queued player task: $err") + } + } + }) + } } private inner class Ticket(val ticket: ServerChunk.ITicket, val pos: ChunkPos) : IChunkListener { @@ -186,6 +204,32 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p } private var lastSeenFlyingType = FlyingType.NONE + private var currentlyTrackingRegions: List = listOf() + + private fun networkEntityForFirstTime(entity: AbstractEntity) { + val initial = FastByteArrayOutputStream() + entity.writeNetwork(DataOutputStream(initial), client.isLegacy) + val (data, version) = entity.networkGroup.write(isLegacy = client.isLegacy) + + entityVersions.put(entity.entityID, version) + + send(EntityCreatePacket( + entity.type, + ByteArrayList.wrap(initial.array, initial.length), + data, + entity.entityID + )) + } + + // called when entities join world, to allow instant tracking + fun evaluateShouldTrack(entity: AbstractEntity) { + if (entity.connectionID == client.connectionID) + return + + if (currentlyTrackingRegions.any { it.toDoubleAABB().intersect(entity.metaBoundingBox) }) { + networkEntityForFirstTime(entity) + } + } fun tick() { if (!client.worldStartAcknowledged) @@ -214,17 +258,19 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p } run { - var next = tasks.poll() + var next = tasksQueue.poll() - while (next != null && !isRemoved.get()) { - next.invoke(world) - next = tasks.poll() + while (next != null) { + tasks.add(next) + next = tasksQueue.poll() } } + tasks.removeIf { it.isDone } client.playerEntity = world.entities[client.playerID] as? PlayerEntity val trackingRegions = client.trackingTileRegions() + this.currentlyTrackingRegions = trackingRegions run { val newTrackedChunks = ArrayList() @@ -278,18 +324,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p 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 - )) + networkEntityForFirstTime(entity) } else if (entity.networkGroup.upstream.hasChangedSince(entityVersions.get(id))) { val (data, version) = entity.networkGroup.write(remoteVersion = entityVersions.get(id), isLegacy = client.isLegacy) entityVersions.put(id, version) @@ -338,8 +373,10 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p fun remove(reason: String = "ServerWorldTracker got removed", nullifyVariables: Boolean = true, setReturnWarp: Boolean = true) { if (isRemoved.compareAndSet(false, true)) { - // erase all tasks just to be sure + tasks.forEach { it.cancel(false) } + tasksQueue.forEach { it.cancel(false) } tasks.clear() + tasksQueue.clear() val playerEntity = client.playerEntity diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt index 15cf72af..ddb43888 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt @@ -171,6 +171,8 @@ abstract class AbstractEntity : Comparable { val connection = world.client.activeConnection // TODO: incomplete connection?.send(EntityCreatePacket(type, writeNetwork(connection.isLegacy), networkGroup.write(0L, connection.isLegacy).first, entityID)) + } else if (world is ServerWorld) { + world.clients.forEach { it.evaluateShouldTrack(this) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt index 7ff55924..a3985037 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt @@ -714,10 +714,9 @@ class PlantEntity() : TileEntity() { if (fallsWhenDead) { health.reset() breakAtPosition(baseDamagePosition, source) - } else { - remove(RemovalReason.DYING) } + remove(RemovalReason.DYING) return true }