From 2be54964de4fa83802d3b6124b4f2bea764405ca Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:40:41 +0100 Subject: [PATCH 01/10] refactor: optimize visualizer state management and improve concurrency handling --- .../surfapi/bukkit/api/util/bukkit-util.kt | 27 +- .../visualizer/visualizer/SurfVisualizer.kt | 3 +- .../visualizer/AbstractSurfVisualizerImpl.kt | 76 +++-- .../SurfVisualizerMultipleLocationsImpl.kt | 294 ++++++++++++------ .../SurfVisualizerSingleLocationImpl.kt | 51 ++- 5 files changed, 304 insertions(+), 147 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/util/bukkit-util.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/util/bukkit-util.kt index 112a5c0af..8eba698bb 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/util/bukkit-util.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/util/bukkit-util.kt @@ -12,11 +12,10 @@ import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf import io.papermc.paper.math.BlockPosition import io.papermc.paper.math.Position import it.unimi.dsi.fastutil.objects.ObjectList -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.* import kotlinx.coroutines.future.await -import kotlinx.coroutines.withContext +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit import org.bukkit.* import org.bukkit.block.Block import org.bukkit.entity.Entity @@ -25,6 +24,7 @@ import org.bukkit.plugin.java.JavaPlugin import org.spongepowered.math.vector.Vector3d import org.spongepowered.math.vector.Vector3i import java.util.* +import java.util.concurrent.ConcurrentHashMap /** * Creates a [NamespacedKey] using the calling plugin and the given name. @@ -237,20 +237,23 @@ suspend fun Collection.computeHighestYBlock(world: World): ObjectList< list.add(point) } - val snapshots = mutableLong2ObjectMapOf(byChunk.size) + val snapshots = ConcurrentHashMap(byChunk.size) coroutineScope { - byChunk.keys.map { key -> - async { - val snapshot = - world.getChunkAtAsync(getXFromChunkKey(key), getZFromChunkKey(key)) + val semaphore = Semaphore(16) // Limit concurrent chunk loads to prevent overwhelming the server + val iterator = byChunk.keys.iterator() + while (iterator.hasNext()) { + val key = iterator.nextLong() + launch { + semaphore.withPermit { + val snapshot = world.getChunkAtAsync(getXFromChunkKey(key), getZFromChunkKey(key)) .await() .getChunkSnapshot(true, false, false, false) - snapshots.put(key, snapshot) + snapshots[key] = snapshot + } } - }.awaitAll() + } } - val result = mutableObjectListOf(size) val it = byChunk.long2ObjectEntrySet().fastIterator() while (it.hasNext()) { diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizer.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizer.kt index ac9458ffd..7c2966da9 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizer.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizer.kt @@ -1,6 +1,5 @@ package dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer -import it.unimi.dsi.fastutil.objects.ObjectSet import org.bukkit.block.BlockType import org.bukkit.entity.Player import org.jetbrains.annotations.UnmodifiableView @@ -52,7 +51,7 @@ interface SurfVisualizer { * Modifications to the set directly are not allowed, ensuring consistency * with the visualizer's state. */ - val viewers: @UnmodifiableView ObjectSet + val viewers: @UnmodifiableView Set /** * Adds a specified player to the list of viewers for this visualizer. diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/AbstractSurfVisualizerImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/AbstractSurfVisualizerImpl.kt index e090d6fe8..a91b22ed5 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/AbstractSurfVisualizerImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/AbstractSurfVisualizerImpl.kt @@ -4,16 +4,14 @@ import com.github.shynixn.mccoroutine.folia.entityDispatcher import com.github.shynixn.mccoroutine.folia.launch import dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer.SurfVisualizer import dev.slne.surf.surfapi.bukkit.server.plugin -import dev.slne.surf.surfapi.core.api.collection.TransformingObjectSet -import dev.slne.surf.surfapi.core.api.util.freeze import dev.slne.surf.surfapi.core.api.util.logger -import dev.slne.surf.surfapi.core.api.util.mutableObjectSetOf -import dev.slne.surf.surfapi.core.api.util.synchronize import org.bukkit.Bukkit import org.bukkit.Chunk import org.bukkit.entity.Player import java.lang.ref.Cleaner import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean abstract class AbstractSurfVisualizerImpl : SurfVisualizer { protected val log = logger() @@ -22,29 +20,43 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { private val cleaner = Cleaner.create() } - protected val viewerUuids = mutableObjectSetOf().synchronize() - private val _viewers = - TransformingObjectSet(viewerUuids, { Bukkit.getPlayer(it) }, { it.uniqueId }) - override val viewers = _viewers.freeze() + protected val viewerUuids: MutableSet = ConcurrentHashMap.newKeySet() + override val viewers: Set = Collections.unmodifiableSet(viewerUuids) override val uid: UUID = UUID.randomUUID() - @Volatile - protected var visualizing = false + protected val visualizing = AtomicBoolean(false) init { - cleaner.register(this) { - stopVisualizing() + val cleanupState = createCleanupState() + cleaner.register(this, cleanupState) + } + + protected abstract fun createCleanupState(): CleanupState + + abstract class CleanupState : Runnable { + private val log = logger() + + override fun run() { + try { + cleanup() + } catch (e: Throwable) { + log.atWarning() + .withCause(e) + .log("Failed to clean up visualizer on GC") + } } + + protected abstract fun cleanup() } override fun startVisualizing(): Boolean { - if (visualizing) return false + if (!visualizing.compareAndSet(false, true)) return false try { - visualizing = true startVisualizingInternal() return true } catch (e: Throwable) { + visualizing.set(false) log.atSevere() .withCause(e) .log("Failed to start visualizing") @@ -53,13 +65,13 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { } override fun stopVisualizing(): Boolean { - if (!visualizing) return false + if (!visualizing.compareAndSet(true, false)) return false try { stopVisualizingInternal() - visualizing = false return true } catch (e: Throwable) { + visualizing.set(true) log.atSevere() .withCause(e) .log("Failed to stop visualizing") @@ -68,7 +80,7 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { } - override fun isVisualizing() = visualizing + override fun isVisualizing() = visualizing.get() override fun addViewer(player: Player) { if (player.isOnline && viewerUuids.add(player.uniqueId)) { @@ -85,22 +97,21 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { } override fun clearViewers() { - for (uuid in viewerUuids) { - Bukkit.getPlayer(uuid) - ?.takeIf { it.isOnline } - ?.let { onViewerRemoved(it) } + val iterator = viewerUuids.iterator() + while (iterator.hasNext()) { + val next = iterator.next() + Bukkit.getPlayer(next)?.let { onViewerRemoved(it) } + iterator.remove() } - - viewerUuids.clear() } override fun hasViewers() = viewerUuids.isNotEmpty() override fun visibleTo(player: Player) = player.uniqueId in viewerUuids open fun onViewerAdded(player: Player) { - if (!visualizing) return + if (!visualizing.get()) return - plugin.launch(plugin.entityDispatcher(player)) { + player.enterContextIfNeeded { player.sentChunks.forEach { chunk -> onPlayerReceiveChunk(player, chunk) } @@ -108,8 +119,9 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { } open fun onViewerRemoved(player: Player) { - if (!visualizing) return - plugin.launch(plugin.entityDispatcher(player)) { + if (!visualizing.get()) return + + player.enterContextIfNeeded { player.sentChunks.forEach { chunk -> onPlayerUnloadChunk(player, chunk) } @@ -122,6 +134,16 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { abstract fun onPlayerReceiveChunk(player: Player, chunk: Chunk) abstract fun onPlayerUnloadChunk(player: Player, chunk: Chunk) + protected inline fun Player.enterContextIfNeeded(crossinline action: () -> Unit) { + if (server.isOwnedByCurrentRegion(this)) { + action() + } else { + plugin.launch(plugin.entityDispatcher(this)) { + action() + } + } + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is AbstractSurfVisualizerImpl) return false diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt index 8a4c0d591..45e0ba9bb 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt @@ -2,8 +2,6 @@ package dev.slne.surf.surfapi.bukkit.server.impl.visualizer.visualizer -import com.github.shynixn.mccoroutine.folia.entityDispatcher -import com.github.shynixn.mccoroutine.folia.launch import dev.slne.surf.surfapi.bukkit.api.nms.NmsUseWithCaution import dev.slne.surf.surfapi.bukkit.api.nms.bridges.nmsCommonBridge import dev.slne.surf.surfapi.bukkit.api.nms.bridges.packets.PacketOperation @@ -12,15 +10,21 @@ import dev.slne.surf.surfapi.bukkit.api.nms.bridges.packets.entity.nmsSpawnPacke import dev.slne.surf.surfapi.bukkit.api.util.isChunkVisible import dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer.SurfVisualizerMultipleLocations import dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer.UpdateStrategy -import dev.slne.surf.surfapi.bukkit.server.plugin import dev.slne.surf.surfapi.core.api.util.* +import it.unimi.dsi.fastutil.ints.Int2ObjectMap +import it.unimi.dsi.fastutil.ints.IntOpenHashSet import it.unimi.dsi.fastutil.ints.IntSet +import it.unimi.dsi.fastutil.objects.Object2ObjectMap +import org.bukkit.Bukkit import org.bukkit.Chunk import org.bukkit.World import org.bukkit.entity.Player import org.spongepowered.math.vector.Vector3d import java.lang.ref.WeakReference import java.util.* +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.read +import kotlin.concurrent.write class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizerImpl(), SurfVisualizerMultipleLocations { @@ -31,10 +35,42 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer private val id2point = mutableInt2ObjectMapOf() private val point2Id = mutableObject2IntMapOf().apply { defaultReturnValue(Int.MIN_VALUE) } + private val sentToPlayers = mutableObject2ObjectMapOf() + private val lock = ReentrantReadWriteLock() + override val visualLocations - get() = id2point.mapTo(mutableObjectSetOf()) { it.value.location }.freeze() + get() = readLocked { id2point.mapTo(mutableObjectSetOf(id2point.size)) { it.value.location } }.freeze() + + private inline fun readLocked(block: () -> R): R = lock.read(block) + private inline fun writeLocked(block: () -> R): R = lock.write(block) + + override fun createCleanupState(): CleanupState { + return MultiLocationCleanupState(id2point, viewerUuids, sentToPlayers) + } + + private class MultiLocationCleanupState( + private val id2point: Int2ObjectMap, + private val viewerUuids: MutableSet, + private val sentToPlayers: Object2ObjectMap, + ) : CleanupState() { + override fun cleanup() { + if (id2point.isEmpty()) return + + val allIds = IntOpenHashSet(id2point.keys) + val despawn = nmsSpawnPackets.despawn(allIds) + + for (uuid in viewerUuids) { + val player = Bukkit.getPlayer(uuid) ?: continue + despawn.execute(player) + } + + id2point.clear() + viewerUuids.clear() + sentToPlayers.clear() + } + } override fun startVisualizingInternal() { update() @@ -42,62 +78,88 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer override fun stopVisualizingInternal() { for (viewer in viewers) { - nmsSpawnPackets.despawn(id2point.keys).execute(viewer) - getSentToPlayer(viewer).clear() + val sent = drainSentToPlayer(viewer) ?: continue + if (sent.isEmpty()) continue + + val player = Bukkit.getPlayer(viewer) ?: continue + nmsSpawnPackets.despawn(sent).execute(player) } } override fun update(strategy: UpdateStrategy) { - if (!visualizing) return + if (!visualizing.get()) return if (!checkNotNullWorld()) return when (strategy) { UpdateStrategy.ALL -> { + val pointsSnapshot = readLocked { + point2Id.object2IntEntrySet().map { it.intValue to it.key } + } + for (viewer in viewers) { - plugin.launch(plugin.entityDispatcher(viewer)) { - val sent = getSentToPlayer(viewer) - nmsSpawnPackets.despawn(sent).execute(viewer) - sent.clear() + val sent = drainSentToPlayer(viewer) + val player = Bukkit.getPlayer(viewer) ?: continue + player.enterContextIfNeeded { + if (!sent.isNullOrEmpty()) { + nmsSpawnPackets.despawn(sent).execute(player) + } + val idsToMarkSent = mutableIntSetOf() val spawn = PacketOperation.start() - point2Id.object2IntEntrySet().fastForEach { entry -> - val point = entry.key - val id = entry.intValue - - if (viewer.isChunkVisible( - world, - point.chunkX, - point.chunkZ - ) && sent.add(id) - ) { + + for ((id, point) in pointsSnapshot) { + if (player.isChunkVisible(world, point.chunkX, point.chunkZ) && idsToMarkSent.add(id)) { spawn + spawnPacket(id, point) } } - spawn.execute(viewer) + + writeLocked { + getOrCreateSentToPlayer(viewer).addAll(idsToMarkSent) + } + + spawn.execute(player) } } } UpdateStrategy.POSITION -> { for (viewer in viewers) { - plugin.launch(plugin.entityDispatcher(viewer)) { - val operation = PacketOperation.start() - val sent = getSentToPlayer(viewer) - - val sentIterator = sent.intIterator() - while (sentIterator.hasNext()) { - val id = sentIterator.nextInt() - val point = id2point[id] ?: continue - - if (viewer.isChunkVisible(world, point.chunkX, point.chunkZ)) { - operation + updatePositionPacket(id, point) - } else { - operation + nmsSpawnPackets.despawn(id) - sentIterator.remove() - } + val player = Bukkit.getPlayer(viewer) + if (player == null) { + drainSentToPlayer(viewer) + continue + } + + val sentSnapshot = getSentToPlayerSnapshot(viewer) + val operation = PacketOperation.start() + val idsToRemove = mutableIntSetOf() + + val iterator = sentSnapshot.iterator() + while (iterator.hasNext()) { + val id = iterator.nextInt() + val point = readLocked { id2point[id] } + + if (point == null) { + operation + nmsSpawnPackets.despawn(id) + idsToRemove.add(id) + continue + } + + if (player.isChunkVisible(world, point.chunkX, point.chunkZ)) { + operation + updatePositionPacket(id, point) + } else { + operation + nmsSpawnPackets.despawn(id) + idsToRemove.add(id) + } + } + + if (idsToRemove.isNotEmpty()) { + writeLocked { + getSentToPlayer(viewer)?.removeAll(idsToRemove) } - operation.execute(viewer) } + + operation.execute(player) } } } @@ -111,11 +173,12 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer val point = VisualPoint(visualLocation, settings) put(id, point) - if (!visualizing) return + if (!visualizing.get()) return if (!checkNotNullWorld()) return for (viewer in viewers) { - spawn(viewer, id, point) + val player = Bukkit.getPlayer(viewer) ?: continue + spawn(player, id, point) } } @@ -126,36 +189,42 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer return } - if (!visualizing) { + if (!visualizing.get()) { for ((loc, setting) in locations) { addVisualLocation(loc, setting) } - } else { - val toSpawn = mutableInt2ObjectMapOf() - - for ((loc, setting) in locations) { - val id = nmsCommonBridge.nextEntityId() - val point = VisualPoint(loc, setting) - put(id, point) - toSpawn[id] = point - } - - for (player in viewers) { - plugin.launch(plugin.entityDispatcher(player)) { - val sent = getSentToPlayer(player) - val spawnOperation = PacketOperation.start() + return + } - toSpawn.int2ObjectEntrySet().fastForEach { entry -> - val id = entry.intKey - val point = entry.value + val toSpawn = mutableInt2ObjectMapOf() + for ((loc, setting) in locations) { + val id = nmsCommonBridge.nextEntityId() + val point = VisualPoint(loc, setting) + put(id, point) + toSpawn[id] = point + } - if (player.isChunkVisible(world, point.chunkX, point.chunkZ) && sent.add(id)) { - spawnOperation + spawnPacket(id, point) - } + for (viewer in viewers) { + val player = Bukkit.getPlayer(viewer) ?: continue + player.enterContextIfNeeded { + val idsToAdd = mutableIntSetOf() + val spawnOperation = PacketOperation.start() + + toSpawn.int2ObjectEntrySet().fastForEach { entry -> + val id = entry.intKey + val point = entry.value + + if (player.isChunkVisible(world, point.chunkX, point.chunkZ)) { + idsToAdd.add(id) + spawnOperation + spawnPacket(id, point) } + } - spawnOperation.execute(player) + writeLocked { + getOrCreateSentToPlayer(viewer).addAll(idsToAdd) } + + spawnOperation.execute(player) } } } @@ -164,33 +233,36 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer val result = remove(visualLocation) ?: return val (id, point) = result - if (!visualizing) return + if (!visualizing.get()) return if (!checkNotNullWorld()) return for (viewer in viewers) { - despawn(viewer, id, point, true) + val player = Bukkit.getPlayer(viewer) ?: continue + despawn(player, id, point, true) } } override fun clearVisualLocations() { - if (visualizing && checkNotNullWorld()) { - val idsToRemove = id2point.keys + val idsToRemove = readLocked { + IntOpenHashSet(id2point.keys) + } + + if (visualizing.get() && checkNotNullWorld() && idsToRemove.isNotEmpty()) { for (viewer in viewers) { - nmsSpawnPackets.despawn(idsToRemove).execute(viewer) + val player = Bukkit.getPlayer(viewer) ?: continue + nmsSpawnPackets.despawn(idsToRemove).execute(player) } } clear() } - @Synchronized - private fun put(id: Int, point: VisualPoint) { + private fun put(id: Int, point: VisualPoint) = writeLocked { id2point[id] = point point2Id[point] = id } - @Synchronized - private fun remove(location: Vector3d): Pair? { + private fun remove(location: Vector3d): Pair? = writeLocked { val point = point2Id.keys.find { it.location == location } ?: return null val id = point2Id.removeInt(point) @@ -202,31 +274,46 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer return null } - @Synchronized - private fun clear() { + private fun clear() = writeLocked { id2point.clear() point2Id.clear() sentToPlayers.clear() } - @Synchronized - private fun getSentToPlayer(player: Player) = - sentToPlayers.computeIfAbsent(player.uniqueId) { mutableIntSetOf() } + private fun getSentToPlayer(uuid: UUID): IntSet? = readLocked { + sentToPlayers[uuid] + } + + private fun getOrCreateSentToPlayer(uuid: UUID): IntSet = writeLocked { + sentToPlayers.computeIfAbsent(uuid) { mutableIntSetOf() } + } + + private fun getSentToPlayerSnapshot(uuid: UUID) = readLocked { + sentToPlayers[uuid]?.let { IntOpenHashSet(it) } ?: IntOpenHashSet() + } + + private fun drainSentToPlayer(uuid: UUID) = writeLocked { + sentToPlayers.remove(uuid) + } private fun spawn(player: Player, id: Int, point: VisualPoint) { - plugin.launch(plugin.entityDispatcher(player)) { + player.enterContextIfNeeded { if (player.isChunkVisible(world, point.chunkX, point.chunkZ)) { spawnPacket(id, point).execute(player) - getSentToPlayer(player).add(id) + writeLocked { + getOrCreateSentToPlayer(player.uniqueId).add(id) + } } } } private fun despawn(player: Player, id: Int, point: VisualPoint, force: Boolean = false) { - plugin.launch(plugin.entityDispatcher(player)) { + player.enterContextIfNeeded { if (force || !player.isChunkVisible(world, point.chunkX, point.chunkZ)) { nmsSpawnPackets.despawn(id).execute(player) - getSentToPlayer(player).remove(id) + writeLocked { + getOrCreateSentToPlayer(player.uniqueId).remove(id) + } } } } @@ -238,31 +325,46 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer nmsSpawnPackets.teleport(id, point.pos) override fun onPlayerReceiveChunk(player: Player, chunk: Chunk) { + val entries = readLocked { + point2Id.object2IntEntrySet().map { it.intValue to it.key } + } + val spawnOperation = PacketOperation.start() - val sent = getSentToPlayer(player) + val idsToAdd = mutableIntSetOf() + - point2Id.object2IntEntrySet().fastForEach { entry -> - val point = entry.key - val id = entry.intValue - if (world != chunk.world || point.chunkX != chunk.x || point.chunkZ != chunk.z) return@fastForEach + for ((id, point) in entries) { + if (world != chunk.world || point.chunkX != chunk.x || point.chunkZ != chunk.z) continue spawnOperation + spawnPacket(id, point) - sent.add(id) + idsToAdd.add(id) + } + + if (idsToAdd.isNotEmpty()) { + writeLocked { + getOrCreateSentToPlayer(player.uniqueId).addAll(idsToAdd) + } } spawnOperation.execute(player) } override fun onPlayerUnloadChunk(player: Player, chunk: Chunk) { - val sent = getSentToPlayer(player) + val sentSnapshot = getSentToPlayerSnapshot(player.uniqueId) val despawn = mutableIntSetOf() - val it = sent.intIterator() - while (it.hasNext()) { - val id = it.nextInt() - val point = id2point[id] ?: continue - if (world != chunk.world || point.chunkX != chunk.x || point.chunkZ != chunk.z) continue + val iterator = sentSnapshot.iterator() + while (iterator.hasNext()) { + val id = iterator.nextInt() + val point = readLocked { id2point[id] } + if (point != null && (world != chunk.world || point.chunkX != chunk.x || point.chunkZ != chunk.z)) { + continue + } despawn.add(id) - it.remove() + } + + if (despawn.isEmpty()) return + writeLocked { + getSentToPlayer(player.uniqueId)?.removeAll(despawn) } nmsSpawnPackets.despawn(despawn).execute(player) @@ -271,8 +373,10 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer fun checkNotNullWorld(): Boolean { if (worldReference.get() == null) { - visualizing = false - viewerUuids.clear() + visualizing.set(false) + writeLocked { + sentToPlayers.clear() + } log.atWarning() .log("World reference is no longer valid, stopping visualizer") return false diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerSingleLocationImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerSingleLocationImpl.kt index 8ba4ea1e2..dbc3b63c3 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerSingleLocationImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerSingleLocationImpl.kt @@ -12,9 +12,11 @@ import dev.slne.surf.surfapi.bukkit.api.util.isChunkVisible import dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer.SurfVisualizer import dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer.SurfVisualizerSingleLocation import dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer.UpdateStrategy +import org.bukkit.Bukkit import org.bukkit.Chunk import org.bukkit.Location import org.bukkit.entity.Player +import java.util.* class SurfVisualizerSingleLocationImpl(location: Location) : AbstractSurfVisualizerImpl(), SurfVisualizerSingleLocation { @@ -34,12 +36,33 @@ class SurfVisualizerSingleLocationImpl(location: Location) : AbstractSurfVisuali update() } + override fun createCleanupState(): CleanupState { + return SingleLocationCleanupState(entityId, viewerUuids) + } + + private class SingleLocationCleanupState( + private val entityId: Int, + private val viewerUuids: MutableSet, + ) : CleanupState() { + override fun cleanup() { + val despawn = nmsSpawnPackets.despawn(entityId) + for (uuid in viewerUuids) { + val player = Bukkit.getPlayer(uuid) ?: continue + despawn.execute(player) + } + viewerUuids.clear() + } + } + override fun startVisualizingInternal() { update() } override fun stopVisualizingInternal() { - viewers.forEach { despawnPacket().execute(it) } + for (uuid in viewers) { + val player = Bukkit.getPlayer(uuid) ?: continue + despawnPacket().execute(player) + } } override fun onPlayerReceiveChunk(player: Player, chunk: Chunk) { @@ -55,7 +78,7 @@ class SurfVisualizerSingleLocationImpl(location: Location) : AbstractSurfVisuali } override fun update(strategy: UpdateStrategy) { - if (!visualizing) return + if (!visualizing.get()) return when (strategy) { UpdateStrategy.ALL -> { @@ -63,11 +86,14 @@ class SurfVisualizerSingleLocationImpl(location: Location) : AbstractSurfVisuali val spawn = spawnPacket() for (viewer in viewers) { - despawn.execute(viewer) - val seesLocation = viewer.isChunkVisible(location) - - if (seesLocation) { - spawn.execute(viewer) + val player = Bukkit.getPlayer(viewer) ?: continue + player.enterContextIfNeeded { + despawn.execute(player) + val seesLocation = player.isChunkVisible(location) + + if (seesLocation) { + spawn.execute(player) + } } } } @@ -75,10 +101,13 @@ class SurfVisualizerSingleLocationImpl(location: Location) : AbstractSurfVisuali UpdateStrategy.POSITION -> { val updatePosition = updatePositionPacket() for (viewer in viewers) { - if (viewer.isChunkVisible(location)) { - updatePosition.execute(viewer) - } else { - despawnPacket().execute(viewer) + val player = Bukkit.getPlayer(viewer) ?: continue + player.enterContextIfNeeded { + if (player.isChunkVisible(location)) { + updatePosition.execute(player) + } else { + despawnPacket().execute(player) + } } } } From b69f1ae7c1d2bf126298659ade374ab10021552a Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:53:21 +0100 Subject: [PATCH 02/10] refactor: enhance visualizer state management and ensure proper closure handling --- gradle.properties | 2 +- .../NmsClientboundPacketListener.java | 6 +- .../NmsServerboundPacketListener.java | 6 +- .../visualizer/visualizer/SurfVisualizer.kt | 12 +- .../impl/nms/SurfBukkitNmsBridgeImpl.kt | 4 +- .../visualizer/SurfBukkitVisualizerApiImpl.kt | 5 + .../visualizer/AbstractSurfVisualizerImpl.kt | 74 ++++++++---- .../SurfVisualizerMultipleLocationsImpl.kt | 108 ++++++++++++------ .../SurfVisualizerSingleLocationImpl.kt | 26 ++++- .../collection/TransformingSet2ObjectSet.kt | 38 ++++++ 10 files changed, 200 insertions(+), 81 deletions(-) create mode 100644 surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/collection/TransformingSet2ObjectSet.kt diff --git a/gradle.properties b/gradle.properties index 4858161dd..20212d851 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled javaVersion=25 mcVersion=1.21.11 group=dev.slne.surf -version=1.21.11-2.69.0 +version=1.21.11-2.70.0 relocationPrefix=dev.slne.surf.surfapi.libs snapshot=false diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/java/dev/slne/surf/surfapi/bukkit/api/nms/listener/NmsClientboundPacketListener.java b/surf-api-bukkit/surf-api-bukkit-api/src/main/java/dev/slne/surf/surfapi/bukkit/api/nms/listener/NmsClientboundPacketListener.java index 5d551e16c..fb5614c2b 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/java/dev/slne/surf/surfapi/bukkit/api/nms/listener/NmsClientboundPacketListener.java +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/java/dev/slne/surf/surfapi/bukkit/api/nms/listener/NmsClientboundPacketListener.java @@ -21,11 +21,7 @@ default PacketListenerResult handleEarlyClientboundPacket(Packet packet, @Nullab if (player != null) { return handleClientboundPacket(packet, player); } else { - throw new IllegalStateException( - "No player is available for this clientbound packet yet. " + - "This can happen during early connection phases such as login. " + - "Override handleEarlyClientboundPacket(...) if your listener should handle packets before a Player exists." - ); + return PacketListenerResult.CONTINUE; } } } diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/java/dev/slne/surf/surfapi/bukkit/api/nms/listener/NmsServerboundPacketListener.java b/surf-api-bukkit/surf-api-bukkit-api/src/main/java/dev/slne/surf/surfapi/bukkit/api/nms/listener/NmsServerboundPacketListener.java index b4933286c..85ec4c10c 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/java/dev/slne/surf/surfapi/bukkit/api/nms/listener/NmsServerboundPacketListener.java +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/java/dev/slne/surf/surfapi/bukkit/api/nms/listener/NmsServerboundPacketListener.java @@ -21,11 +21,7 @@ default PacketListenerResult handleEarlyServerboundPacket(Packet packet, @Nullab if (player != null) { return handleServerboundPacket(packet, player); } else { - throw new IllegalStateException( - "No player is available for this serverbound packet yet. " + - "This can happen during early connection phases such as login. " + - "Override handleEarlyServerboundPacket(...) if your listener should handle packets before a Player exists." - ); + return PacketListenerResult.CONTINUE; } } } diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizer.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizer.kt index 7c2966da9..8c88bc0ae 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizer.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizer.kt @@ -1,8 +1,10 @@ package dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer +import it.unimi.dsi.fastutil.objects.ObjectSet import org.bukkit.block.BlockType import org.bukkit.entity.Player import org.jetbrains.annotations.UnmodifiableView +import java.lang.AutoCloseable import java.util.* @@ -15,7 +17,7 @@ import java.util.* * and may be subject to changes in future updates. */ @ExperimentalVisualizerApi -interface SurfVisualizer { +interface SurfVisualizer : AutoCloseable { /** * A unique identifier for the visualizer instance. * This identifier is used to distinguish one visualizer object from another. @@ -51,7 +53,10 @@ interface SurfVisualizer { * Modifications to the set directly are not allowed, ensuring consistency * with the visualizer's state. */ - val viewers: @UnmodifiableView Set + val viewerUuids: @UnmodifiableView Set + + @Deprecated("Use viewerUuids instead", ReplaceWith("viewerUuids")) + val viewers: @UnmodifiableView ObjectSet /** * Adds a specified player to the list of viewers for this visualizer. @@ -103,6 +108,9 @@ interface SurfVisualizer { */ fun update(strategy: UpdateStrategy = UpdateStrategy.ALL) + + override fun close() // TODO: Provide kdocs + /** * Companion object for the `SurfVisualizer` interface. * Provides shared constants and utilities related to visualizer functionality. diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/nms/SurfBukkitNmsBridgeImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/nms/SurfBukkitNmsBridgeImpl.kt index 18dfdeb7a..1703e2c3c 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/nms/SurfBukkitNmsBridgeImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/nms/SurfBukkitNmsBridgeImpl.kt @@ -89,7 +89,7 @@ class SurfBukkitNmsBridgeImpl : SurfBukkitNmsBridge { } catch (e: Throwable) { log.atSevere() .withCause(e) - .log("Failed to handle serverbound packet") + .log("Failed to handle serverbound packet $clazz for listener $listener") PacketListenerResult.CONTINUE } @@ -118,7 +118,7 @@ class SurfBukkitNmsBridgeImpl : SurfBukkitNmsBridge { } catch (e: Throwable) { log.atSevere() .withCause(e) - .log("Failed to handle clientbound packet") + .log("Failed to handle clientbound packet ${packet.packetClass} for listener $listener") PacketListenerResult.CONTINUE } diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/SurfBukkitVisualizerApiImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/SurfBukkitVisualizerApiImpl.kt index 070c875d7..8bc78a9e2 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/SurfBukkitVisualizerApiImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/SurfBukkitVisualizerApiImpl.kt @@ -94,6 +94,11 @@ class SurfBukkitVisualizerApiImpl : SurfBukkitVisualizerApi { active.forEach { it.removeViewer(player) } } + + fun onVisualizerClose(visualizer: AbstractSurfVisualizerImpl) { + visualizers.invalidate(visualizer.uid) + areaVisualizers.invalidate(visualizer.uid) + } } val visualizerApiImpl get() = SurfBukkitVisualizerApi.instance as SurfBukkitVisualizerApiImpl diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/AbstractSurfVisualizerImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/AbstractSurfVisualizerImpl.kt index a91b22ed5..0cf1edc9e 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/AbstractSurfVisualizerImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/AbstractSurfVisualizerImpl.kt @@ -3,8 +3,11 @@ package dev.slne.surf.surfapi.bukkit.server.impl.visualizer.visualizer import com.github.shynixn.mccoroutine.folia.entityDispatcher import com.github.shynixn.mccoroutine.folia.launch import dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer.SurfVisualizer +import dev.slne.surf.surfapi.bukkit.server.impl.visualizer.visualizerApiImpl import dev.slne.surf.surfapi.bukkit.server.plugin +import dev.slne.surf.surfapi.core.api.collection.TransformingSet2ObjectSet import dev.slne.surf.surfapi.core.api.util.logger +import it.unimi.dsi.fastutil.objects.ObjectSet import org.bukkit.Bukkit import org.bukkit.Chunk import org.bukkit.entity.Player @@ -17,21 +20,20 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { protected val log = logger() companion object { - private val cleaner = Cleaner.create() + val cleaner: Cleaner = Cleaner.create() } - protected val viewerUuids: MutableSet = ConcurrentHashMap.newKeySet() - override val viewers: Set = Collections.unmodifiableSet(viewerUuids) - override val uid: UUID = UUID.randomUUID() + protected val internalViewerUuids: MutableSet = ConcurrentHashMap.newKeySet() + override val viewerUuids: MutableSet = Collections.unmodifiableSet(internalViewerUuids) - protected val visualizing = AtomicBoolean(false) + @Deprecated("Use viewerUuids instead", replaceWith = ReplaceWith("viewerUuids")) + override val viewers: ObjectSet = + TransformingSet2ObjectSet(viewerUuids, Bukkit::getPlayer, Player::getUniqueId) - init { - val cleanupState = createCleanupState() - cleaner.register(this, cleanupState) - } + override val uid: UUID = UUID.randomUUID() - protected abstract fun createCleanupState(): CleanupState + protected val visualizing = AtomicBoolean(false) + protected val closed = AtomicBoolean(false) abstract class CleanupState : Runnable { private val log = logger() @@ -50,6 +52,7 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { } override fun startVisualizing(): Boolean { + ensureNotClosed() if (!visualizing.compareAndSet(false, true)) return false try { @@ -65,13 +68,20 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { } override fun stopVisualizing(): Boolean { - if (!visualizing.compareAndSet(true, false)) return false + ensureNotClosed() + return stopVisualizing(false) + } + + fun stopVisualizing(force: Boolean): Boolean { + if (!visualizing.compareAndSet(true, false) && !force) return false + if (!force) { + ensureNotClosed() + } try { stopVisualizingInternal() return true } catch (e: Throwable) { - visualizing.set(true) log.atSevere() .withCause(e) .log("Failed to stop visualizing") @@ -79,17 +89,18 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { } } - override fun isVisualizing() = visualizing.get() override fun addViewer(player: Player) { - if (player.isOnline && viewerUuids.add(player.uniqueId)) { + ensureNotClosed() + if (player.isOnline && internalViewerUuids.add(player.uniqueId)) { onViewerAdded(player) } } override fun removeViewer(player: Player) { - if (viewerUuids.remove(player.uniqueId)) { + ensureNotClosed() + if (internalViewerUuids.remove(player.uniqueId)) { if (player.isOnline) { onViewerRemoved(player) } @@ -97,7 +108,8 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { } override fun clearViewers() { - val iterator = viewerUuids.iterator() + ensureNotClosed() + val iterator = internalViewerUuids.iterator() while (iterator.hasNext()) { val next = iterator.next() Bukkit.getPlayer(next)?.let { onViewerRemoved(it) } @@ -105,29 +117,35 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { } } - override fun hasViewers() = viewerUuids.isNotEmpty() - override fun visibleTo(player: Player) = player.uniqueId in viewerUuids + override fun hasViewers() = internalViewerUuids.isNotEmpty() + override fun visibleTo(player: Player) = player.uniqueId in internalViewerUuids open fun onViewerAdded(player: Player) { + ensureNotClosed() if (!visualizing.get()) return player.enterContextIfNeeded { + if (!visualizing.get()) return@enterContextIfNeeded player.sentChunks.forEach { chunk -> onPlayerReceiveChunk(player, chunk) } } } - open fun onViewerRemoved(player: Player) { - if (!visualizing.get()) return + abstract fun onViewerRemoved(player: Player) - player.enterContextIfNeeded { - player.sentChunks.forEach { chunk -> - onPlayerUnloadChunk(player, chunk) - } - } + @Synchronized + final override fun close() { + if (!closed.compareAndSet(false, true)) return + + visualizerApiImpl.onVisualizerClose(this) + + stopVisualizing(true) + onClose() + internalViewerUuids.clear() } + protected abstract fun onClose() protected abstract fun startVisualizingInternal() protected abstract fun stopVisualizingInternal() @@ -144,6 +162,12 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { } } + protected fun ensureNotClosed() { + if (closed.get()) { + throw IllegalStateException("Visualizer is already closed!") + } + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is AbstractSurfVisualizerImpl) return false diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt index 45e0ba9bb..cdb7e256c 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt @@ -46,19 +46,20 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer private inline fun readLocked(block: () -> R): R = lock.read(block) private inline fun writeLocked(block: () -> R): R = lock.write(block) - override fun createCleanupState(): CleanupState { - return MultiLocationCleanupState(id2point, viewerUuids, sentToPlayers) + init { + cleaner.register(this, MultiLocationCleanupState(id2point, internalViewerUuids, sentToPlayers, lock)) } private class MultiLocationCleanupState( private val id2point: Int2ObjectMap, private val viewerUuids: MutableSet, private val sentToPlayers: Object2ObjectMap, + private val lock: ReentrantReadWriteLock, ) : CleanupState() { override fun cleanup() { if (id2point.isEmpty()) return - val allIds = IntOpenHashSet(id2point.keys) + val allIds = lock.read { IntOpenHashSet(id2point.keys) } val despawn = nmsSpawnPackets.despawn(allIds) for (uuid in viewerUuids) { @@ -66,8 +67,18 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer despawn.execute(player) } + lock.write { + id2point.clear() + viewerUuids.clear() + sentToPlayers.clear() + } + } + } + + override fun onClose() { + writeLocked { id2point.clear() - viewerUuids.clear() + point2Id.clear() sentToPlayers.clear() } } @@ -77,7 +88,7 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer } override fun stopVisualizingInternal() { - for (viewer in viewers) { + for (viewer in viewerUuids) { val sent = drainSentToPlayer(viewer) ?: continue if (sent.isEmpty()) continue @@ -87,6 +98,7 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer } override fun update(strategy: UpdateStrategy) { + ensureNotClosed() if (!visualizing.get()) return if (!checkNotNullWorld()) return @@ -96,7 +108,7 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer point2Id.object2IntEntrySet().map { it.intValue to it.key } } - for (viewer in viewers) { + for (viewer in viewerUuids) { val sent = drainSentToPlayer(viewer) val player = Bukkit.getPlayer(viewer) ?: continue player.enterContextIfNeeded { @@ -104,6 +116,8 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer nmsSpawnPackets.despawn(sent).execute(player) } + if (!visualizing.get()) return@enterContextIfNeeded + val idsToMarkSent = mutableIntSetOf() val spawn = PacketOperation.start() @@ -123,43 +137,47 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer } UpdateStrategy.POSITION -> { - for (viewer in viewers) { + for (viewer in viewerUuids) { val player = Bukkit.getPlayer(viewer) if (player == null) { drainSentToPlayer(viewer) continue } - val sentSnapshot = getSentToPlayerSnapshot(viewer) - val operation = PacketOperation.start() - val idsToRemove = mutableIntSetOf() + player.enterContextIfNeeded { + if (!visualizing.get()) return@enterContextIfNeeded - val iterator = sentSnapshot.iterator() - while (iterator.hasNext()) { - val id = iterator.nextInt() - val point = readLocked { id2point[id] } + val sentSnapshot = getSentToPlayerSnapshot(viewer) + val operation = PacketOperation.start() + val idsToRemove = mutableIntSetOf() - if (point == null) { - operation + nmsSpawnPackets.despawn(id) - idsToRemove.add(id) - continue - } + val iterator = sentSnapshot.iterator() + while (iterator.hasNext()) { + val id = iterator.nextInt() + val point = readLocked { id2point[id] } - if (player.isChunkVisible(world, point.chunkX, point.chunkZ)) { - operation + updatePositionPacket(id, point) - } else { - operation + nmsSpawnPackets.despawn(id) - idsToRemove.add(id) + if (point == null) { + operation + nmsSpawnPackets.despawn(id) + idsToRemove.add(id) + continue + } + + if (player.isChunkVisible(world, point.chunkX, point.chunkZ)) { + operation + updatePositionPacket(id, point) + } else { + operation + nmsSpawnPackets.despawn(id) + idsToRemove.add(id) + } } - } - if (idsToRemove.isNotEmpty()) { - writeLocked { - getSentToPlayer(viewer)?.removeAll(idsToRemove) + if (idsToRemove.isNotEmpty()) { + writeLocked { + getSentToPlayer(viewer)?.removeAll(idsToRemove) + } } - } - operation.execute(player) + operation.execute(player) + } } } } @@ -169,6 +187,7 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer visualLocation: Vector3d, settings: BlockDisplaySettings, ) { + ensureNotClosed() val id = nmsCommonBridge.nextEntityId() val point = VisualPoint(visualLocation, settings) put(id, point) @@ -176,13 +195,14 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer if (!visualizing.get()) return if (!checkNotNullWorld()) return - for (viewer in viewers) { + for (viewer in viewerUuids) { val player = Bukkit.getPlayer(viewer) ?: continue spawn(player, id, point) } } override fun addVisualLocations(locations: Collection>) { + ensureNotClosed() if (locations.isEmpty()) return if (locations.size == 1) { addVisualLocation(locations.first().first, locations.first().second) @@ -204,9 +224,10 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer toSpawn[id] = point } - for (viewer in viewers) { + for (viewer in viewerUuids) { val player = Bukkit.getPlayer(viewer) ?: continue player.enterContextIfNeeded { + if (!visualizing.get()) return@enterContextIfNeeded val idsToAdd = mutableIntSetOf() val spawnOperation = PacketOperation.start() @@ -230,25 +251,27 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer } override fun removeVisualLocation(visualLocation: Vector3d) { + ensureNotClosed() val result = remove(visualLocation) ?: return val (id, point) = result if (!visualizing.get()) return if (!checkNotNullWorld()) return - for (viewer in viewers) { + for (viewer in viewerUuids) { val player = Bukkit.getPlayer(viewer) ?: continue despawn(player, id, point, true) } } override fun clearVisualLocations() { + ensureNotClosed() val idsToRemove = readLocked { IntOpenHashSet(id2point.keys) } - if (visualizing.get() && checkNotNullWorld() && idsToRemove.isNotEmpty()) { - for (viewer in viewers) { + if (checkNotNullWorld() && idsToRemove.isNotEmpty()) { + for (viewer in viewerUuids) { val player = Bukkit.getPlayer(viewer) ?: continue nmsSpawnPackets.despawn(idsToRemove).execute(player) } @@ -298,7 +321,7 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer private fun spawn(player: Player, id: Int, point: VisualPoint) { player.enterContextIfNeeded { - if (player.isChunkVisible(world, point.chunkX, point.chunkZ)) { + if (visualizing.get() && player.isChunkVisible(world, point.chunkX, point.chunkZ)) { spawnPacket(id, point).execute(player) writeLocked { getOrCreateSentToPlayer(player.uniqueId).add(id) @@ -325,6 +348,9 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer nmsSpawnPackets.teleport(id, point.pos) override fun onPlayerReceiveChunk(player: Player, chunk: Chunk) { + ensureNotClosed() + if (!visualizing.get()) return + val entries = readLocked { point2Id.object2IntEntrySet().map { it.intValue to it.key } } @@ -349,6 +375,9 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer } override fun onPlayerUnloadChunk(player: Player, chunk: Chunk) { + ensureNotClosed() + if (!visualizing.get()) return + val sentSnapshot = getSentToPlayerSnapshot(player.uniqueId) val despawn = mutableIntSetOf() @@ -370,6 +399,13 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer nmsSpawnPackets.despawn(despawn).execute(player) } + override fun onViewerRemoved(player: Player) { + ensureNotClosed() + + val sent = drainSentToPlayer(player.uniqueId) ?: return + if (sent.isEmpty()) return + nmsSpawnPackets.despawn(sent).execute(player) + } fun checkNotNullWorld(): Boolean { if (worldReference.get() == null) { diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerSingleLocationImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerSingleLocationImpl.kt index dbc3b63c3..4c4d70f39 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerSingleLocationImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerSingleLocationImpl.kt @@ -24,6 +24,7 @@ class SurfVisualizerSingleLocationImpl(location: Location) : AbstractSurfVisuali override var location: Location = location set(value) { + ensureNotClosed() field = value update(UpdateStrategy.POSITION) } @@ -32,12 +33,13 @@ class SurfVisualizerSingleLocationImpl(location: Location) : AbstractSurfVisuali blockData = SurfVisualizer.DEFAULT_BLOCK_TYPE.createBlockData() } set(value) { + ensureNotClosed() field = value update() } - override fun createCleanupState(): CleanupState { - return SingleLocationCleanupState(entityId, viewerUuids) + init { + cleaner.register(this, SingleLocationCleanupState(entityId, internalViewerUuids)) } private class SingleLocationCleanupState( @@ -54,30 +56,41 @@ class SurfVisualizerSingleLocationImpl(location: Location) : AbstractSurfVisuali } } + override fun onClose() { + } + override fun startVisualizingInternal() { update() } override fun stopVisualizingInternal() { - for (uuid in viewers) { + for (uuid in viewerUuids) { val player = Bukkit.getPlayer(uuid) ?: continue despawnPacket().execute(player) } } override fun onPlayerReceiveChunk(player: Player, chunk: Chunk) { + ensureNotClosed() if (chunk.world == location.world && location.chunkX == chunk.x && location.chunkZ == chunk.z) { spawnPacket().execute(player) } } override fun onPlayerUnloadChunk(player: Player, chunk: Chunk) { + ensureNotClosed() if (chunk.world == location.world && location.chunkX == chunk.x && location.chunkZ == chunk.z) { despawnPacket().execute(player) } } + override fun onViewerRemoved(player: Player) { + ensureNotClosed() + despawnPacket().execute(player) + } + override fun update(strategy: UpdateStrategy) { + ensureNotClosed() if (!visualizing.get()) return when (strategy) { @@ -85,10 +98,11 @@ class SurfVisualizerSingleLocationImpl(location: Location) : AbstractSurfVisuali val despawn = despawnPacket() val spawn = spawnPacket() - for (viewer in viewers) { + for (viewer in viewerUuids) { val player = Bukkit.getPlayer(viewer) ?: continue player.enterContextIfNeeded { despawn.execute(player) + if (!visualizing.get()) return@enterContextIfNeeded val seesLocation = player.isChunkVisible(location) if (seesLocation) { @@ -100,10 +114,11 @@ class SurfVisualizerSingleLocationImpl(location: Location) : AbstractSurfVisuali UpdateStrategy.POSITION -> { val updatePosition = updatePositionPacket() - for (viewer in viewers) { + for (viewer in viewerUuids) { val player = Bukkit.getPlayer(viewer) ?: continue player.enterContextIfNeeded { if (player.isChunkVisible(location)) { + if (!visualizing.get()) return@enterContextIfNeeded updatePosition.execute(player) } else { despawnPacket().execute(player) @@ -115,6 +130,7 @@ class SurfVisualizerSingleLocationImpl(location: Location) : AbstractSurfVisuali } override fun settings(consumer: BlockDisplaySettings.() -> Unit) { + ensureNotClosed() settings.consumer() update() } diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/collection/TransformingSet2ObjectSet.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/collection/TransformingSet2ObjectSet.kt new file mode 100644 index 000000000..dfa2a0a5b --- /dev/null +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/collection/TransformingSet2ObjectSet.kt @@ -0,0 +1,38 @@ +package dev.slne.surf.surfapi.core.api.collection + +import it.unimi.dsi.fastutil.objects.ObjectIterator +import it.unimi.dsi.fastutil.objects.ObjectSet + +class TransformingSet2ObjectSet( + private val fromSet: MutableSet, + private val toTransformer: (O) -> M?, + private val fromTransformer: (M) -> O?, +) : ObjectSet { + override fun iterator() = object : ObjectIterator { + private val iterator = fromSet.iterator() + override fun remove() = iterator.remove() + override fun next(): M? = toTransformer(iterator.next()) + override fun hasNext() = iterator.hasNext() + } + + override fun add(element: M): Boolean = transformFrom(element)?.let(fromSet::add) == true + override fun remove(element: M) = transformFrom(element)?.let(fromSet::remove) == true + + override fun addAll(elements: Collection) = + fromSet.addAll(elements.mapNotNull(::transformFrom)) + + override fun removeAll(elements: Collection) = + fromSet.removeAll(elements.mapNotNull(::transformFrom).toSet()) + + override fun retainAll(elements: Collection) = + fromSet.retainAll(elements.mapNotNull(::transformFrom).toSet()) + + override fun clear() = fromSet.clear() + override val size: Int get() = fromSet.size + override fun isEmpty() = fromSet.isEmpty() + override fun contains(element: M) = fromSet.contains(transformFrom(element)) + override fun containsAll(elements: Collection) = fromSet.containsAll(elements.mapNotNull(::transformFrom)) + + private fun transformFrom(element: M) = if (element != null) fromTransformer(element) else null + private fun transformTo(element: O) = if (element != null) toTransformer(element) else null +} \ No newline at end of file From a874307399ebd6bd286201128f3236d636163182 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:31:42 +0100 Subject: [PATCH 03/10] refactor: improve visualizer cleanup and state management --- .../visualizer/visualizer/SurfVisualizer.kt | 1 + .../visualizer/SurfBukkitVisualizerApiImpl.kt | 24 ++++--------------- .../visualizer/AbstractSurfVisualizerImpl.kt | 14 ++++++++++- .../visualizer/SurfVisualizerAreaImpl.kt | 6 +---- .../SurfVisualizerMultipleLocationsImpl.kt | 10 +++++++- 5 files changed, 28 insertions(+), 27 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizer.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizer.kt index 8c88bc0ae..be098e8ea 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizer.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizer.kt @@ -110,6 +110,7 @@ interface SurfVisualizer : AutoCloseable { override fun close() // TODO: Provide kdocs + fun isClosed(): Boolean /** * Companion object for the `SurfVisualizer` interface. diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/SurfBukkitVisualizerApiImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/SurfBukkitVisualizerApiImpl.kt index 8bc78a9e2..f6c2e9025 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/SurfBukkitVisualizerApiImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/SurfBukkitVisualizerApiImpl.kt @@ -58,41 +58,25 @@ class SurfBukkitVisualizerApiImpl : SurfBukkitVisualizerApi { } private fun getActiveVisualizers(player: Player) = - visualizers.asMap().values.filter { it.isVisualizing() && it.visibleTo(player) } + visualizers.asMap().values.filter { !it.isClosed() && it.isVisualizing() && it.visibleTo(player) } private val log = logger() fun processChunkReceiveUpdateForPlayer(player: Player, chunk: Chunk) { val active = getActiveVisualizers(player) - - if (active.isNotEmpty()) { - log.atInfo() - .log("Received update for player ${player.name} for ${active.size} visualizers") - } - active.forEach { it.onPlayerReceiveChunk(player, chunk) } } fun processChunkUnloadForPlayer(player: Player, chunk: Chunk) { val active = getActiveVisualizers(player) - - if (active.isNotEmpty()) { - log.atInfo() - .log("Received unload for player ${player.name} for ${active.size} visualizers") - } - active.forEach { it.onPlayerUnloadChunk(player, chunk) } } fun processPlayerQuit(player: Player) { - val active = visualizers.asMap().values - - if (active.isNotEmpty()) { - log.atInfo() - .log("Player ${player.name} quit, removing from ${active.size} visualizers") + for (active in visualizers.asMap().values) { + if (active.isClosed()) continue + active.removeViewer(player) } - - active.forEach { it.removeViewer(player) } } fun onVisualizerClose(visualizer: AbstractSurfVisualizerImpl) { diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/AbstractSurfVisualizerImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/AbstractSurfVisualizerImpl.kt index 0cf1edc9e..308df3629 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/AbstractSurfVisualizerImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/AbstractSurfVisualizerImpl.kt @@ -103,6 +103,8 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { if (internalViewerUuids.remove(player.uniqueId)) { if (player.isOnline) { onViewerRemoved(player) + } else { + clearStaleData(player.uniqueId) } } } @@ -112,7 +114,12 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { val iterator = internalViewerUuids.iterator() while (iterator.hasNext()) { val next = iterator.next() - Bukkit.getPlayer(next)?.let { onViewerRemoved(it) } + val player = Bukkit.getPlayer(next) + if (player != null) { + onViewerRemoved(player) + } else { + clearStaleData(next) + } iterator.remove() } } @@ -133,6 +140,7 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { } abstract fun onViewerRemoved(player: Player) + abstract fun clearStaleData(uuid: UUID) @Synchronized final override fun close() { @@ -145,6 +153,10 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { internalViewerUuids.clear() } + override fun isClosed(): Boolean { + return closed.get() + } + protected abstract fun onClose() protected abstract fun startVisualizingInternal() protected abstract fun stopVisualizingInternal() diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt index ed335d79c..e83ec56f9 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt @@ -111,7 +111,7 @@ class SurfVisualizerAreaImpl( } if (placeDelay.isPositive()) { - for ((i, point) in finalEdgePoints.withIndex()) { + for ((i, point) in finalEdgePoints.convexHull2D().withIndex()) { delegate.addVisualLocation(point, settings) if (i < finalEdgePoints.size - 1) { delay(placeDelay) @@ -119,11 +119,7 @@ class SurfVisualizerAreaImpl( } } else { delegate.addVisualLocations(finalEdgePoints, settings) - finalEdgePoints.forEach { - delegate.addVisualLocation(it, settings) - } } - } override fun equals(other: Any?): Boolean { diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt index cdb7e256c..ab894f7e9 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt @@ -128,6 +128,7 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer } writeLocked { + if (!visualizing.get()) return@enterContextIfNeeded getOrCreateSentToPlayer(viewer).addAll(idsToMarkSent) } @@ -163,6 +164,7 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer } if (player.isChunkVisible(world, point.chunkX, point.chunkZ)) { + if (!visualizing.get()) continue operation + updatePositionPacket(id, point) } else { operation + nmsSpawnPackets.despawn(id) @@ -242,6 +244,7 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer } writeLocked { + if (!visualizing.get()) return@enterContextIfNeeded getOrCreateSentToPlayer(viewer).addAll(idsToAdd) } @@ -357,10 +360,11 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer val spawnOperation = PacketOperation.start() val idsToAdd = mutableIntSetOf() - + val sent = getSentToPlayerSnapshot(player.uniqueId) for ((id, point) in entries) { if (world != chunk.world || point.chunkX != chunk.x || point.chunkZ != chunk.z) continue + if (sent.contains(id)) continue spawnOperation + spawnPacket(id, point) idsToAdd.add(id) } @@ -407,6 +411,10 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer nmsSpawnPackets.despawn(sent).execute(player) } + override fun clearStaleData(uuid: UUID) { + drainSentToPlayer(uuid) + } + fun checkNotNullWorld(): Boolean { if (worldReference.get() == null) { visualizing.set(false) From ec5d4de490ceba9051e35030f78d29ef56576580 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:48:59 +0100 Subject: [PATCH 04/10] refactor: improve viewer management in visualizer API --- .../visualizer/SurfBukkitVisualizerApiImpl.kt | 27 ++++++++++++++----- .../visualizer/AbstractSurfVisualizerImpl.kt | 9 +++++++ .../SurfVisualizerMultipleLocationsImpl.kt | 23 +++++++++++----- .../SurfVisualizerSingleLocationImpl.kt | 4 +++ 4 files changed, 50 insertions(+), 13 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/SurfBukkitVisualizerApiImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/SurfBukkitVisualizerApiImpl.kt index f6c2e9025..ce59c206b 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/SurfBukkitVisualizerApiImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/SurfBukkitVisualizerApiImpl.kt @@ -11,24 +11,26 @@ import dev.slne.surf.surfapi.bukkit.server.impl.visualizer.visualizer.AbstractSu import dev.slne.surf.surfapi.bukkit.server.impl.visualizer.visualizer.SurfVisualizerAreaImpl import dev.slne.surf.surfapi.bukkit.server.impl.visualizer.visualizer.SurfVisualizerMultipleLocationsImpl import dev.slne.surf.surfapi.bukkit.server.impl.visualizer.visualizer.SurfVisualizerSingleLocationImpl -import dev.slne.surf.surfapi.core.api.util.logger import org.bukkit.Chunk import org.bukkit.Location import org.bukkit.World import org.bukkit.entity.Player import org.spongepowered.math.vector.Vector3d import java.util.* +import java.util.concurrent.ConcurrentHashMap import kotlin.time.Duration @AutoService(SurfBukkitVisualizerApi::class) class SurfBukkitVisualizerApiImpl : SurfBukkitVisualizerApi { private val visualizers = Caffeine.newBuilder() - .softValues() + .weakValues() .build() private val areaVisualizers = Caffeine.newBuilder() - .softValues() + .weakValues() .build() + private val playerToVisualizers = ConcurrentHashMap>() + override fun createSingleLocationVisualizer(location: Location): SurfVisualizerSingleLocation { return SurfVisualizerSingleLocationImpl(location).also { visualizers.put(it.uid, it) } } @@ -57,11 +59,22 @@ class SurfBukkitVisualizerApiImpl : SurfBukkitVisualizerApi { return areaVisualizers.getIfPresent(uid) ?: visualizers.getIfPresent(uid) } - private fun getActiveVisualizers(player: Player) = - visualizers.asMap().values.filter { !it.isClosed() && it.isVisualizing() && it.visibleTo(player) } + private fun getActiveVisualizers(player: Player): List { + val visualizerUuids = playerToVisualizers[player.uniqueId] ?: return emptyList() + return visualizerUuids.mapNotNull { uid -> + visualizers.getIfPresent(uid)?.takeIf { !it.isClosed() && it.isVisualizing() } + } + } + fun onViewerAdded(visualizerUid: UUID, playerUid: UUID) { + playerToVisualizers.computeIfAbsent(playerUid) { ConcurrentHashMap.newKeySet() } + .add(visualizerUid) + } + + fun onViewerRemoved(visualizerUid: UUID, playerUid: UUID) { + playerToVisualizers[playerUid]?.remove(visualizerUid) + } - private val log = logger() fun processChunkReceiveUpdateForPlayer(player: Player, chunk: Chunk) { val active = getActiveVisualizers(player) active.forEach { it.onPlayerReceiveChunk(player, chunk) } @@ -73,6 +86,8 @@ class SurfBukkitVisualizerApiImpl : SurfBukkitVisualizerApi { } fun processPlayerQuit(player: Player) { + playerToVisualizers.remove(player.uniqueId) + for (active in visualizers.asMap().values) { if (active.isClosed()) continue active.removeViewer(player) diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/AbstractSurfVisualizerImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/AbstractSurfVisualizerImpl.kt index 308df3629..d6d896b24 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/AbstractSurfVisualizerImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/AbstractSurfVisualizerImpl.kt @@ -94,6 +94,7 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { override fun addViewer(player: Player) { ensureNotClosed() if (player.isOnline && internalViewerUuids.add(player.uniqueId)) { + visualizerApiImpl.onViewerAdded(uid, player.uniqueId) onViewerAdded(player) } } @@ -101,6 +102,7 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { override fun removeViewer(player: Player) { ensureNotClosed() if (internalViewerUuids.remove(player.uniqueId)) { + visualizerApiImpl.onViewerRemoved(uid, player.uniqueId) if (player.isOnline) { onViewerRemoved(player) } else { @@ -115,11 +117,14 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { while (iterator.hasNext()) { val next = iterator.next() val player = Bukkit.getPlayer(next) + + visualizerApiImpl.onViewerRemoved(uid, next) if (player != null) { onViewerRemoved(player) } else { clearStaleData(next) } + iterator.remove() } } @@ -150,6 +155,10 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { stopVisualizing(true) onClose() + + for (viewUuid in internalViewerUuids) { + visualizerApiImpl.onViewerRemoved(uid, viewUuid) + } internalViewerUuids.clear() } diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt index ab894f7e9..6449ee610 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt @@ -10,8 +10,10 @@ import dev.slne.surf.surfapi.bukkit.api.nms.bridges.packets.entity.nmsSpawnPacke import dev.slne.surf.surfapi.bukkit.api.util.isChunkVisible import dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer.SurfVisualizerMultipleLocations import dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer.UpdateStrategy +import dev.slne.surf.surfapi.bukkit.server.impl.visualizer.visualizerApiImpl import dev.slne.surf.surfapi.core.api.util.* import it.unimi.dsi.fastutil.ints.Int2ObjectMap +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.ints.IntOpenHashSet import it.unimi.dsi.fastutil.ints.IntSet import it.unimi.dsi.fastutil.objects.Object2ObjectMap @@ -47,10 +49,11 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer private inline fun writeLocked(block: () -> R): R = lock.write(block) init { - cleaner.register(this, MultiLocationCleanupState(id2point, internalViewerUuids, sentToPlayers, lock)) + cleaner.register(this, MultiLocationCleanupState(uid, id2point, internalViewerUuids, sentToPlayers, lock)) } private class MultiLocationCleanupState( + private val visualizerUuid: UUID, private val id2point: Int2ObjectMap, private val viewerUuids: MutableSet, private val sentToPlayers: Object2ObjectMap, @@ -63,6 +66,7 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer val despawn = nmsSpawnPackets.despawn(allIds) for (uuid in viewerUuids) { + visualizerApiImpl.onViewerRemoved(visualizerUuid, uuid) val player = Bukkit.getPlayer(uuid) ?: continue despawn.execute(player) } @@ -105,7 +109,7 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer when (strategy) { UpdateStrategy.ALL -> { val pointsSnapshot = readLocked { - point2Id.object2IntEntrySet().map { it.intValue to it.key } + Int2ObjectOpenHashMap(id2point) } for (viewer in viewerUuids) { @@ -121,7 +125,9 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer val idsToMarkSent = mutableIntSetOf() val spawn = PacketOperation.start() - for ((id, point) in pointsSnapshot) { + pointsSnapshot.int2ObjectEntrySet().fastForEach { entry -> + val id = entry.intKey + val point = entry.value if (player.isChunkVisible(world, point.chunkX, point.chunkZ) && idsToMarkSent.add(id)) { spawn + spawnPacket(id, point) } @@ -355,16 +361,19 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer if (!visualizing.get()) return val entries = readLocked { - point2Id.object2IntEntrySet().map { it.intValue to it.key } + Int2ObjectOpenHashMap(id2point) } val spawnOperation = PacketOperation.start() val idsToAdd = mutableIntSetOf() val sent = getSentToPlayerSnapshot(player.uniqueId) - for ((id, point) in entries) { - if (world != chunk.world || point.chunkX != chunk.x || point.chunkZ != chunk.z) continue - if (sent.contains(id)) continue + entries.int2ObjectEntrySet().fastForEach { entry -> + val id = entry.intKey + val point = entry.value + if (world != chunk.world || point.chunkX != chunk.x || point.chunkZ != chunk.z) return@fastForEach + if (sent.contains(id)) return@fastForEach + spawnOperation + spawnPacket(id, point) idsToAdd.add(id) } diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerSingleLocationImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerSingleLocationImpl.kt index 4c4d70f39..a36a76ae8 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerSingleLocationImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerSingleLocationImpl.kt @@ -89,6 +89,10 @@ class SurfVisualizerSingleLocationImpl(location: Location) : AbstractSurfVisuali despawnPacket().execute(player) } + override fun clearStaleData(uuid: UUID) { + + } + override fun update(strategy: UpdateStrategy) { ensureNotClosed() if (!visualizing.get()) return From 47a45e877e8edcf9247ec70a4909bd5e2df2a2ee Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:59:26 +0100 Subject: [PATCH 05/10] refactor: optimize chunk key management and streamline visual location updates --- .../surfapi/bukkit/api/util/bukkit-util.kt | 24 +++++++------------ .../visualizer/SurfVisualizerAreaImpl.kt | 6 ++--- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/util/bukkit-util.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/util/bukkit-util.kt index 8eba698bb..9def8669c 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/util/bukkit-util.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/util/bukkit-util.kt @@ -7,10 +7,10 @@ import com.github.shynixn.mccoroutine.folia.entityDispatcher import com.github.shynixn.mccoroutine.folia.regionDispatcher import dev.slne.surf.surfapi.bukkit.api.SurfBukkitApi import dev.slne.surf.surfapi.core.api.util.getCallerClass -import dev.slne.surf.surfapi.core.api.util.mutableLong2ObjectMapOf import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf import io.papermc.paper.math.BlockPosition import io.papermc.paper.math.Position +import it.unimi.dsi.fastutil.longs.LongOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectList import kotlinx.coroutines.* import kotlinx.coroutines.future.await @@ -230,17 +230,16 @@ fun ChunkSnapshot.getHighestBlockYAtBlockCoordinates( } suspend fun Collection.computeHighestYBlock(world: World): ObjectList { - val byChunk = mutableLong2ObjectMapOf>(size / 4 + 1) + val chunkKeys = LongOpenHashSet(size / 4 + 1) for (point in this) { val key = Chunk.getChunkKey(point.x() shr 4, point.z() shr 4) - val list = byChunk.computeIfAbsent(key) { mutableObjectListOf() } - list.add(point) + chunkKeys.add(key) } - val snapshots = ConcurrentHashMap(byChunk.size) + val snapshots = ConcurrentHashMap(chunkKeys.size) coroutineScope { val semaphore = Semaphore(16) // Limit concurrent chunk loads to prevent overwhelming the server - val iterator = byChunk.keys.iterator() + val iterator = chunkKeys.iterator() while (iterator.hasNext()) { val key = iterator.nextLong() launch { @@ -255,16 +254,11 @@ suspend fun Collection.computeHighestYBlock(world: World): ObjectList< } val result = mutableObjectListOf(size) - val it = byChunk.long2ObjectEntrySet().fastIterator() - while (it.hasNext()) { - val entry = it.next() - val key = entry.longKey - val pointsInChunk = entry.value + for (point in this) { + val key = Chunk.getChunkKey(point.x() shr 4, point.z() shr 4) val snapshot = snapshots[key] ?: error("ChunkSnapshot for key $key not found") - for (point in pointsInChunk) { - val y = snapshot.getHighestBlockYAtBlockCoordinates(point.x(), point.z()) - result.add(Vector3i(point.x(), y, point.z())) - } + val y = snapshot.getHighestBlockYAtBlockCoordinates(point.x(), point.z()) + result.add(Vector3i(point.x(), y, point.z())) } return result diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt index e83ec56f9..320afc56e 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt @@ -88,8 +88,8 @@ class SurfVisualizerAreaImpl( private suspend fun recompute() = recomputationMutex.withLock { delegate.clearVisualLocations() - if (corners.size < 2) return - if (!delegate.checkNotNullWorld()) return + if (corners.size < 2) return@withLock + if (!delegate.checkNotNullWorld()) return@withLock val hull = corners.convexHull2D() val cornerBlocks = hull @@ -111,7 +111,7 @@ class SurfVisualizerAreaImpl( } if (placeDelay.isPositive()) { - for ((i, point) in finalEdgePoints.convexHull2D().withIndex()) { + for ((i, point) in finalEdgePoints.withIndex()) { delegate.addVisualLocation(point, settings) if (i < finalEdgePoints.size - 1) { delay(placeDelay) From 26f895c953dacbb15e2c9df23f4ca0aeaee8b21c Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Fri, 20 Mar 2026 22:04:59 +0100 Subject: [PATCH 06/10] refactor: enhance visualizer state management and ensure thread safety in chunk handling --- .../bukkit/api/region/TickThreadGuard.kt | 26 +++++ .../surfapi/bukkit/api/util/bukkit-util.kt | 13 ++- .../visualizer/SurfVisualizerArea.kt | 4 +- .../SurfBukkitPacketListenerApiImpl.kt | 2 +- .../server/impl/region/TickThreadGuardImpl.kt | 71 +++++++++++++ .../visualizer/AbstractSurfVisualizerImpl.kt | 12 ++- .../visualizer/SurfVisualizerAreaImpl.kt | 77 +++++++++++---- .../SurfVisualizerMultipleLocationsImpl.kt | 99 ++++++++++++++----- .../bukkit/server/nms/nms-extensions.kt | 2 +- 9 files changed, 255 insertions(+), 51 deletions(-) create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/region/TickThreadGuard.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/region/TickThreadGuardImpl.kt diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/region/TickThreadGuard.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/region/TickThreadGuard.kt new file mode 100644 index 000000000..af88e3a4f --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/region/TickThreadGuard.kt @@ -0,0 +1,26 @@ +package dev.slne.surf.surfapi.bukkit.api.region + +import dev.slne.surf.surfapi.core.api.util.requiredService +import io.papermc.paper.math.Position +import org.bukkit.World +import org.bukkit.entity.Entity +import org.bukkit.util.BoundingBox + +@Suppress("UnstableApiUsage") +interface TickThreadGuard { + + fun ensureTickThread(world: World, pos: Position, reason: String) + fun ensureTickThread(world: World, pos: Position, blockRadius: Int, reason: String) + fun ensureTickThread(world: World, chunkX: Int, chunkZ: Int, reason: String) + + fun ensureTickThread(entity: Entity, reason: String) + + fun ensureTickThread(world: World, box: BoundingBox, reason: String) + fun ensureTickThread(world: World, blockX: Double, blockZ: Double, reason: String) + + companion object : TickThreadGuard by tickThreadGuard { + val instance = tickThreadGuard + } +} + +val tickThreadGuard = requiredService() \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/util/bukkit-util.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/util/bukkit-util.kt index 9def8669c..8b0515acc 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/util/bukkit-util.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/util/bukkit-util.kt @@ -244,10 +244,17 @@ suspend fun Collection.computeHighestYBlock(world: World): ObjectList< val key = iterator.nextLong() launch { semaphore.withPermit { - val snapshot = world.getChunkAtAsync(getXFromChunkKey(key), getZFromChunkKey(key)) + val chunk = world.getChunkAtAsync(getXFromChunkKey(key), getZFromChunkKey(key)) .await() - .getChunkSnapshot(true, false, false, false) - snapshots[key] = snapshot + + withContext( + JavaPlugin.getProvidingPlugin(SurfBukkitApi::class.java) + .regionDispatcher(world, chunk.x, chunk.z) + ) { + val snapshot = chunk + .getChunkSnapshot(true, false, false, false) + snapshots[key] = snapshot + } } } } diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizerArea.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizerArea.kt index ad42f8753..c55666dba 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizerArea.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizerArea.kt @@ -2,7 +2,7 @@ package dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer import dev.slne.surf.surfapi.bukkit.api.nms.bridges.packets.entity.BlockDisplaySettings import it.unimi.dsi.fastutil.objects.ObjectSet -import org.jetbrains.annotations.UnmodifiableView +import org.jetbrains.annotations.Unmodifiable import org.spongepowered.math.vector.Vector3d /** @@ -27,7 +27,7 @@ interface SurfVisualizerArea : SurfVisualizer { * * This property is part of the experimental API and may be subject to changes in the future. */ - val cornerLocations: @UnmodifiableView ObjectSet + val cornerLocations: @Unmodifiable ObjectSet /** * Adds a location to the set of corner locations in the visualizer area. diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/packet/listener/SurfBukkitPacketListenerApiImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/packet/listener/SurfBukkitPacketListenerApiImpl.kt index 3160966da..9ca37b9fb 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/packet/listener/SurfBukkitPacketListenerApiImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/packet/listener/SurfBukkitPacketListenerApiImpl.kt @@ -211,7 +211,7 @@ class SurfBukkitPacketListenerApiImpl : SurfBukkitPacketListenerApi { } fun interface ListenerResultConverter { - fun convert(result: T, packet: Packet<*>?): Packet<*>? + fun convert(result: T?, packet: Packet<*>?): Packet<*>? } fun interface ListenerInvoker { diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/region/TickThreadGuardImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/region/TickThreadGuardImpl.kt new file mode 100644 index 000000000..7e19f6a69 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/region/TickThreadGuardImpl.kt @@ -0,0 +1,71 @@ +package dev.slne.surf.surfapi.bukkit.server.impl.region + +import ca.spottedleaf.moonrise.common.util.TickThread +import com.google.auto.service.AutoService +import dev.slne.surf.surfapi.bukkit.api.region.TickThreadGuard +import dev.slne.surf.surfapi.bukkit.api.util.chunkX +import dev.slne.surf.surfapi.bukkit.api.util.chunkZ +import dev.slne.surf.surfapi.bukkit.server.nms.toNms +import dev.slne.surf.surfapi.core.api.util.checkInstantiationByServiceLoader +import io.papermc.paper.math.Position +import net.minecraft.core.BlockPos +import net.minecraft.world.phys.AABB +import org.bukkit.World +import org.bukkit.entity.Entity +import org.bukkit.util.BoundingBox + +@Suppress("UnstableApiUsage") +@AutoService(TickThreadGuard::class) +class TickThreadGuardImpl : TickThreadGuard { + init { + checkInstantiationByServiceLoader() + } + + override fun ensureTickThread(world: World, pos: Position, reason: String) { + TickThread.ensureTickThread(world.toNms(), pos.chunkX, pos.chunkZ, reason) + } + + override fun ensureTickThread( + world: World, + pos: Position, + blockRadius: Int, + reason: String + ) { + TickThread.ensureTickThread( + world.toNms(), + BlockPos(pos.blockX(), pos.blockY(), pos.blockZ()), + blockRadius, + reason + ) + } + + override fun ensureTickThread( + world: World, + chunkX: Int, + chunkZ: Int, + reason: String + ) { + TickThread.ensureTickThread(world.toNms(), chunkX, chunkZ, reason) + } + + override fun ensureTickThread(entity: Entity, reason: String) { + TickThread.ensureTickThread(entity.toNms(), reason) + } + + override fun ensureTickThread(world: World, box: BoundingBox, reason: String) { + TickThread.ensureTickThread( + world.toNms(), + AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ), + reason + ) + } + + override fun ensureTickThread( + world: World, + blockX: Double, + blockZ: Double, + reason: String + ) { + TickThread.ensureTickThread(world.toNms(), blockX, blockZ, reason) + } +} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/AbstractSurfVisualizerImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/AbstractSurfVisualizerImpl.kt index d6d896b24..df85c9a78 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/AbstractSurfVisualizerImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/AbstractSurfVisualizerImpl.kt @@ -15,6 +15,7 @@ import java.lang.ref.Cleaner import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong abstract class AbstractSurfVisualizerImpl : SurfVisualizer { protected val log = logger() @@ -34,6 +35,7 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { protected val visualizing = AtomicBoolean(false) protected val closed = AtomicBoolean(false) + private val stateVersion = AtomicLong(0) abstract class CleanupState : Runnable { private val log = logger() @@ -54,6 +56,7 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { override fun startVisualizing(): Boolean { ensureNotClosed() if (!visualizing.compareAndSet(false, true)) return false + nextStateVersion() try { startVisualizingInternal() @@ -78,6 +81,8 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { ensureNotClosed() } + nextStateVersion() + try { stopVisualizingInternal() return true @@ -136,8 +141,9 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { ensureNotClosed() if (!visualizing.get()) return + val version = currentStateVersion() player.enterContextIfNeeded { - if (!visualizing.get()) return@enterContextIfNeeded + if (!isActiveVersion(version)) return@enterContextIfNeeded player.sentChunks.forEach { chunk -> onPlayerReceiveChunk(player, chunk) } @@ -189,6 +195,10 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { } } + protected fun nextStateVersion(): Long = stateVersion.incrementAndGet() + protected fun currentStateVersion(): Long = stateVersion.get() + protected fun isActiveVersion(version: Long): Boolean = visualizing.get() && stateVersion.get() == version + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is AbstractSurfVisualizerImpl) return false diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt index 320afc56e..27212c449 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt @@ -1,6 +1,6 @@ package dev.slne.surf.surfapi.bukkit.server.impl.visualizer.visualizer -import com.github.shynixn.mccoroutine.folia.launch +import com.github.shynixn.mccoroutine.folia.scope import dev.slne.surf.surfapi.bukkit.api.nms.bridges.packets.entity.BlockDisplaySettings import dev.slne.surf.surfapi.bukkit.api.util.computeHighestYBlock import dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer.SurfVisualizer @@ -12,11 +12,12 @@ import dev.slne.surf.surfapi.core.api.math.VoxelLineTracer import dev.slne.surf.surfapi.core.api.util.toObjectSet import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectSet -import kotlinx.coroutines.delay -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.consumeEach import org.bukkit.World import org.spongepowered.math.vector.Vector3d +import java.util.concurrent.ConcurrentHashMap import kotlin.time.Duration class SurfVisualizerAreaImpl( @@ -30,9 +31,13 @@ class SurfVisualizerAreaImpl( ), ) : SurfVisualizer by delegate, SurfVisualizerArea { - private val corners = ObjectLinkedOpenHashSet(initialEdges) - override val cornerLocations by lazy { corners.toObjectSet() } - private val recomputationMutex = Mutex() + private val corners = ConcurrentHashMap.newKeySet(initialEdges.size) + override val cornerLocations get() = corners.toObjectSet() + private val computationChannel = Channel(Channel.CONFLATED) + + private val scope = CoroutineScope( + plugin.scope.coroutineContext + SupervisorJob(plugin.scope.coroutineContext[Job]) + ) override var settings: BlockDisplaySettings = initialSettings ?: BlockDisplaySettings.create { blockData = SurfVisualizer.DEFAULT_BLOCK_TYPE.createBlockData() @@ -43,9 +48,16 @@ class SurfVisualizerAreaImpl( } init { - if (initialEdges.isNotEmpty()) { + corners.addAll(initialEdges) + if (corners.isNotEmpty()) { launchRecompute() } + + scope.launch { + computationChannel.consumeEach { + recompute() + } + } } override fun settings(consumer: BlockDisplaySettings.() -> Unit) { @@ -81,26 +93,35 @@ class SurfVisualizerAreaImpl( } private fun launchRecompute() { - plugin.launch { - recompute() - } + computationChannel.trySend(Unit) +// plugin.launch { +// recompute() +// } } - private suspend fun recompute() = recomputationMutex.withLock { + private suspend fun recompute() { + if (delegate.isClosed()) return + val cornersSnapshot = ObjectLinkedOpenHashSet(corners) + val settingsSnapshot = settings.clone() + delegate.clearVisualLocations() - if (corners.size < 2) return@withLock - if (!delegate.checkNotNullWorld()) return@withLock + if (cornersSnapshot.size < 2) return + if (!delegate.checkNotNullWorld()) return - val hull = corners.convexHull2D() - val cornerBlocks = hull + currentCoroutineContext().ensureActive() + val hull = cornersSnapshot.convexHull2D() val edgePoints = ObjectLinkedOpenHashSet() - for (i in cornerBlocks.indices) { + + for (i in hull.indices) { edgePoints += VoxelLineTracer.trace( - cornerBlocks[i], - cornerBlocks[(i + 1) % cornerBlocks.size] + hull[i], + hull[(i + 1) % hull.size] ) } + + currentCoroutineContext().ensureActive() + val finalEdgePoints: ObjectSet = if (useHighestYBlock) { edgePoints.map { it.toInt() } .computeHighestYBlock(delegate.world) @@ -110,18 +131,32 @@ class SurfVisualizerAreaImpl( edgePoints } + currentCoroutineContext().ensureActive() + if (placeDelay.isPositive()) { for ((i, point) in finalEdgePoints.withIndex()) { - delegate.addVisualLocation(point, settings) + currentCoroutineContext().ensureActive() + delegate.addVisualLocation(point, settingsSnapshot) if (i < finalEdgePoints.size - 1) { delay(placeDelay) } } } else { - delegate.addVisualLocations(finalEdgePoints, settings) + delegate.addVisualLocations(finalEdgePoints, settingsSnapshot) } } + override fun close() { + scope.cancel("Visualizer closed.") + computationChannel.close() + delegate.close() + } + + override fun stopVisualizing(): Boolean { + scope.coroutineContext[Job]?.children?.forEach { it.cancel() } + return delegate.stopVisualizing() + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is SurfVisualizerAreaImpl) return false diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt index 6449ee610..396df9073 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt @@ -7,6 +7,7 @@ import dev.slne.surf.surfapi.bukkit.api.nms.bridges.nmsCommonBridge import dev.slne.surf.surfapi.bukkit.api.nms.bridges.packets.PacketOperation import dev.slne.surf.surfapi.bukkit.api.nms.bridges.packets.entity.BlockDisplaySettings import dev.slne.surf.surfapi.bukkit.api.nms.bridges.packets.entity.nmsSpawnPackets +import dev.slne.surf.surfapi.bukkit.api.region.TickThreadGuard import dev.slne.surf.surfapi.bukkit.api.util.isChunkVisible import dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer.SurfVisualizerMultipleLocations import dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer.UpdateStrategy @@ -92,12 +93,19 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer } override fun stopVisualizingInternal() { - for (viewer in viewerUuids) { - val sent = drainSentToPlayer(viewer) ?: continue - if (sent.isEmpty()) continue + val sentByViewer = writeLocked { + viewerUuids.associateWith { viewerUuid -> + IntOpenHashSet(sentToPlayers.remove(viewerUuid) ?: IntOpenHashSet()) + } + } - val player = Bukkit.getPlayer(viewer) ?: continue - nmsSpawnPackets.despawn(sent).execute(player) + for ((viewerUuid, ids) in sentByViewer) { + if (ids.isEmpty()) continue + + val player = Bukkit.getPlayer(viewerUuid) ?: continue + player.enterContextIfNeeded { + nmsSpawnPackets.despawn(ids).execute(player) + } } } @@ -112,12 +120,20 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer Int2ObjectOpenHashMap(id2point) } + val version = currentStateVersion() for (viewer in viewerUuids) { - val sent = drainSentToPlayer(viewer) - val player = Bukkit.getPlayer(viewer) ?: continue + val player = Bukkit.getPlayer(viewer) + if (player == null) { + clearStaleData(viewer) + continue + } + player.enterContextIfNeeded { - if (!sent.isNullOrEmpty()) { - nmsSpawnPackets.despawn(sent).execute(player) + if (!isActiveVersion(version)) return@enterContextIfNeeded + + val previouslySent = drainSentToPlayer(viewer) + if (!previouslySent.isNullOrEmpty()) { + nmsSpawnPackets.despawn(previouslySent).execute(player) } if (!visualizing.get()) return@enterContextIfNeeded @@ -144,27 +160,30 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer } UpdateStrategy.POSITION -> { + val version = currentStateVersion() for (viewer in viewerUuids) { val player = Bukkit.getPlayer(viewer) if (player == null) { - drainSentToPlayer(viewer) + clearStaleData(viewer) continue } player.enterContextIfNeeded { + if (!isActiveVersion(version)) return@enterContextIfNeeded if (!visualizing.get()) return@enterContextIfNeeded val sentSnapshot = getSentToPlayerSnapshot(viewer) + val pointsSnapshot = readLocked { Int2ObjectOpenHashMap(id2point) } + val operation = PacketOperation.start() val idsToRemove = mutableIntSetOf() val iterator = sentSnapshot.iterator() while (iterator.hasNext()) { val id = iterator.nextInt() - val point = readLocked { id2point[id] } + val point = pointsSnapshot[id] if (point == null) { - operation + nmsSpawnPackets.despawn(id) idsToRemove.add(id) continue } @@ -173,7 +192,6 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer if (!visualizing.get()) continue operation + updatePositionPacket(id, point) } else { - operation + nmsSpawnPackets.despawn(id) idsToRemove.add(id) } } @@ -182,6 +200,7 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer writeLocked { getSentToPlayer(viewer)?.removeAll(idsToRemove) } + nmsSpawnPackets.despawn(idsToRemove).execute(player) } operation.execute(player) @@ -204,7 +223,12 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer if (!checkNotNullWorld()) return for (viewer in viewerUuids) { - val player = Bukkit.getPlayer(viewer) ?: continue + val player = Bukkit.getPlayer(viewer) + if (player == null) { + clearStaleData(viewer) + continue + } + spawn(player, id, point) } } @@ -232,9 +256,16 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer toSpawn[id] = point } + val version = currentStateVersion() for (viewer in viewerUuids) { - val player = Bukkit.getPlayer(viewer) ?: continue + val player = Bukkit.getPlayer(viewer) + if (player == null) { + clearStaleData(viewer) + continue + } + player.enterContextIfNeeded { + if (!isActiveVersion(version)) return@enterContextIfNeeded if (!visualizing.get()) return@enterContextIfNeeded val idsToAdd = mutableIntSetOf() val spawnOperation = PacketOperation.start() @@ -268,7 +299,12 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer if (!checkNotNullWorld()) return for (viewer in viewerUuids) { - val player = Bukkit.getPlayer(viewer) ?: continue + val player = Bukkit.getPlayer(viewer) + if (player == null) { + clearStaleData(viewer) + continue + } + despawn(player, id, point, true) } } @@ -279,10 +315,19 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer IntOpenHashSet(id2point.keys) } + val version = currentStateVersion() if (checkNotNullWorld() && idsToRemove.isNotEmpty()) { for (viewer in viewerUuids) { - val player = Bukkit.getPlayer(viewer) ?: continue - nmsSpawnPackets.despawn(idsToRemove).execute(player) + val player = Bukkit.getPlayer(viewer) + if (player == null) { + clearStaleData(viewer) + continue + } + + player.enterContextIfNeeded { + if (!isActiveVersion(version)) return@enterContextIfNeeded + nmsSpawnPackets.despawn(idsToRemove).execute(player) + } } } @@ -329,23 +374,27 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer } private fun spawn(player: Player, id: Int, point: VisualPoint) { + val version = currentStateVersion() player.enterContextIfNeeded { + if (!isActiveVersion(version)) return@enterContextIfNeeded if (visualizing.get() && player.isChunkVisible(world, point.chunkX, point.chunkZ)) { - spawnPacket(id, point).execute(player) writeLocked { getOrCreateSentToPlayer(player.uniqueId).add(id) } + spawnPacket(id, point).execute(player) } } } private fun despawn(player: Player, id: Int, point: VisualPoint, force: Boolean = false) { + val version = currentStateVersion() player.enterContextIfNeeded { + if (!isActiveVersion(version)) return@enterContextIfNeeded if (force || !player.isChunkVisible(world, point.chunkX, point.chunkZ)) { - nmsSpawnPackets.despawn(id).execute(player) writeLocked { getOrCreateSentToPlayer(player.uniqueId).remove(id) } + nmsSpawnPackets.despawn(id).execute(player) } } } @@ -359,6 +408,7 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer override fun onPlayerReceiveChunk(player: Player, chunk: Chunk) { ensureNotClosed() if (!visualizing.get()) return + TickThreadGuard.ensureTickThread(player, "Cannot receive async chunk load for visualizer") val entries = readLocked { Int2ObjectOpenHashMap(id2point) @@ -390,14 +440,16 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer override fun onPlayerUnloadChunk(player: Player, chunk: Chunk) { ensureNotClosed() if (!visualizing.get()) return + TickThreadGuard.ensureTickThread(player, "Cannot receive async chunk unload for visualizer") val sentSnapshot = getSentToPlayerSnapshot(player.uniqueId) + val pointsSnapshot = readLocked { Int2ObjectOpenHashMap(id2point) } val despawn = mutableIntSetOf() val iterator = sentSnapshot.iterator() while (iterator.hasNext()) { val id = iterator.nextInt() - val point = readLocked { id2point[id] } + val point = pointsSnapshot[id] if (point != null && (world != chunk.world || point.chunkX != chunk.x || point.chunkZ != chunk.z)) { continue } @@ -417,7 +469,10 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer val sent = drainSentToPlayer(player.uniqueId) ?: return if (sent.isEmpty()) return - nmsSpawnPackets.despawn(sent).execute(player) + + player.enterContextIfNeeded { + nmsSpawnPackets.despawn(sent).execute(player) + } } override fun clearStaleData(uuid: UUID) { diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/nms/nms-extensions.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/nms/nms-extensions.kt index ee14b83a6..59b506271 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/nms/nms-extensions.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/nms/nms-extensions.kt @@ -133,7 +133,7 @@ fun EntityType.toNmsHolder() = CraftEntityType.bukkitToMinecraftHolder(this) as Holder.Reference> fun World.toNms(): ServerLevel = (this as CraftWorld).handle -fun Entity.toNms(): net.minecraft.world.entity.Entity = (this as CraftEntity).handle +fun Entity.toNms(): net.minecraft.world.entity.Entity = (this as CraftEntity).handleRaw fun LivingEntity.toNms(): net.minecraft.world.entity.LivingEntity = (this as CraftLivingEntity).handle fun DamageSource.toNms(): net.minecraft.world.damagesource.DamageSource = (this as CraftDamageSource).handle From 3f2720a9db2e200f44343125bb457770a0c7a656 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Fri, 20 Mar 2026 22:37:26 +0100 Subject: [PATCH 07/10] chore: bump abi --- .../api/surf-api-bukkit-api.api | 29 ++++++++++++++++++- .../api/surf-api-core-api.api | 19 ++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/api/surf-api-bukkit-api.api b/surf-api-bukkit/surf-api-bukkit-api/api/surf-api-bukkit-api.api index c99f2854a..6047b745f 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/api/surf-api-bukkit-api.api +++ b/surf-api-bukkit/surf-api-bukkit-api/api/surf-api-bukkit-api.api @@ -2252,6 +2252,30 @@ public abstract class dev/slne/surf/surfapi/bukkit/api/permission/PermissionRegi public final fun create (Ljava/lang/String;)Ljava/lang/String; } +public abstract interface class dev/slne/surf/surfapi/bukkit/api/region/TickThreadGuard { + public static final field Companion Ldev/slne/surf/surfapi/bukkit/api/region/TickThreadGuard$Companion; + public abstract fun ensureTickThread (Lorg/bukkit/World;DDLjava/lang/String;)V + public abstract fun ensureTickThread (Lorg/bukkit/World;IILjava/lang/String;)V + public abstract fun ensureTickThread (Lorg/bukkit/World;Lio/papermc/paper/math/Position;ILjava/lang/String;)V + public abstract fun ensureTickThread (Lorg/bukkit/World;Lio/papermc/paper/math/Position;Ljava/lang/String;)V + public abstract fun ensureTickThread (Lorg/bukkit/World;Lorg/bukkit/util/BoundingBox;Ljava/lang/String;)V + public abstract fun ensureTickThread (Lorg/bukkit/entity/Entity;Ljava/lang/String;)V +} + +public final class dev/slne/surf/surfapi/bukkit/api/region/TickThreadGuard$Companion : dev/slne/surf/surfapi/bukkit/api/region/TickThreadGuard { + public fun ensureTickThread (Lorg/bukkit/World;DDLjava/lang/String;)V + public fun ensureTickThread (Lorg/bukkit/World;IILjava/lang/String;)V + public fun ensureTickThread (Lorg/bukkit/World;Lio/papermc/paper/math/Position;ILjava/lang/String;)V + public fun ensureTickThread (Lorg/bukkit/World;Lio/papermc/paper/math/Position;Ljava/lang/String;)V + public fun ensureTickThread (Lorg/bukkit/World;Lorg/bukkit/util/BoundingBox;Ljava/lang/String;)V + public fun ensureTickThread (Lorg/bukkit/entity/Entity;Ljava/lang/String;)V + public final fun getInstance ()Ldev/slne/surf/surfapi/bukkit/api/region/TickThreadGuard; +} + +public final class dev/slne/surf/surfapi/bukkit/api/region/TickThreadGuardKt { + public static final fun getTickThreadGuard ()Ldev/slne/surf/surfapi/bukkit/api/region/TickThreadGuard; +} + public abstract interface annotation class dev/slne/surf/surfapi/bukkit/api/scoreboard/ObsoleteScoreboardApi : java/lang/annotation/Annotation { } @@ -2466,14 +2490,17 @@ public final class dev/slne/surf/surfapi/bukkit/api/visualizer/SurfBukkitVisuali public abstract interface annotation class dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/ExperimentalVisualizerApi : java/lang/annotation/Annotation { } -public abstract interface class dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizer { +public abstract interface class dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizer : java/lang/AutoCloseable { public static final field Companion Ldev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizer$Companion; public static final field DEFAULT_BLOCK_TYPE Lorg/bukkit/block/BlockType; public abstract fun addViewer (Lorg/bukkit/entity/Player;)V public abstract fun clearViewers ()V + public abstract fun close ()V public abstract fun getUid ()Ljava/util/UUID; + public abstract fun getViewerUuids ()Ljava/util/Set; public abstract fun getViewers ()Lit/unimi/dsi/fastutil/objects/ObjectSet; public abstract fun hasViewers ()Z + public abstract fun isClosed ()Z public abstract fun isVisualizing ()Z public abstract fun removeViewer (Lorg/bukkit/entity/Player;)V public abstract fun startVisualizing ()Z diff --git a/surf-api-core/surf-api-core-api/api/surf-api-core-api.api b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api index a09f0d4b3..742d63e2e 100644 --- a/surf-api-core/surf-api-core-api/api/surf-api-core-api.api +++ b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api @@ -49,6 +49,25 @@ public final class dev/slne/surf/surfapi/core/api/collection/TransformingObjectS public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; } +public final class dev/slne/surf/surfapi/core/api/collection/TransformingSet2ObjectSet : it/unimi/dsi/fastutil/objects/ObjectSet { + public fun (Ljava/util/Set;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V + public fun add (Ljava/lang/Object;)Z + public fun addAll (Ljava/util/Collection;)Z + public fun clear ()V + public fun contains (Ljava/lang/Object;)Z + public fun containsAll (Ljava/util/Collection;)Z + public fun getSize ()I + public fun isEmpty ()Z + public fun iterator ()Lit/unimi/dsi/fastutil/objects/ObjectIterator; + public synthetic fun iterator ()Ljava/util/Iterator; + public fun remove (Ljava/lang/Object;)Z + public fun removeAll (Ljava/util/Collection;)Z + public fun retainAll (Ljava/util/Collection;)Z + public final fun size ()I + public fun toArray ()[Ljava/lang/Object; + public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; +} + public final class dev/slne/surf/surfapi/core/api/command/SurfCommandUtil { public static final field INSTANCE Ldev/slne/surf/surfapi/core/api/command/SurfCommandUtil; public static final fun createException (Lnet/kyori/adventure/text/ComponentLike;)Ldev/slne/surf/surfapi/core/api/command/exception/WrapperCommandExceptionComponent; From 6e1ce54bd0f2422ede1aee447fa7a3ea7ffcfe05 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Fri, 20 Mar 2026 23:03:45 +0100 Subject: [PATCH 08/10] refactor: enhance visualizer resource management and cleanup methods --- .../visualizer/visualizer/SurfVisualizer.kt | 19 ++++++++++- .../visualizer/SurfVisualizerAreaImpl.kt | 8 +++-- .../SurfVisualizerSingleLocationImpl.kt | 5 ++- .../collection/TransformingSet2ObjectSet.kt | 32 +++++++++++++++++-- 4 files changed, 57 insertions(+), 7 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizer.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizer.kt index be098e8ea..24f6bf94e 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizer.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/visualizer/visualizer/SurfVisualizer.kt @@ -108,8 +108,25 @@ interface SurfVisualizer : AutoCloseable { */ fun update(strategy: UpdateStrategy = UpdateStrategy.ALL) + /** + * Closes the current visualizer instance, releasing any resources or connections + * associated with it. This method stops any ongoing visualization activities + * and ensures that all registered viewers are cleared. + * + * Once this method is called, the visualizer transitions into a closed state, + * making it unavailable for further operations unless explicitly restarted + * or reinitialized. + * + * Typical usage scenarios include shutting down the visualizer gracefully + * or when it is no longer needed. + */ + override fun close() - override fun close() // TODO: Provide kdocs + /** + * Checks whether the visualizer has been closed. + * + * @return `true` if the visualizer is in a closed state, `false` otherwise. + */ fun isClosed(): Boolean /** diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt index 27212c449..c46e037c5 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt @@ -94,9 +94,6 @@ class SurfVisualizerAreaImpl( private fun launchRecompute() { computationChannel.trySend(Unit) -// plugin.launch { -// recompute() -// } } private suspend fun recompute() { @@ -157,6 +154,11 @@ class SurfVisualizerAreaImpl( return delegate.stopVisualizing() } + override fun startVisualizing(): Boolean { + + return delegate.startVisualizing() + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is SurfVisualizerAreaImpl) return false diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerSingleLocationImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerSingleLocationImpl.kt index a36a76ae8..c4685af76 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerSingleLocationImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerSingleLocationImpl.kt @@ -12,6 +12,7 @@ import dev.slne.surf.surfapi.bukkit.api.util.isChunkVisible import dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer.SurfVisualizer import dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer.SurfVisualizerSingleLocation import dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer.UpdateStrategy +import dev.slne.surf.surfapi.bukkit.server.impl.visualizer.visualizerApiImpl import org.bukkit.Bukkit import org.bukkit.Chunk import org.bukkit.Location @@ -39,16 +40,18 @@ class SurfVisualizerSingleLocationImpl(location: Location) : AbstractSurfVisuali } init { - cleaner.register(this, SingleLocationCleanupState(entityId, internalViewerUuids)) + cleaner.register(this, SingleLocationCleanupState(uid, entityId, internalViewerUuids)) } private class SingleLocationCleanupState( + private val uid: UUID, private val entityId: Int, private val viewerUuids: MutableSet, ) : CleanupState() { override fun cleanup() { val despawn = nmsSpawnPackets.despawn(entityId) for (uuid in viewerUuids) { + visualizerApiImpl.onViewerRemoved(uid, uuid) val player = Bukkit.getPlayer(uuid) ?: continue despawn.execute(player) } diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/collection/TransformingSet2ObjectSet.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/collection/TransformingSet2ObjectSet.kt index dfa2a0a5b..2a94f5d22 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/collection/TransformingSet2ObjectSet.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/collection/TransformingSet2ObjectSet.kt @@ -10,9 +10,37 @@ class TransformingSet2ObjectSet( ) : ObjectSet { override fun iterator() = object : ObjectIterator { private val iterator = fromSet.iterator() + private var nextValue: M? = null + private var nextComputed: Boolean = false + + override fun hasNext(): Boolean { + if (nextComputed) { + return nextValue != null + } + while (iterator.hasNext()) { + val transformed = transformTo(iterator.next()) + if (transformed != null) { + nextValue = transformed + nextComputed = true + return true + } + } + nextValue = null + nextComputed = true + return false + } + + override fun next(): M { + if (!hasNext()) { + throw NoSuchElementException() + } + nextComputed = false + val result = nextValue + nextValue = null + @Suppress("UNCHECKED_CAST") + return result as M + } override fun remove() = iterator.remove() - override fun next(): M? = toTransformer(iterator.next()) - override fun hasNext() = iterator.hasNext() } override fun add(element: M): Boolean = transformFrom(element)?.let(fromSet::add) == true From 217d8f09d8240eaa829a1dd679ba03479f27f360 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sat, 21 Mar 2026 13:17:21 +0100 Subject: [PATCH 09/10] refactor: improve visualizer job management and enhance visualization checks --- .../visualizer/SurfVisualizerAreaImpl.kt | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt index c46e037c5..594663585 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt @@ -39,6 +39,8 @@ class SurfVisualizerAreaImpl( plugin.scope.coroutineContext + SupervisorJob(plugin.scope.coroutineContext[Job]) ) + private var workerJob: Job? = null + override var settings: BlockDisplaySettings = initialSettings ?: BlockDisplaySettings.create { blockData = SurfVisualizer.DEFAULT_BLOCK_TYPE.createBlockData() } @@ -52,12 +54,6 @@ class SurfVisualizerAreaImpl( if (corners.isNotEmpty()) { launchRecompute() } - - scope.launch { - computationChannel.consumeEach { - recompute() - } - } } override fun settings(consumer: BlockDisplaySettings.() -> Unit) { @@ -96,6 +92,15 @@ class SurfVisualizerAreaImpl( computationChannel.trySend(Unit) } + private fun startRecompute() { + workerJob?.cancel("Visualizer recompute cancelled.") + workerJob = scope.launch { + computationChannel.consumeEach { + recompute() + } + } + } + private suspend fun recompute() { if (delegate.isClosed()) return val cornersSnapshot = ObjectLinkedOpenHashSet(corners) @@ -150,12 +155,11 @@ class SurfVisualizerAreaImpl( } override fun stopVisualizing(): Boolean { - scope.coroutineContext[Job]?.children?.forEach { it.cancel() } + scope.coroutineContext[Job]?.cancelChildren() return delegate.stopVisualizing() } override fun startVisualizing(): Boolean { - return delegate.startVisualizing() } From 44e7a9fa5c121937c2396df14234b16657e2872d Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:30:48 +0100 Subject: [PATCH 10/10] refactor: enhance visualizer area handling and optimize chunk resolution logic --- .../visualizer/SurfBukkitVisualizerApiImpl.kt | 12 +- .../visualizer/SurfVisualizerAreaImpl.kt | 195 ++++++++++++++++-- .../SurfVisualizerMultipleLocationsImpl.kt | 2 + 3 files changed, 189 insertions(+), 20 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/SurfBukkitVisualizerApiImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/SurfBukkitVisualizerApiImpl.kt index ce59c206b..fb4c65b48 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/SurfBukkitVisualizerApiImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/SurfBukkitVisualizerApiImpl.kt @@ -27,7 +27,7 @@ class SurfBukkitVisualizerApiImpl : SurfBukkitVisualizerApi { .build() private val areaVisualizers = Caffeine.newBuilder() .weakValues() - .build() + .build() private val playerToVisualizers = ConcurrentHashMap>() @@ -66,6 +66,13 @@ class SurfBukkitVisualizerApiImpl : SurfBukkitVisualizerApi { } } + private fun getActiveAreaVisualizers(player: Player): List { + val visualizerUuids = playerToVisualizers[player.uniqueId] ?: return emptyList() + return visualizerUuids.mapNotNull { uid -> + areaVisualizers.getIfPresent(uid)?.takeIf { !it.isClosed() && it.isVisualizing() } + } + } + fun onViewerAdded(visualizerUid: UUID, playerUid: UUID) { playerToVisualizers.computeIfAbsent(playerUid) { ConcurrentHashMap.newKeySet() } .add(visualizerUid) @@ -76,6 +83,9 @@ class SurfBukkitVisualizerApiImpl : SurfBukkitVisualizerApi { } fun processChunkReceiveUpdateForPlayer(player: Player, chunk: Chunk) { + val activeAreas = getActiveAreaVisualizers(player) + activeAreas.forEach { it.onChunkBecameVisible(player, chunk) } + val active = getActiveVisualizers(player) active.forEach { it.onPlayerReceiveChunk(player, chunk) } } diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt index 594663585..5d7d4b5b6 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerAreaImpl.kt @@ -1,21 +1,31 @@ package dev.slne.surf.surfapi.bukkit.server.impl.visualizer.visualizer +import com.github.benmanes.caffeine.cache.Caffeine import com.github.shynixn.mccoroutine.folia.scope import dev.slne.surf.surfapi.bukkit.api.nms.bridges.packets.entity.BlockDisplaySettings -import dev.slne.surf.surfapi.bukkit.api.util.computeHighestYBlock +import dev.slne.surf.surfapi.bukkit.api.util.getHighestBlockYAtBlockCoordinates +import dev.slne.surf.surfapi.bukkit.api.util.getXFromChunkKey +import dev.slne.surf.surfapi.bukkit.api.util.getZFromChunkKey +import dev.slne.surf.surfapi.bukkit.api.util.isChunkVisible import dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer.SurfVisualizer import dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer.SurfVisualizerArea import dev.slne.surf.surfapi.bukkit.server.impl.visualizer.visualizerApiImpl import dev.slne.surf.surfapi.bukkit.server.plugin import dev.slne.surf.surfapi.core.api.algorithms.convexHull2D import dev.slne.surf.surfapi.core.api.math.VoxelLineTracer +import dev.slne.surf.surfapi.core.api.util.mutableLongSetOf import dev.slne.surf.surfapi.core.api.util.toObjectSet import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet -import it.unimi.dsi.fastutil.objects.ObjectSet import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.consumeEach +import kotlinx.coroutines.future.await +import org.bukkit.Bukkit +import org.bukkit.Chunk +import org.bukkit.ChunkSnapshot import org.bukkit.World +import org.bukkit.entity.Player +import org.bukkit.util.NumberConversions import org.spongepowered.math.vector.Vector3d import java.util.concurrent.ConcurrentHashMap import kotlin.time.Duration @@ -49,6 +59,28 @@ class SurfVisualizerAreaImpl( launchRecompute() } + /** + * Pending edge points grouped by chunk key, awaiting Y resolution. + * These are the raw 2D points (y=0) from the hull computation. + * Once a chunk becomes visible to any viewer, the points are resolved, + * added to the delegate, and removed from this map. + */ + private val pendingByChunk = ConcurrentHashMap>() + + /** + * Cache of chunk snapshots for Y resolution. Avoids re-loading chunks + * that were already resolved. + */ + private val snapshotCache = Caffeine.newBuilder() + .maximumSize(512) + .build() + + /** + * The settings snapshot captured at recompute time, used for lazily resolved points. + */ + @Volatile + private var pendingSettings: BlockDisplaySettings? = null + init { corners.addAll(initialEdges) if (corners.isNotEmpty()) { @@ -106,12 +138,18 @@ class SurfVisualizerAreaImpl( val cornersSnapshot = ObjectLinkedOpenHashSet(corners) val settingsSnapshot = settings.clone() + // Clear all previous state delegate.clearVisualLocations() + pendingByChunk.clear() + snapshotCache.invalidateAll() + pendingSettings = null + if (cornersSnapshot.size < 2) return if (!delegate.checkNotNullWorld()) return currentCoroutineContext().ensureActive() + // Compute 2D hull and edge points (cheap, no chunk loading) val hull = cornersSnapshot.convexHull2D() val edgePoints = ObjectLinkedOpenHashSet() @@ -124,34 +162,148 @@ class SurfVisualizerAreaImpl( currentCoroutineContext().ensureActive() - val finalEdgePoints: ObjectSet = if (useHighestYBlock) { - edgePoints.map { it.toInt() } - .computeHighestYBlock(delegate.world) - .map { it.add(0, 1, 0).toDouble() } - .toCollection(ObjectLinkedOpenHashSet()) - } else { - edgePoints + + if (!useHighestYBlock) { + // No height resolution needed — add all points directly + if (placeDelay.isPositive()) { + for ((i, point) in edgePoints.withIndex()) { + currentCoroutineContext().ensureActive() + delegate.addVisualLocation(point, settingsSnapshot) + if (i < edgePoints.size - 1) { + delay(placeDelay) + } + } + } else { + delegate.addVisualLocations(edgePoints, settingsSnapshot) + } + return + } + pendingSettings = settingsSnapshot + + // Group edge points by chunk + for (point in edgePoints) { + val chunkKey = Chunk.getChunkKey( + NumberConversions.floor(point.x()) shr 4, + NumberConversions.floor(point.z()) shr 4 + ) + pendingByChunk.computeIfAbsent(chunkKey) { + ConcurrentHashMap.newKeySet() + }.add(point) } currentCoroutineContext().ensureActive() + // Immediately resolve chunks that any viewer can already see + if (delegate.isVisualizing() && delegate.hasViewers()) { + resolveVisibleChunks(settingsSnapshot) + } + } + + /** + * Resolves pending chunks that are currently visible to at least one viewer. + * Called after recompute and after startVisualizing. + */ + private suspend fun resolveVisibleChunks(settingsSnapshot: BlockDisplaySettings) { + val world = delegate.world + + // Collect chunk keys that any viewer can see + val visibleKeys = mutableLongSetOf() + for (viewerUuid in delegate.viewerUuids) { + val player = Bukkit.getPlayer(viewerUuid) ?: continue + for (chunkKey in pendingByChunk.keys) { + val cx = getXFromChunkKey(chunkKey) + val cz = getZFromChunkKey(chunkKey) + if (player.isChunkVisible(world, cx, cz)) { + visibleKeys.add(chunkKey) + } + } + } + + val iterator = visibleKeys.iterator() + while (iterator.hasNext()) { + val chunkKey = iterator.nextLong() + currentCoroutineContext().ensureActive() + resolveChunk(chunkKey, settingsSnapshot) + } + } + + private suspend fun resolveChunk(chunkKey: Long, settingsSnapshot: BlockDisplaySettings) { + val points = pendingByChunk.remove(chunkKey) ?: return + val world = delegate.getWorldIfPresent() ?: return + + val snapshot = getOrLoadSnapshot(chunkKey, world) ?: return + + val resolvedPoints = points.map { point -> + val y = snapshot.getHighestBlockYAtBlockCoordinates( + NumberConversions.floor(point.x()), + NumberConversions.floor(point.z()) + ) + Vector3d(point.x(), (y + 1).toDouble(), point.z()) to settingsSnapshot + } + if (placeDelay.isPositive()) { - for ((i, point) in finalEdgePoints.withIndex()) { + for ((i, pair) in resolvedPoints.withIndex()) { currentCoroutineContext().ensureActive() - delegate.addVisualLocation(point, settingsSnapshot) - if (i < finalEdgePoints.size - 1) { + delegate.addVisualLocation(pair.first, pair.second) + if (i < resolvedPoints.size - 1) { delay(placeDelay) } } } else { - delegate.addVisualLocations(finalEdgePoints, settingsSnapshot) + delegate.addVisualLocations(resolvedPoints) } } - override fun close() { - scope.cancel("Visualizer closed.") - computationChannel.close() - delegate.close() + private suspend fun getOrLoadSnapshot(chunkKey: Long, world: World): ChunkSnapshot? { + val cached = snapshotCache.getIfPresent(chunkKey) + if (cached != null) return cached + + val cx = getXFromChunkKey(chunkKey) + val cz = getZFromChunkKey(chunkKey) + + val chunk = world.getChunkAtAsync(cx, cz).await() + val snapshot = chunk.getChunkSnapshot(true, false, false, false) + snapshotCache.put(chunkKey, snapshot) + return snapshot + } + + fun onChunkBecameVisible(player: Player, chunk: Chunk) { + if (!useHighestYBlock) return + if (!delegate.isVisualizing()) return + if (delegate.isClosed()) return + + val chunkKey = chunk.chunkKey + val points = pendingByChunk.remove(chunkKey) ?: return + + // We're on the tick thread and the chunk is loaded — snapshot is cheap + val snapshot = chunk.getChunkSnapshot(true, false, false, false) + snapshotCache.put(chunkKey, snapshot) + + val settingsSnapshot = pendingSettings?.clone() ?: return + + val resolvedPoints = points.map { point -> + val y = snapshot.getHighestBlockYAtBlockCoordinates( + NumberConversions.floor(point.x()), + NumberConversions.floor(point.z()) + ) + Vector3d(point.x(), (y + 1).toDouble(), point.z()) to settingsSnapshot + } + + // Add to delegate — this will also spawn for the player since + // delegate.isVisualizing() is true and the player is a viewer + delegate.addVisualLocations(resolvedPoints) + } + + override fun startVisualizing(): Boolean { + startRecompute() + val result = delegate.startVisualizing() + if (result && useHighestYBlock && pendingByChunk.isNotEmpty()) { + val settingsSnapshot = pendingSettings ?: return true + scope.launch { + resolveVisibleChunks(settingsSnapshot) + } + } + return result } override fun stopVisualizing(): Boolean { @@ -159,8 +311,13 @@ class SurfVisualizerAreaImpl( return delegate.stopVisualizing() } - override fun startVisualizing(): Boolean { - return delegate.startVisualizing() + override fun close() { + scope.cancel("Visualizer closed.") + computationChannel.close() + pendingByChunk.clear() + snapshotCache.invalidateAll() + pendingSettings = null + delegate.close() } override fun equals(other: Any?): Boolean { diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt index 396df9073..0571f67db 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/visualizer/visualizer/SurfVisualizerMultipleLocationsImpl.kt @@ -35,6 +35,8 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer val world: World get() = worldReference.get() ?: error("World reference is no longer valid") + fun getWorldIfPresent(): World? = worldReference.get() + private val id2point = mutableInt2ObjectMapOf() private val point2Id = mutableObject2IntMapOf().apply { defaultReturnValue(Int.MIN_VALUE) }