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/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-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/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 112a5c0af..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 @@ -7,16 +7,15 @@ 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.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. @@ -230,38 +230,42 @@ 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 = mutableLong2ObjectMapOf(byChunk.size) + val snapshots = ConcurrentHashMap(chunkKeys.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 = chunkKeys.iterator() + while (iterator.hasNext()) { + val key = iterator.nextLong() + launch { + semaphore.withPermit { + val chunk = world.getChunkAtAsync(getXFromChunkKey(key), getZFromChunkKey(key)) .await() - .getChunkSnapshot(true, false, false, false) - snapshots.put(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 + } + } } - }.awaitAll() + } } - 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-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..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 @@ -4,6 +4,7 @@ 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.* @@ -16,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. @@ -52,6 +53,9 @@ interface SurfVisualizer { * Modifications to the set directly are not allowed, ensuring consistency * with the visualizer's state. */ + val viewerUuids: @UnmodifiableView Set + + @Deprecated("Use viewerUuids instead", ReplaceWith("viewerUuids")) val viewers: @UnmodifiableView ObjectSet /** @@ -104,6 +108,27 @@ interface SurfVisualizer { */ 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() + + /** + * Checks whether the visualizer has been closed. + * + * @return `true` if the visualizer is in a closed state, `false` otherwise. + */ + fun isClosed(): Boolean + /** * Companion object for the `SurfVisualizer` interface. * Provides shared constants and utilities related to visualizer functionality. 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/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/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/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..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 @@ -11,23 +11,25 @@ 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() - .build() + .weakValues() + .build() + + private val playerToVisualizers = ConcurrentHashMap>() override fun createSingleLocationVisualizer(location: Location): SurfVisualizerSingleLocation { return SurfVisualizerSingleLocationImpl(location).also { visualizers.put(it.uid, it) } @@ -57,42 +59,54 @@ class SurfBukkitVisualizerApiImpl : SurfBukkitVisualizerApi { return areaVisualizers.getIfPresent(uid) ?: visualizers.getIfPresent(uid) } - private fun getActiveVisualizers(player: Player) = - visualizers.asMap().values.filter { 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() } + } + } + 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() } + } + } - private val log = logger() - fun processChunkReceiveUpdateForPlayer(player: Player, chunk: Chunk) { - val active = getActiveVisualizers(player) + fun onViewerAdded(visualizerUid: UUID, playerUid: UUID) { + playerToVisualizers.computeIfAbsent(playerUid) { ConcurrentHashMap.newKeySet() } + .add(visualizerUid) + } - if (active.isNotEmpty()) { - log.atInfo() - .log("Received update for player ${player.name} for ${active.size} visualizers") - } + fun onViewerRemoved(visualizerUid: UUID, playerUid: UUID) { + playerToVisualizers[playerUid]?.remove(visualizerUid) + } + + 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) } } 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 + playerToVisualizers.remove(player.uniqueId) - 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) { + visualizers.invalidate(visualizer.uid) + areaVisualizers.invalidate(visualizer.uid) } } 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..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 @@ -3,48 +3,66 @@ 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.TransformingObjectSet -import dev.slne.surf.surfapi.core.api.util.freeze +import dev.slne.surf.surfapi.core.api.collection.TransformingSet2ObjectSet 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 it.unimi.dsi.fastutil.objects.ObjectSet 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 +import java.util.concurrent.atomic.AtomicLong abstract class AbstractSurfVisualizerImpl : SurfVisualizer { protected val log = logger() companion object { - private val cleaner = Cleaner.create() + val cleaner: 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 internalViewerUuids: MutableSet = ConcurrentHashMap.newKeySet() + override val viewerUuids: MutableSet = Collections.unmodifiableSet(internalViewerUuids) + + @Deprecated("Use viewerUuids instead", replaceWith = ReplaceWith("viewerUuids")) + override val viewers: ObjectSet = + TransformingSet2ObjectSet(viewerUuids, Bukkit::getPlayer, Player::getUniqueId) + override val uid: UUID = UUID.randomUUID() - @Volatile - protected var visualizing = false + protected val visualizing = AtomicBoolean(false) + protected val closed = AtomicBoolean(false) + private val stateVersion = AtomicLong(0) - init { - cleaner.register(this) { - stopVisualizing() + 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 + ensureNotClosed() + if (!visualizing.compareAndSet(false, true)) return false + nextStateVersion() try { - visualizing = true startVisualizingInternal() return true } catch (e: Throwable) { + visualizing.set(false) log.atSevere() .withCause(e) .log("Failed to start visualizing") @@ -53,11 +71,20 @@ abstract class AbstractSurfVisualizerImpl : SurfVisualizer { } override fun stopVisualizing(): Boolean { - if (!visualizing) return false + ensureNotClosed() + return stopVisualizing(false) + } + + fun stopVisualizing(force: Boolean): Boolean { + if (!visualizing.compareAndSet(true, false) && !force) return false + if (!force) { + ensureNotClosed() + } + + nextStateVersion() try { stopVisualizingInternal() - visualizing = false return true } catch (e: Throwable) { log.atSevere() @@ -67,61 +94,111 @@ 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)) { + ensureNotClosed() + if (player.isOnline && internalViewerUuids.add(player.uniqueId)) { + visualizerApiImpl.onViewerAdded(uid, player.uniqueId) onViewerAdded(player) } } override fun removeViewer(player: Player) { - if (viewerUuids.remove(player.uniqueId)) { + ensureNotClosed() + if (internalViewerUuids.remove(player.uniqueId)) { + visualizerApiImpl.onViewerRemoved(uid, player.uniqueId) if (player.isOnline) { onViewerRemoved(player) + } else { + clearStaleData(player.uniqueId) } } } override fun clearViewers() { - for (uuid in viewerUuids) { - Bukkit.getPlayer(uuid) - ?.takeIf { it.isOnline } - ?.let { onViewerRemoved(it) } - } + ensureNotClosed() + val iterator = internalViewerUuids.iterator() + while (iterator.hasNext()) { + val next = iterator.next() + val player = Bukkit.getPlayer(next) + + visualizerApiImpl.onViewerRemoved(uid, next) + if (player != null) { + onViewerRemoved(player) + } else { + clearStaleData(next) + } - viewerUuids.clear() + iterator.remove() + } } - 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) { - if (!visualizing) return + ensureNotClosed() + if (!visualizing.get()) return - plugin.launch(plugin.entityDispatcher(player)) { + val version = currentStateVersion() + player.enterContextIfNeeded { + if (!isActiveVersion(version)) return@enterContextIfNeeded player.sentChunks.forEach { chunk -> onPlayerReceiveChunk(player, chunk) } } } - open fun onViewerRemoved(player: Player) { - if (!visualizing) return - plugin.launch(plugin.entityDispatcher(player)) { - player.sentChunks.forEach { chunk -> - onPlayerUnloadChunk(player, chunk) - } + abstract fun onViewerRemoved(player: Player) + abstract fun clearStaleData(uuid: UUID) + + @Synchronized + final override fun close() { + if (!closed.compareAndSet(false, true)) return + + visualizerApiImpl.onVisualizerClose(this) + + stopVisualizing(true) + onClose() + + for (viewUuid in internalViewerUuids) { + visualizerApiImpl.onViewerRemoved(uid, viewUuid) } + internalViewerUuids.clear() + } + + override fun isClosed(): Boolean { + return closed.get() } + protected abstract fun onClose() protected abstract fun startVisualizingInternal() protected abstract fun stopVisualizingInternal() 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() + } + } + } + + protected fun ensureNotClosed() { + if (closed.get()) { + throw IllegalStateException("Visualizer is already closed!") + } + } + + 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 ed335d79c..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,22 +1,33 @@ package dev.slne.surf.surfapi.bukkit.server.impl.visualizer.visualizer -import com.github.shynixn.mccoroutine.folia.launch +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.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 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 class SurfVisualizerAreaImpl( @@ -30,9 +41,15 @@ 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]) + ) + + private var workerJob: Job? = null override var settings: BlockDisplaySettings = initialSettings ?: BlockDisplaySettings.create { blockData = SurfVisualizer.DEFAULT_BLOCK_TYPE.createBlockData() @@ -42,8 +59,31 @@ 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 { - if (initialEdges.isNotEmpty()) { + corners.addAll(initialEdges) + if (corners.isNotEmpty()) { launchRecompute() } } @@ -81,49 +121,203 @@ class SurfVisualizerAreaImpl( } private fun launchRecompute() { - plugin.launch { - recompute() + computationChannel.trySend(Unit) + } + + private fun startRecompute() { + workerJob?.cancel("Visualizer recompute cancelled.") + workerJob = scope.launch { + computationChannel.consumeEach { + recompute() + } } } - private suspend fun recompute() = recomputationMutex.withLock { + private suspend fun recompute() { + if (delegate.isClosed()) return + val cornersSnapshot = ObjectLinkedOpenHashSet(corners) + val settingsSnapshot = settings.clone() + + // Clear all previous state delegate.clearVisualLocations() - if (corners.size < 2) return + pendingByChunk.clear() + snapshotCache.invalidateAll() + pendingSettings = null + + if (cornersSnapshot.size < 2) return if (!delegate.checkNotNullWorld()) return - val hull = corners.convexHull2D() - val cornerBlocks = hull + currentCoroutineContext().ensureActive() + // Compute 2D hull and edge points (cheap, no chunk loading) + 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] ) } - val finalEdgePoints: ObjectSet = if (useHighestYBlock) { - edgePoints.map { it.toInt() } - .computeHighestYBlock(delegate.world) - .map { it.add(0, 1, 0).toDouble() } - .toCollection(ObjectLinkedOpenHashSet()) - } else { - edgePoints + + currentCoroutineContext().ensureActive() + + + 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()) { - delegate.addVisualLocation(point, settings) - if (i < finalEdgePoints.size - 1) { + for ((i, pair) in resolvedPoints.withIndex()) { + currentCoroutineContext().ensureActive() + delegate.addVisualLocation(pair.first, pair.second) + if (i < resolvedPoints.size - 1) { delay(placeDelay) } } } else { - delegate.addVisualLocations(finalEdgePoints, settings) - finalEdgePoints.forEach { - delegate.addVisualLocation(it, settings) + delegate.addVisualLocations(resolvedPoints) + } + } + + 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 { + scope.coroutineContext[Job]?.cancelChildren() + return delegate.stopVisualizing() + } + 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 8a4c0d591..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 @@ -2,25 +2,32 @@ 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 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 -import dev.slne.surf.surfapi.bukkit.server.plugin +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 +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 { @@ -28,75 +35,177 @@ 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) } + 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) + + init { + 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, + private val lock: ReentrantReadWriteLock, + ) : CleanupState() { + override fun cleanup() { + if (id2point.isEmpty()) return + + val allIds = lock.read { IntOpenHashSet(id2point.keys) } + val despawn = nmsSpawnPackets.despawn(allIds) + + for (uuid in viewerUuids) { + visualizerApiImpl.onViewerRemoved(visualizerUuid, uuid) + val player = Bukkit.getPlayer(uuid) ?: continue + despawn.execute(player) + } + + lock.write { + id2point.clear() + viewerUuids.clear() + sentToPlayers.clear() + } + } + } + + override fun onClose() { + writeLocked { + id2point.clear() + point2Id.clear() + sentToPlayers.clear() + } + } override fun startVisualizingInternal() { update() } override fun stopVisualizingInternal() { - for (viewer in viewers) { - nmsSpawnPackets.despawn(id2point.keys).execute(viewer) - getSentToPlayer(viewer).clear() + val sentByViewer = writeLocked { + viewerUuids.associateWith { viewerUuid -> + IntOpenHashSet(sentToPlayers.remove(viewerUuid) ?: IntOpenHashSet()) + } + } + + for ((viewerUuid, ids) in sentByViewer) { + if (ids.isEmpty()) continue + + val player = Bukkit.getPlayer(viewerUuid) ?: continue + player.enterContextIfNeeded { + nmsSpawnPackets.despawn(ids).execute(player) + } } } override fun update(strategy: UpdateStrategy) { - if (!visualizing) return + ensureNotClosed() + if (!visualizing.get()) return if (!checkNotNullWorld()) return when (strategy) { UpdateStrategy.ALL -> { - for (viewer in viewers) { - plugin.launch(plugin.entityDispatcher(viewer)) { - val sent = getSentToPlayer(viewer) - nmsSpawnPackets.despawn(sent).execute(viewer) - sent.clear() + val pointsSnapshot = readLocked { + Int2ObjectOpenHashMap(id2point) + } + val version = currentStateVersion() + for (viewer in viewerUuids) { + val player = Bukkit.getPlayer(viewer) + if (player == null) { + clearStaleData(viewer) + continue + } + + player.enterContextIfNeeded { + if (!isActiveVersion(version)) return@enterContextIfNeeded + + val previouslySent = drainSentToPlayer(viewer) + if (!previouslySent.isNullOrEmpty()) { + nmsSpawnPackets.despawn(previouslySent).execute(player) + } + + if (!visualizing.get()) return@enterContextIfNeeded + + 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) - ) { + + 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) } } - spawn.execute(viewer) + + writeLocked { + if (!visualizing.get()) return@enterContextIfNeeded + getOrCreateSentToPlayer(viewer).addAll(idsToMarkSent) + } + + spawn.execute(player) } } } UpdateStrategy.POSITION -> { - for (viewer in viewers) { - plugin.launch(plugin.entityDispatcher(viewer)) { + val version = currentStateVersion() + for (viewer in viewerUuids) { + val player = Bukkit.getPlayer(viewer) + if (player == null) { + 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 sent = getSentToPlayer(viewer) + val idsToRemove = mutableIntSetOf() - val sentIterator = sent.intIterator() - while (sentIterator.hasNext()) { - val id = sentIterator.nextInt() - val point = id2point[id] ?: continue + val iterator = sentSnapshot.iterator() + while (iterator.hasNext()) { + val id = iterator.nextInt() + val point = pointsSnapshot[id] - if (viewer.isChunkVisible(world, point.chunkX, point.chunkZ)) { + if (point == null) { + idsToRemove.add(id) + continue + } + + if (player.isChunkVisible(world, point.chunkX, point.chunkZ)) { + if (!visualizing.get()) continue operation + updatePositionPacket(id, point) } else { - operation + nmsSpawnPackets.despawn(id) - sentIterator.remove() + idsToRemove.add(id) + } + } + + if (idsToRemove.isNotEmpty()) { + writeLocked { + getSentToPlayer(viewer)?.removeAll(idsToRemove) } + nmsSpawnPackets.despawn(idsToRemove).execute(player) } - operation.execute(viewer) + + operation.execute(player) } } } @@ -107,90 +216,132 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer visualLocation: Vector3d, settings: BlockDisplaySettings, ) { + ensureNotClosed() val id = nmsCommonBridge.nextEntityId() 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) + for (viewer in viewerUuids) { + val player = Bukkit.getPlayer(viewer) + if (player == null) { + clearStaleData(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) return } - if (!visualizing) { + if (!visualizing.get()) { for ((loc, setting) in locations) { addVisualLocation(loc, setting) } - } else { - val toSpawn = mutableInt2ObjectMapOf() + return + } - for ((loc, setting) in locations) { - val id = nmsCommonBridge.nextEntityId() - val point = VisualPoint(loc, setting) - put(id, point) - toSpawn[id] = point + val toSpawn = mutableInt2ObjectMapOf() + for ((loc, setting) in locations) { + val id = nmsCommonBridge.nextEntityId() + val point = VisualPoint(loc, setting) + put(id, point) + toSpawn[id] = point + } + + val version = currentStateVersion() + for (viewer in viewerUuids) { + val player = Bukkit.getPlayer(viewer) + if (player == null) { + clearStaleData(viewer) + continue } - for (player in viewers) { - plugin.launch(plugin.entityDispatcher(player)) { - val sent = getSentToPlayer(player) - val spawnOperation = PacketOperation.start() + player.enterContextIfNeeded { + if (!isActiveVersion(version)) return@enterContextIfNeeded + if (!visualizing.get()) return@enterContextIfNeeded + val idsToAdd = mutableIntSetOf() + val spawnOperation = PacketOperation.start() - toSpawn.int2ObjectEntrySet().fastForEach { entry -> - val id = entry.intKey - val point = entry.value + toSpawn.int2ObjectEntrySet().fastForEach { entry -> + val id = entry.intKey + val point = entry.value - if (player.isChunkVisible(world, point.chunkX, point.chunkZ) && sent.add(id)) { - spawnOperation + spawnPacket(id, point) - } + if (player.isChunkVisible(world, point.chunkX, point.chunkZ)) { + idsToAdd.add(id) + spawnOperation + spawnPacket(id, point) } + } - spawnOperation.execute(player) + writeLocked { + if (!visualizing.get()) return@enterContextIfNeeded + getOrCreateSentToPlayer(viewer).addAll(idsToAdd) } + + spawnOperation.execute(player) } } } override fun removeVisualLocation(visualLocation: Vector3d) { + ensureNotClosed() 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) + for (viewer in viewerUuids) { + val player = Bukkit.getPlayer(viewer) + if (player == null) { + clearStaleData(viewer) + continue + } + + despawn(player, id, point, true) } } override fun clearVisualLocations() { - if (visualizing && checkNotNullWorld()) { - val idsToRemove = id2point.keys - for (viewer in viewers) { - nmsSpawnPackets.despawn(idsToRemove).execute(viewer) + ensureNotClosed() + val idsToRemove = readLocked { + IntOpenHashSet(id2point.keys) + } + + val version = currentStateVersion() + if (checkNotNullWorld() && idsToRemove.isNotEmpty()) { + for (viewer in viewerUuids) { + val player = Bukkit.getPlayer(viewer) + if (player == null) { + clearStaleData(viewer) + continue + } + + player.enterContextIfNeeded { + if (!isActiveVersion(version)) return@enterContextIfNeeded + 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 +353,50 @@ 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)) { - if (player.isChunkVisible(world, point.chunkX, point.chunkZ)) { + val version = currentStateVersion() + player.enterContextIfNeeded { + if (!isActiveVersion(version)) return@enterContextIfNeeded + if (visualizing.get() && player.isChunkVisible(world, point.chunkX, point.chunkZ)) { + writeLocked { + getOrCreateSentToPlayer(player.uniqueId).add(id) + } spawnPacket(id, point).execute(player) - getSentToPlayer(player).add(id) } } } private fun despawn(player: Player, id: Int, point: VisualPoint, force: Boolean = false) { - plugin.launch(plugin.entityDispatcher(player)) { + val version = currentStateVersion() + player.enterContextIfNeeded { + if (!isActiveVersion(version)) return@enterContextIfNeeded if (force || !player.isChunkVisible(world, point.chunkX, point.chunkZ)) { + writeLocked { + getOrCreateSentToPlayer(player.uniqueId).remove(id) + } nmsSpawnPackets.despawn(id).execute(player) - getSentToPlayer(player).remove(id) } } } @@ -238,41 +408,85 @@ class SurfVisualizerMultipleLocationsImpl(world: World) : AbstractSurfVisualizer nmsSpawnPackets.teleport(id, point.pos) 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) + } + val spawnOperation = PacketOperation.start() - val sent = getSentToPlayer(player) + val idsToAdd = mutableIntSetOf() + val sent = getSentToPlayerSnapshot(player.uniqueId) - point2Id.object2IntEntrySet().fastForEach { entry -> - val point = entry.key - val id = entry.intValue + 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) - 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) + 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 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 = pointsSnapshot[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) } + override fun onViewerRemoved(player: Player) { + ensureNotClosed() + + val sent = drainSentToPlayer(player.uniqueId) ?: return + if (sent.isEmpty()) return + + player.enterContextIfNeeded { + nmsSpawnPackets.despawn(sent).execute(player) + } + } + + override fun clearStaleData(uuid: UUID) { + drainSentToPlayer(uuid) + } 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..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,9 +12,12 @@ 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 import org.bukkit.entity.Player +import java.util.* class SurfVisualizerSingleLocationImpl(location: Location) : AbstractSurfVisualizerImpl(), SurfVisualizerSingleLocation { @@ -22,6 +25,7 @@ class SurfVisualizerSingleLocationImpl(location: Location) : AbstractSurfVisuali override var location: Location = location set(value) { + ensureNotClosed() field = value update(UpdateStrategy.POSITION) } @@ -30,55 +34,102 @@ class SurfVisualizerSingleLocationImpl(location: Location) : AbstractSurfVisuali blockData = SurfVisualizer.DEFAULT_BLOCK_TYPE.createBlockData() } set(value) { + ensureNotClosed() field = value update() } + init { + 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) + } + viewerUuids.clear() + } + } + + override fun onClose() { + } + override fun startVisualizingInternal() { update() } override fun stopVisualizingInternal() { - viewers.forEach { despawnPacket().execute(it) } + 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 clearStaleData(uuid: UUID) { + + } + override fun update(strategy: UpdateStrategy) { - if (!visualizing) return + ensureNotClosed() + if (!visualizing.get()) return when (strategy) { UpdateStrategy.ALL -> { val despawn = despawnPacket() val spawn = spawnPacket() - for (viewer in viewers) { - despawn.execute(viewer) - val seesLocation = viewer.isChunkVisible(location) + 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) { - spawn.execute(viewer) + if (seesLocation) { + spawn.execute(player) + } } } } UpdateStrategy.POSITION -> { val updatePosition = updatePositionPacket() - for (viewer in viewers) { - if (viewer.isChunkVisible(location)) { - updatePosition.execute(viewer) - } else { - despawnPacket().execute(viewer) + 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) + } } } } @@ -86,6 +137,7 @@ class SurfVisualizerSingleLocationImpl(location: Location) : AbstractSurfVisuali } override fun settings(consumer: BlockDisplaySettings.() -> Unit) { + ensureNotClosed() settings.consumer() update() } 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 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; 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..2a94f5d22 --- /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,66 @@ +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() + 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 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