GhostDisplays/main/kotlin/net/hareworks/ghostdisplays/internal/controller/BaseDisplayController.kt
2025-12-06 04:40:18 +09:00

200 lines
6.6 KiB
Kotlin

package net.hareworks.ghostdisplays.internal.controller
import java.util.HashSet
import java.util.UUID
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicBoolean
import net.hareworks.ghostdisplays.api.DisplayController
import net.hareworks.ghostdisplays.api.audience.AudiencePredicate
import net.hareworks.ghostdisplays.api.click.ClickPriority
import net.hareworks.ghostdisplays.api.click.ClickSurface
import net.hareworks.ghostdisplays.api.click.DisplayClickContext
import net.hareworks.ghostdisplays.api.click.DisplayClickHandler
import net.hareworks.ghostdisplays.api.click.HandlerRegistration
import org.bukkit.Bukkit
import org.bukkit.entity.Display
import org.bukkit.entity.Entity
import org.bukkit.entity.Interaction
import org.bukkit.entity.Player
import org.bukkit.event.player.PlayerInteractEntityEvent
import org.bukkit.plugin.java.JavaPlugin
internal class BaseDisplayController<T : Display>(
private val plugin: JavaPlugin,
override val display: T,
override val interaction: Interaction?,
private val registry: DisplayRegistry
) : DisplayController<T> {
private val destroyed = AtomicBoolean(false)
private val viewerCounts = ConcurrentHashMap<UUID, Int>()
private val handlers = CopyOnWriteArrayList<HandlerEntry>()
private val audiences = CopyOnWriteArrayList<AudienceBindingImpl>()
override fun show(player: Player) {
runSync {
val uuid = player.uniqueId
val newCount = viewerCounts.compute(uuid) { _, count -> (count ?: 0) + 1 } ?: 1
if (newCount == 1) {
player.showEntity(plugin, display)
interaction?.let { player.showEntity(plugin, it) }
}
}
}
override fun hide(player: Player) {
runSync {
val uuid = player.uniqueId
val current = viewerCounts[uuid] ?: return@runSync
if (current <= 1) {
viewerCounts.remove(uuid)
player.hideEntity(plugin, display)
interaction?.let { player.hideEntity(plugin, it) }
} else {
viewerCounts[uuid] = current - 1
}
}
}
override fun isViewing(playerId: UUID): Boolean = viewerCounts.containsKey(playerId)
override fun viewerIds(): Set<UUID> = HashSet(viewerCounts.keys)
override fun applyEntityUpdate(mutator: (T) -> Unit) {
callSync {
mutator(display)
display.updateDisplay(false)
}
}
override fun destroy() {
if (!destroyed.compareAndSet(false, true)) return
registry.unregister(this)
val viewers = viewerIds().mapNotNull { Bukkit.getPlayer(it) }
runSync {
viewers.forEach {
it.hideEntity(plugin, display)
interaction?.let { interactionEntity -> it.hideEntity(plugin, interactionEntity) }
}
display.remove()
interaction?.remove()
viewerCounts.clear()
}
handlers.clear()
audiences.forEach { it.clear() }
audiences.clear()
}
override fun onClick(priority: ClickPriority, handler: DisplayClickHandler): HandlerRegistration {
val entry = HandlerEntry(priority, handler)
handlers += entry
return HandlerRegistration {
handlers.remove(entry)
}
}
override fun addAudience(predicate: AudiencePredicate): HandlerRegistration {
val binding = AudienceBindingImpl(predicate)
audiences += binding
refreshAudienceInternal(binding = binding)
return HandlerRegistration { binding.unregister() }
}
private fun refreshAudienceInternal(target: Player? = null, binding: AudienceBindingImpl? = null) {
runSync {
val players = target?.let { listOf(it) } ?: Bukkit.getOnlinePlayers()
val targets = binding?.let { listOf(it) } ?: audiences
if (players.isEmpty() || targets.isEmpty()) return@runSync
players.forEach { player ->
targets.forEach { it.evaluate(player) }
}
}
}
override fun refreshAudience(target: Player?) {
refreshAudienceInternal(target, null)
}
internal fun handleClick(event: PlayerInteractEntityEvent, clicked: Entity, surface: ClickSurface) {
val sortedHandlers = handlers.sortedBy { it.priority.ordinal }
if (sortedHandlers.isEmpty()) return
val context = DisplayClickContext(
player = event.player,
hand = event.hand,
clickedEntity = clicked,
surface = surface,
controller = this,
event = event
)
sortedHandlers.forEach { entry ->
entry.handler.handle(context)
if (event.isCancelled) return
}
}
internal fun handlePlayerQuit(player: Player) {
val uuid = player.uniqueId
viewerCounts.remove(uuid)
audiences.forEach { it.forget(uuid) }
}
private fun runSync(action: () -> Unit) {
if (Bukkit.isPrimaryThread()) {
action()
} else {
Bukkit.getScheduler().runTask(plugin, action)
}
}
private fun <R> callSync(action: () -> R): R {
return if (Bukkit.isPrimaryThread()) {
action()
} else {
val future = CompletableFuture<R>()
Bukkit.getScheduler().runTask(plugin) { future.complete(action()) }
future.join()
}
}
private inner class AudienceBindingImpl(
private val predicate: AudiencePredicate
) {
private val activeViewers = ConcurrentHashMap.newKeySet<UUID>()
fun evaluate(player: Player) {
val uuid = player.uniqueId
val matches = runCatching { predicate.test(player) }.getOrDefault(false)
if (matches) {
if (activeViewers.add(uuid)) {
show(player)
}
} else {
if (activeViewers.remove(uuid)) {
hide(player)
}
}
}
fun forget(playerId: UUID) {
activeViewers.remove(playerId)
}
fun unregister() {
audiences.remove(this)
val targets = activeViewers.toList()
targets.mapNotNull { Bukkit.getPlayer(it) }.forEach { hide(it) }
activeViewers.clear()
}
fun clear() {
activeViewers.clear()
}
}
private data class HandlerEntry(
val priority: ClickPriority,
val handler: DisplayClickHandler
)
}