feat: Audienceのタイミング

This commit is contained in:
Keisuke Hirata 2025-12-07 03:41:42 +09:00
parent 4a302f5f6f
commit bc774765c5
17 changed files with 318 additions and 72 deletions

4
.gitignore vendored
View File

@ -1,5 +1,3 @@
# Ignore Gradle project-specific cache directory
.gradle .gradle
bin
# Ignore Gradle build output directory
build build

@ -1 +1 @@
Subproject commit 559341c0415699a2b39a1a523f4ef4ed93ce3fdc Subproject commit 9af293122b860d8c65d68a2bdc02a9c2c5e206cc

View File

@ -1,5 +1,6 @@
package net.hareworks.ghostdisplays.api package net.hareworks.ghostdisplays.api
import net.hareworks.ghostdisplays.api.audience.AudienceAction
import net.hareworks.ghostdisplays.api.audience.AudiencePredicate import net.hareworks.ghostdisplays.api.audience.AudiencePredicate
import net.hareworks.ghostdisplays.api.click.ClickPriority import net.hareworks.ghostdisplays.api.click.ClickPriority
import net.hareworks.ghostdisplays.api.click.DisplayClickHandler import net.hareworks.ghostdisplays.api.click.DisplayClickHandler
@ -23,8 +24,24 @@ interface DisplayController<T : Display> {
fun applyEntityUpdate(mutator: (T) -> Unit) fun applyEntityUpdate(mutator: (T) -> Unit)
/**
* Displayの基本可視状態を設定します
* ルールにマッチしなかった場合のデフォルトの振る舞いとなります
*/
fun setBaseVisibility(visible: Boolean)
/**
* 従来の簡易メソッドAction=ADD として登録します
*/
fun addAudience(predicate: AudiencePredicate): HandlerRegistration fun addAudience(predicate: AudiencePredicate): HandlerRegistration
/**
* ルールを追加します評価順序は追加順です
*/
fun addAudienceRule(predicate: AudiencePredicate, action: AudienceAction): HandlerRegistration
fun clearAudienceRules()
fun refreshAudience(target: Player? = null) fun refreshAudience(target: Player? = null)
fun destroy() fun destroy()

View File

@ -0,0 +1,16 @@
package net.hareworks.ghostdisplays.api.audience
/**
* Predicateの評価結果に対するアクション
*/
enum class AudienceAction {
/**
* 表示対象に追加する (Visible = true)
*/
ADD,
/**
* 表示対象から除外する (Visible = false)
*/
EXCLUDE
}

View File

@ -1,5 +1,6 @@
package net.hareworks.ghostdisplays.api.audience package net.hareworks.ghostdisplays.api.audience
import org.bukkit.Location import org.bukkit.Location
import org.bukkit.entity.Player import org.bukkit.entity.Player
import java.util.UUID import java.util.UUID
@ -19,7 +20,15 @@ object AudiencePredicates {
player.uniqueId == target player.uniqueId == target
} }
fun players(targets: Collection<UUID>): AudiencePredicate {
val set = targets.toSet()
return AudiencePredicate { player ->
player.uniqueId in set
}
}
fun near(location: Location, radius: Double): AudiencePredicate { fun near(location: Location, radius: Double): AudiencePredicate {
val radiusSq = radius * radius val radiusSq = radius * radius
val worldName = location.world?.name val worldName = location.world?.name
require(worldName != null) { "Location must have a world" } require(worldName != null) { "Location must have a world" }

View File

@ -111,6 +111,9 @@ object CommandRegistrar {
literal("delete") { literal("delete") {
string("id") { string("id") {
suggests { prefix ->
manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) }
}
executes { executes {
val id = argument<String>("id") val id = argument<String>("id")
try { try {
@ -139,6 +142,9 @@ object CommandRegistrar {
literal("info") { literal("info") {
string("id") { string("id") {
suggests { prefix ->
manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) }
}
executes { executes {
val id = argument<String>("id") val id = argument<String>("id")
val display = manager.findDisplay(id) val display = manager.findDisplay(id)
@ -161,6 +167,9 @@ object CommandRegistrar {
literal("viewer") { literal("viewer") {
literal("add") { literal("add") {
string("id") { string("id") {
suggests { prefix ->
manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) }
}
players("targets") { players("targets") {
executes { executes {
val id = argument<String>("id") val id = argument<String>("id")
@ -177,6 +186,9 @@ object CommandRegistrar {
} }
literal("remove") { literal("remove") {
string("id") { string("id") {
suggests { prefix ->
manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) }
}
players("targets") { players("targets") {
executes { executes {
val id = argument<String>("id") val id = argument<String>("id")
@ -193,6 +205,9 @@ object CommandRegistrar {
} }
literal("clear") { literal("clear") {
string("id") { string("id") {
suggests { prefix ->
manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) }
}
executes { executes {
val id = argument<String>("id") val id = argument<String>("id")
try { try {
@ -209,6 +224,12 @@ object CommandRegistrar {
literal("text") { literal("text") {
literal("edit") { literal("edit") {
string("id") { string("id") {
suggests { prefix ->
manager.listDisplays()
.filter { it.kind == DisplayKind.TEXT }
.map { it.id }
.filter { it.startsWith(prefix, ignoreCase = true) }
}
executes { executes {
val player = sender.requirePlayer() ?: return@executes val player = sender.requirePlayer() ?: return@executes
val id = argument<String>("id") val id = argument<String>("id")
@ -224,6 +245,12 @@ object CommandRegistrar {
} }
literal("set") { literal("set") {
string("id") { string("id") {
suggests { prefix ->
manager.listDisplays()
.filter { it.kind == DisplayKind.TEXT }
.map { it.id }
.filter { it.startsWith(prefix, ignoreCase = true) }
}
string("content") { string("content") {
executes { executes {
val id = argument<String>("id") val id = argument<String>("id")
@ -254,6 +281,12 @@ object CommandRegistrar {
literal("block") { literal("block") {
literal("set") { literal("set") {
string("id") { string("id") {
suggests { prefix ->
manager.listDisplays()
.filter { it.kind == DisplayKind.BLOCK }
.map { it.id }
.filter { it.startsWith(prefix, ignoreCase = true) }
}
string("state") { string("state") {
executes { executes {
val id = argument<String>("id") val id = argument<String>("id")
@ -276,6 +309,12 @@ object CommandRegistrar {
literal("item") { literal("item") {
literal("set") { literal("set") {
string("id") { string("id") {
suggests { prefix ->
manager.listDisplays()
.filter { it.kind == DisplayKind.ITEM }
.map { it.id }
.filter { it.startsWith(prefix, ignoreCase = true) }
}
string("material") { string("material") {
executes { executes {
val id = argument<String>("id") val id = argument<String>("id")
@ -298,25 +337,67 @@ object CommandRegistrar {
} }
literal("audience") { literal("audience") {
literal("base") {
string("id") {
suggests { prefix ->
manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) }
}
bool("visible") {
executes {
val id = argument<String>("id")
val visible = argument<Boolean>("visible")
try {
manager.setBaseVisibility(id, visible)
sender.success("Base visibility for '$id' set to $visible.")
} catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to set base visibility.")
}
}
}
}
}
literal("permission") { literal("permission") {
literal("add") { literal("add") {
string("id") { string("id") {
suggests { prefix ->
manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) }
}
string("permission") { string("permission") {
executes { executes {
val id = argument<String>("id") val id = argument<String>("id")
val perm = argument<String>("permission") val perm = argument<String>("permission")
try { try {
manager.addPermissionAudience(id, perm) manager.addPermissionAudience(id, perm)
sender.success("Permission audience '$perm' added to '$id'.") sender.success("Permission audience '$perm' added to '$id' (ADD).")
} catch (ex: DisplayOperationException) { } catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to add permission audience.") sender.failure(ex.message ?: "Failed to add permission audience.")
} }
} }
string("action") {
suggests { listOf("ADD", "EXCLUDE") }
executes {
val id = argument<String>("id")
val perm = argument<String>("permission")
val actionName = argument<String>("action").uppercase()
val action = runCatching { net.hareworks.ghostdisplays.api.audience.AudienceAction.valueOf(actionName) }
.getOrElse { return@executes sender.failure("Invalid action '$actionName'. Use ADD or EXCLUDE.") }
try {
manager.addPermissionAudience(id, perm, action)
sender.success("Permission audience '$perm' added to '$id' ($action).")
} catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to add permission audience.")
}
}
}
} }
} }
} }
literal("remove") { literal("remove") {
string("id") { string("id") {
suggests { prefix ->
manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) }
}
string("permission") { string("permission") {
executes { executes {
val id = argument<String>("id") val id = argument<String>("id")
@ -337,23 +418,100 @@ object CommandRegistrar {
literal("near") { literal("near") {
literal("set") { literal("set") {
string("id") { string("id") {
suggests { prefix ->
manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) }
}
float("radius", min = 1.0) { float("radius", min = 1.0) {
executes { executes {
val id = argument<String>("id") val id = argument<String>("id")
val radius = argument<Double>("radius") val radius = argument<Double>("radius")
try { try {
manager.setNearAudience(id, radius) manager.setNearAudience(id, radius)
sender.success("Radius audience for '$id' set to ${String.format("%.1f", radius)} block(s).") sender.success("Radius audience for '$id' set to ${String.format("%.1f", radius)} block(s) (ADD).")
} catch (ex: DisplayOperationException) { } catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to update radius audience.") sender.failure(ex.message ?: "Failed to update radius audience.")
} }
} }
string("action") {
suggests { listOf("ADD", "EXCLUDE") }
executes {
val id = argument<String>("id")
val radius = argument<Double>("radius")
val actionName = argument<String>("action").uppercase()
val action = runCatching { net.hareworks.ghostdisplays.api.audience.AudienceAction.valueOf(actionName) }
.getOrElse { return@executes sender.failure("Invalid action '$actionName'. Use ADD or EXCLUDE.") }
try {
manager.setNearAudience(id, radius, action)
sender.success("Radius audience for '$id' set to ${String.format("%.1f", radius)} block(s) ($action).")
} catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to update radius audience.")
}
}
}
}
}
}
}
literal("player") {
literal("add") {
string("id") {
suggests { prefix ->
manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) }
}
players("targets") {
executes {
val id = argument<String>("id")
val targets = argument<List<Player>>("targets")
try {
targets.forEach { manager.addPlayerAudience(id, it.uniqueId, it.name) }
sender.success("Added ${targets.size} player(s) to audience of '$id' (ADD).")
} catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to add player audience.")
}
}
string("action") {
suggests { listOf("ADD", "EXCLUDE") }
executes {
val id = argument<String>("id")
val targets = argument<List<Player>>("targets")
val actionName = argument<String>("action").uppercase()
val action = runCatching { net.hareworks.ghostdisplays.api.audience.AudienceAction.valueOf(actionName) }
.getOrElse { return@executes sender.failure("Invalid action '$actionName'. Use ADD or EXCLUDE.") }
try {
targets.forEach { manager.addPlayerAudience(id, it.uniqueId, it.name, action) }
sender.success("Added ${targets.size} player(s) to audience of '$id' ($action).")
} catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to add player audience.")
}
}
}
}
}
}
literal("remove") {
string("id") {
suggests { prefix ->
manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) }
}
players("targets") {
executes {
val id = argument<String>("id")
val targets = argument<List<Player>>("targets")
var count = 0
targets.forEach {
if (manager.removePlayerAudience(id, it.uniqueId)) count++
}
sender.success("Removed $count/${targets.size} player(s) from audience of '$id'.")
}
} }
} }
} }
} }
literal("clear") { literal("clear") {
string("id") { string("id") {
suggests { prefix ->
manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) }
}
executes { executes {
val id = argument<String>("id") val id = argument<String>("id")
try { try {
@ -379,8 +537,11 @@ private fun CommandSender.showUsage() {
info(" /ghostdisplay list | info <id> | delete <id>") info(" /ghostdisplay list | info <id> | delete <id>")
} }
private fun Player.anchorLocation(): Location = private fun Player.anchorLocation(): Location {
eyeLocation.clone().add(direction.normalize().multiply(1.5)) val eye = eyeLocation.clone()
val forward = eye.direction.normalize().multiply(1.5)
return eye.add(forward)
}
private fun parseBlockData(state: String): BlockData = private fun parseBlockData(state: String): BlockData =
Bukkit.createBlockData(state) Bukkit.createBlockData(state)

View File

@ -6,6 +6,7 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.logging.Logger import java.util.logging.Logger
import net.hareworks.ghostdisplays.api.DisplayService import net.hareworks.ghostdisplays.api.DisplayService
import net.hareworks.ghostdisplays.api.InteractionOptions import net.hareworks.ghostdisplays.api.InteractionOptions
import net.hareworks.ghostdisplays.api.audience.AudienceAction
import net.hareworks.ghostdisplays.api.audience.AudiencePredicates import net.hareworks.ghostdisplays.api.audience.AudiencePredicates
import net.hareworks.ghostdisplays.display.DisplayManager.DisplayOperationException import net.hareworks.ghostdisplays.display.DisplayManager.DisplayOperationException
import net.kyori.adventure.text.Component import net.kyori.adventure.text.Component
@ -79,7 +80,7 @@ class DisplayManager(
ensureIdAvailable(normalized) ensureIdAvailable(normalized)
val safeLocation = location.clone() val safeLocation = location.clone()
val controller = service.createItemDisplay(safeLocation, itemStack.clone(), INTERACTION_DEFAULT) val controller = service.createItemDisplay(safeLocation, itemStack.clone(), INTERACTION_DEFAULT)
controller.applyEntityUpdate { it.itemStack = itemStack.clone() } controller.applyEntityUpdate { it.setItemStack(itemStack.clone()) }
val managed = ManagedDisplay.Item( val managed = ManagedDisplay.Item(
id = normalized, id = normalized,
controller = controller, controller = controller,
@ -115,7 +116,7 @@ class DisplayManager(
fun updateItem(id: String, itemStack: ItemStack) { fun updateItem(id: String, itemStack: ItemStack) {
val display = requireItem(id) val display = requireItem(id)
val clone = itemStack.clone() val clone = itemStack.clone()
display.controller.applyEntityUpdate { it.itemStack = clone } display.controller.applyEntityUpdate { it.setItemStack(clone) }
display.itemStack = clone display.itemStack = clone
} }
@ -142,17 +143,22 @@ class DisplayManager(
viewers.forEach { display.controller.hide(it) } viewers.forEach { display.controller.hide(it) }
} }
fun addPermissionAudience(id: String, permission: String) { fun setBaseVisibility(id: String, visible: Boolean) {
val display = requireDisplay(id)
display.controller.setBaseVisibility(visible)
}
fun addPermissionAudience(id: String, permission: String, action: AudienceAction = AudienceAction.ADD) {
val display = requireDisplay(id) val display = requireDisplay(id)
val key = "perm:${permission.lowercase()}" val key = "perm:${permission.lowercase()}"
if (display.removeAudienceBinding(key)) { if (display.removeAudienceBinding(key)) {
logger.info("Replacing permission audience '$permission' for $id") logger.info("Replacing permission audience '$permission' for $id")
} }
val registration = display.controller.addAudience(AudiencePredicates.permission(permission)) val registration = display.controller.addAudienceRule(AudiencePredicates.permission(permission), action)
display.registerAudienceBinding( display.registerAudienceBinding(
AudienceBinding( AudienceBinding(
key = key, key = key,
description = "permission:$permission", description = "permission:$permission [${action.name}]",
registration = registration registration = registration
) )
) )
@ -164,24 +170,45 @@ class DisplayManager(
return display.removeAudienceBinding(key) return display.removeAudienceBinding(key)
} }
fun setNearAudience(id: String, radius: Double) { fun setNearAudience(id: String, radius: Double, action: AudienceAction = AudienceAction.ADD) {
require(radius > 0) { "Radius must be positive." } require(radius > 0) { "Radius must be positive." }
val display = requireDisplay(id) val display = requireDisplay(id)
val key = "near" val key = "near"
display.removeAudienceBinding(key) display.removeAudienceBinding(key)
val registration = display.controller.addAudience(AudiencePredicates.near(display.location, radius)) val registration = display.controller.addAudienceRule(AudiencePredicates.near(display.location, radius), action)
display.registerAudienceBinding( display.registerAudienceBinding(
AudienceBinding( AudienceBinding(
key = key, key = key,
description = "radius:${String.format("%.2f", radius)}", description = "radius:${String.format("%.2f", radius)} [${action.name}]",
registration = registration registration = registration
) )
) )
} }
fun addPlayerAudience(id: String, playerId: UUID, playerName: String, action: AudienceAction = AudienceAction.ADD) {
val display = requireDisplay(id)
val key = "player:$playerId"
display.removeAudienceBinding(key)
val registration = display.controller.addAudienceRule(AudiencePredicates.uuid(playerId), action)
display.registerAudienceBinding(
AudienceBinding(
key = key,
description = "player:$playerName [${action.name}]",
registration = registration
)
)
}
fun removePlayerAudience(id: String, playerId: UUID): Boolean {
val display = requireDisplay(id)
val key = "player:$playerId"
return display.removeAudienceBinding(key)
}
fun clearAudiences(id: String) { fun clearAudiences(id: String) {
val display = requireDisplay(id) val display = requireDisplay(id)
display.clearAudiences() display.clearAudiences()
display.controller.clearAudienceRules()
} }
fun destroyAll() { fun destroyAll() {

View File

@ -37,7 +37,7 @@ class EditSessionManager(
event.player.sendMessage("GhostDisplays: editing for '$displayId' cancelled.") event.player.sendMessage("GhostDisplays: editing for '$displayId' cancelled.")
return return
} }
Bukkit.getScheduler().runTask(plugin) { Bukkit.getScheduler().runTask(plugin, Runnable {
try { try {
manager.updateText(displayId, message) manager.updateText(displayId, message)
event.player.sendMessage("GhostDisplays: updated text for '$displayId'.") event.player.sendMessage("GhostDisplays: updated text for '$displayId'.")
@ -46,7 +46,7 @@ class EditSessionManager(
} finally { } finally {
sessions.remove(event.player.uniqueId) sessions.remove(event.player.uniqueId)
} }
} })
} }
@EventHandler @EventHandler

View File

@ -46,7 +46,7 @@ internal class DefaultDisplayService(
interaction: InteractionOptions, interaction: InteractionOptions,
builder: ItemDisplay.() -> Unit builder: ItemDisplay.() -> Unit
): DisplayController<ItemDisplay> = spawnDisplay(location, ItemDisplay::class.java, interaction) { ): DisplayController<ItemDisplay> = spawnDisplay(location, ItemDisplay::class.java, interaction) {
it.itemStack = itemStack.clone() it.setItemStack(itemStack.clone())
builder(it) builder(it)
} }
@ -94,7 +94,7 @@ internal class DefaultDisplayService(
action() action()
} else { } else {
val future = CompletableFuture<T>() val future = CompletableFuture<T>()
Bukkit.getScheduler().runTask(plugin) { future.complete(action()) } Bukkit.getScheduler().runTask(plugin, Runnable { future.complete(action()) })
future.join() future.join()
} }
} }

View File

@ -7,6 +7,7 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import net.hareworks.ghostdisplays.api.DisplayController import net.hareworks.ghostdisplays.api.DisplayController
import net.hareworks.ghostdisplays.api.audience.AudienceAction
import net.hareworks.ghostdisplays.api.audience.AudiencePredicate import net.hareworks.ghostdisplays.api.audience.AudiencePredicate
import net.hareworks.ghostdisplays.api.click.ClickPriority import net.hareworks.ghostdisplays.api.click.ClickPriority
import net.hareworks.ghostdisplays.api.click.ClickSurface import net.hareworks.ghostdisplays.api.click.ClickSurface
@ -30,7 +31,11 @@ internal class BaseDisplayController<T : Display>(
private val destroyed = AtomicBoolean(false) private val destroyed = AtomicBoolean(false)
private val viewerCounts = ConcurrentHashMap<UUID, Int>() private val viewerCounts = ConcurrentHashMap<UUID, Int>()
private val handlers = CopyOnWriteArrayList<HandlerEntry>() private val handlers = CopyOnWriteArrayList<HandlerEntry>()
private val audiences = CopyOnWriteArrayList<AudienceBindingImpl>()
// Audience Management
private var baseVisibility: Boolean = false
private val audienceRules = CopyOnWriteArrayList<RuleEntry>()
private val autoVisiblePlayers = ConcurrentHashMap.newKeySet<UUID>()
override fun show(player: Player) { override fun show(player: Player) {
runSync { runSync {
@ -64,7 +69,6 @@ internal class BaseDisplayController<T : Display>(
override fun applyEntityUpdate(mutator: (T) -> Unit) { override fun applyEntityUpdate(mutator: (T) -> Unit) {
callSync { callSync {
mutator(display) mutator(display)
display.updateDisplay(false)
} }
} }
@ -82,8 +86,8 @@ internal class BaseDisplayController<T : Display>(
viewerCounts.clear() viewerCounts.clear()
} }
handlers.clear() handlers.clear()
audiences.forEach { it.clear() } audienceRules.clear()
audiences.clear() autoVisiblePlayers.clear()
} }
override fun onClick(priority: ClickPriority, handler: DisplayClickHandler): HandlerRegistration { override fun onClick(priority: ClickPriority, handler: DisplayClickHandler): HandlerRegistration {
@ -94,26 +98,67 @@ internal class BaseDisplayController<T : Display>(
} }
} }
override fun addAudience(predicate: AudiencePredicate): HandlerRegistration { override fun setBaseVisibility(visible: Boolean) {
val binding = AudienceBindingImpl(predicate) this.baseVisibility = visible
audiences += binding refreshAudience()
refreshAudienceInternal(binding = binding)
return HandlerRegistration { binding.unregister() }
} }
private fun refreshAudienceInternal(target: Player? = null, binding: AudienceBindingImpl? = null) { override fun addAudience(predicate: AudiencePredicate): HandlerRegistration {
return addAudienceRule(predicate, AudienceAction.ADD)
}
override fun addAudienceRule(predicate: AudiencePredicate, action: AudienceAction): HandlerRegistration {
val entry = RuleEntry(predicate, action)
audienceRules.add(entry)
refreshAudience()
return HandlerRegistration {
audienceRules.remove(entry)
refreshAudience()
}
}
override fun clearAudienceRules() {
audienceRules.clear()
refreshAudience()
}
override fun refreshAudience(target: Player?) {
refreshAudienceInternal(target)
}
private fun refreshAudienceInternal(target: Player? = null) {
runSync { runSync {
val players = target?.let { listOf(it) } ?: Bukkit.getOnlinePlayers() val players = target?.let { listOf(it) } ?: Bukkit.getOnlinePlayers()
val targets = binding?.let { listOf(it) } ?: audiences if (players.isEmpty()) return@runSync
if (players.isEmpty() || targets.isEmpty()) return@runSync
players.forEach { player -> players.forEach { player ->
targets.forEach { it.evaluate(player) } val uuid = player.uniqueId
val shouldBeVisible = evaluateVisibility(player)
val isCurrentlyAutoVisible = autoVisiblePlayers.contains(uuid)
if (shouldBeVisible && !isCurrentlyAutoVisible) {
autoVisiblePlayers.add(uuid)
show(player)
} else if (!shouldBeVisible && isCurrentlyAutoVisible) {
autoVisiblePlayers.remove(uuid)
hide(player)
}
} }
} }
} }
override fun refreshAudience(target: Player?) { private fun evaluateVisibility(player: Player): Boolean {
refreshAudienceInternal(target, null) var visible = baseVisibility
for (rule in audienceRules) {
val matches = runCatching { rule.predicate.test(player) }.getOrDefault(false)
if (matches) {
when (rule.action) {
AudienceAction.ADD -> visible = true
AudienceAction.EXCLUDE -> visible = false
}
}
}
return visible
} }
internal fun handleClick(event: PlayerInteractEntityEvent, clicked: Entity, surface: ClickSurface) { internal fun handleClick(event: PlayerInteractEntityEvent, clicked: Entity, surface: ClickSurface) {
@ -136,14 +181,17 @@ internal class BaseDisplayController<T : Display>(
internal fun handlePlayerQuit(player: Player) { internal fun handlePlayerQuit(player: Player) {
val uuid = player.uniqueId val uuid = player.uniqueId
viewerCounts.remove(uuid) viewerCounts.remove(uuid)
audiences.forEach { it.forget(uuid) } autoVisiblePlayers.remove(uuid)
// Note: ref count logic doesn't strictly need persistent cleanup if we remove from counts,
// but removing from autoVisiblePlayers ensures we don't think we're showing it if they rejoin?
// Actually, if they quit, we should probably clear for them.
} }
private fun runSync(action: () -> Unit) { private fun runSync(action: () -> Unit) {
if (Bukkit.isPrimaryThread()) { if (Bukkit.isPrimaryThread()) {
action() action()
} else { } else {
Bukkit.getScheduler().runTask(plugin, action) Bukkit.getScheduler().runTask(plugin, Runnable { action() })
} }
} }
@ -152,45 +200,15 @@ internal class BaseDisplayController<T : Display>(
action() action()
} else { } else {
val future = CompletableFuture<R>() val future = CompletableFuture<R>()
Bukkit.getScheduler().runTask(plugin) { future.complete(action()) } Bukkit.getScheduler().runTask(plugin, Runnable { future.complete(action()) })
future.join() future.join()
} }
} }
private inner class AudienceBindingImpl( private data class RuleEntry(
private val predicate: AudiencePredicate val predicate: AudiencePredicate,
) { val action: AudienceAction
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( private data class HandlerEntry(
val priority: ClickPriority, val priority: ClickPriority,