From bc774765c57158a787234086ff71b3d2032e967e Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 7 Dec 2025 03:41:42 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Audience=E3=81=AE=E3=82=BF=E3=82=A4?= =?UTF-8?q?=E3=83=9F=E3=83=B3=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- kommand-lib | 2 +- .../ghostdisplays/GhostDisplaysPlugin.kt | 0 .../ghostdisplays/api/DisplayController.kt | 17 ++ .../ghostdisplays/api/DisplayService.kt | 0 .../ghostdisplays/api/InteractionOptions.kt | 0 .../api/audience/AudienceAction.kt | 16 ++ .../api/audience/AudiencePredicate.kt | 0 .../api/audience/AudiencePredicates.kt | 9 + .../ghostdisplays/api/click/DisplayClick.kt | 0 .../ghostdisplays/command/CommandRegistrar.kt | 169 +++++++++++++++++- .../ghostdisplays/display/DisplayManager.kt | 43 ++++- .../display/EditSessionManager.kt | 4 +- .../ghostdisplays/display/ManagedDisplay.kt | 0 .../internal/DefaultDisplayService.kt | 4 +- .../controller/BaseDisplayController.kt | 122 +++++++------ .../internal/controller/DisplayRegistry.kt | 0 17 files changed, 318 insertions(+), 72 deletions(-) rename {main => src/main}/kotlin/net/hareworks/ghostdisplays/GhostDisplaysPlugin.kt (100%) rename {main => src/main}/kotlin/net/hareworks/ghostdisplays/api/DisplayController.kt (61%) rename {main => src/main}/kotlin/net/hareworks/ghostdisplays/api/DisplayService.kt (100%) rename {main => src/main}/kotlin/net/hareworks/ghostdisplays/api/InteractionOptions.kt (100%) create mode 100644 src/main/kotlin/net/hareworks/ghostdisplays/api/audience/AudienceAction.kt rename {main => src/main}/kotlin/net/hareworks/ghostdisplays/api/audience/AudiencePredicate.kt (100%) rename {main => src/main}/kotlin/net/hareworks/ghostdisplays/api/audience/AudiencePredicates.kt (83%) rename {main => src/main}/kotlin/net/hareworks/ghostdisplays/api/click/DisplayClick.kt (100%) rename {main => src/main}/kotlin/net/hareworks/ghostdisplays/command/CommandRegistrar.kt (65%) rename {main => src/main}/kotlin/net/hareworks/ghostdisplays/display/DisplayManager.kt (83%) rename {main => src/main}/kotlin/net/hareworks/ghostdisplays/display/EditSessionManager.kt (96%) rename {main => src/main}/kotlin/net/hareworks/ghostdisplays/display/ManagedDisplay.kt (100%) rename {main => src/main}/kotlin/net/hareworks/ghostdisplays/internal/DefaultDisplayService.kt (96%) rename {main => src/main}/kotlin/net/hareworks/ghostdisplays/internal/controller/BaseDisplayController.kt (66%) rename {main => src/main}/kotlin/net/hareworks/ghostdisplays/internal/controller/DisplayRegistry.kt (100%) diff --git a/.gitignore b/.gitignore index 1b6985c..8e774d4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -# Ignore Gradle project-specific cache directory .gradle - -# Ignore Gradle build output directory +bin build diff --git a/kommand-lib b/kommand-lib index 559341c..9af2931 160000 --- a/kommand-lib +++ b/kommand-lib @@ -1 +1 @@ -Subproject commit 559341c0415699a2b39a1a523f4ef4ed93ce3fdc +Subproject commit 9af293122b860d8c65d68a2bdc02a9c2c5e206cc diff --git a/main/kotlin/net/hareworks/ghostdisplays/GhostDisplaysPlugin.kt b/src/main/kotlin/net/hareworks/ghostdisplays/GhostDisplaysPlugin.kt similarity index 100% rename from main/kotlin/net/hareworks/ghostdisplays/GhostDisplaysPlugin.kt rename to src/main/kotlin/net/hareworks/ghostdisplays/GhostDisplaysPlugin.kt diff --git a/main/kotlin/net/hareworks/ghostdisplays/api/DisplayController.kt b/src/main/kotlin/net/hareworks/ghostdisplays/api/DisplayController.kt similarity index 61% rename from main/kotlin/net/hareworks/ghostdisplays/api/DisplayController.kt rename to src/main/kotlin/net/hareworks/ghostdisplays/api/DisplayController.kt index 4efccb6..76ef2ce 100644 --- a/main/kotlin/net/hareworks/ghostdisplays/api/DisplayController.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/api/DisplayController.kt @@ -1,5 +1,6 @@ package net.hareworks.ghostdisplays.api +import net.hareworks.ghostdisplays.api.audience.AudienceAction import net.hareworks.ghostdisplays.api.audience.AudiencePredicate import net.hareworks.ghostdisplays.api.click.ClickPriority import net.hareworks.ghostdisplays.api.click.DisplayClickHandler @@ -23,8 +24,24 @@ interface DisplayController { fun applyEntityUpdate(mutator: (T) -> Unit) + /** + * Displayの基本可視状態を設定します。 + * ルールにマッチしなかった場合のデフォルトの振る舞いとなります。 + */ + fun setBaseVisibility(visible: Boolean) + + /** + * 従来の簡易メソッド。Action=ADD として登録します。 + */ fun addAudience(predicate: AudiencePredicate): HandlerRegistration + /** + * ルールを追加します。評価順序は追加順です。 + */ + fun addAudienceRule(predicate: AudiencePredicate, action: AudienceAction): HandlerRegistration + + fun clearAudienceRules() + fun refreshAudience(target: Player? = null) fun destroy() diff --git a/main/kotlin/net/hareworks/ghostdisplays/api/DisplayService.kt b/src/main/kotlin/net/hareworks/ghostdisplays/api/DisplayService.kt similarity index 100% rename from main/kotlin/net/hareworks/ghostdisplays/api/DisplayService.kt rename to src/main/kotlin/net/hareworks/ghostdisplays/api/DisplayService.kt diff --git a/main/kotlin/net/hareworks/ghostdisplays/api/InteractionOptions.kt b/src/main/kotlin/net/hareworks/ghostdisplays/api/InteractionOptions.kt similarity index 100% rename from main/kotlin/net/hareworks/ghostdisplays/api/InteractionOptions.kt rename to src/main/kotlin/net/hareworks/ghostdisplays/api/InteractionOptions.kt diff --git a/src/main/kotlin/net/hareworks/ghostdisplays/api/audience/AudienceAction.kt b/src/main/kotlin/net/hareworks/ghostdisplays/api/audience/AudienceAction.kt new file mode 100644 index 0000000..510bbc7 --- /dev/null +++ b/src/main/kotlin/net/hareworks/ghostdisplays/api/audience/AudienceAction.kt @@ -0,0 +1,16 @@ +package net.hareworks.ghostdisplays.api.audience + +/** + * Predicateの評価結果に対するアクション。 + */ +enum class AudienceAction { + /** + * 表示対象に追加する (Visible = true) + */ + ADD, + + /** + * 表示対象から除外する (Visible = false) + */ + EXCLUDE +} diff --git a/main/kotlin/net/hareworks/ghostdisplays/api/audience/AudiencePredicate.kt b/src/main/kotlin/net/hareworks/ghostdisplays/api/audience/AudiencePredicate.kt similarity index 100% rename from main/kotlin/net/hareworks/ghostdisplays/api/audience/AudiencePredicate.kt rename to src/main/kotlin/net/hareworks/ghostdisplays/api/audience/AudiencePredicate.kt diff --git a/main/kotlin/net/hareworks/ghostdisplays/api/audience/AudiencePredicates.kt b/src/main/kotlin/net/hareworks/ghostdisplays/api/audience/AudiencePredicates.kt similarity index 83% rename from main/kotlin/net/hareworks/ghostdisplays/api/audience/AudiencePredicates.kt rename to src/main/kotlin/net/hareworks/ghostdisplays/api/audience/AudiencePredicates.kt index 7ac73af..f910645 100644 --- a/main/kotlin/net/hareworks/ghostdisplays/api/audience/AudiencePredicates.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/api/audience/AudiencePredicates.kt @@ -1,5 +1,6 @@ package net.hareworks.ghostdisplays.api.audience + import org.bukkit.Location import org.bukkit.entity.Player import java.util.UUID @@ -19,7 +20,15 @@ object AudiencePredicates { player.uniqueId == target } + fun players(targets: Collection): AudiencePredicate { + val set = targets.toSet() + return AudiencePredicate { player -> + player.uniqueId in set + } + } + fun near(location: Location, radius: Double): AudiencePredicate { + val radiusSq = radius * radius val worldName = location.world?.name require(worldName != null) { "Location must have a world" } diff --git a/main/kotlin/net/hareworks/ghostdisplays/api/click/DisplayClick.kt b/src/main/kotlin/net/hareworks/ghostdisplays/api/click/DisplayClick.kt similarity index 100% rename from main/kotlin/net/hareworks/ghostdisplays/api/click/DisplayClick.kt rename to src/main/kotlin/net/hareworks/ghostdisplays/api/click/DisplayClick.kt diff --git a/main/kotlin/net/hareworks/ghostdisplays/command/CommandRegistrar.kt b/src/main/kotlin/net/hareworks/ghostdisplays/command/CommandRegistrar.kt similarity index 65% rename from main/kotlin/net/hareworks/ghostdisplays/command/CommandRegistrar.kt rename to src/main/kotlin/net/hareworks/ghostdisplays/command/CommandRegistrar.kt index a432e09..f4a9310 100644 --- a/main/kotlin/net/hareworks/ghostdisplays/command/CommandRegistrar.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/command/CommandRegistrar.kt @@ -111,6 +111,9 @@ object CommandRegistrar { literal("delete") { string("id") { + suggests { prefix -> + manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) } + } executes { val id = argument("id") try { @@ -139,6 +142,9 @@ object CommandRegistrar { literal("info") { string("id") { + suggests { prefix -> + manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) } + } executes { val id = argument("id") val display = manager.findDisplay(id) @@ -161,6 +167,9 @@ object CommandRegistrar { literal("viewer") { literal("add") { string("id") { + suggests { prefix -> + manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) } + } players("targets") { executes { val id = argument("id") @@ -177,6 +186,9 @@ object CommandRegistrar { } literal("remove") { string("id") { + suggests { prefix -> + manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) } + } players("targets") { executes { val id = argument("id") @@ -193,6 +205,9 @@ object CommandRegistrar { } literal("clear") { string("id") { + suggests { prefix -> + manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) } + } executes { val id = argument("id") try { @@ -209,6 +224,12 @@ object CommandRegistrar { literal("text") { literal("edit") { string("id") { + suggests { prefix -> + manager.listDisplays() + .filter { it.kind == DisplayKind.TEXT } + .map { it.id } + .filter { it.startsWith(prefix, ignoreCase = true) } + } executes { val player = sender.requirePlayer() ?: return@executes val id = argument("id") @@ -224,6 +245,12 @@ object CommandRegistrar { } literal("set") { string("id") { + suggests { prefix -> + manager.listDisplays() + .filter { it.kind == DisplayKind.TEXT } + .map { it.id } + .filter { it.startsWith(prefix, ignoreCase = true) } + } string("content") { executes { val id = argument("id") @@ -254,6 +281,12 @@ object CommandRegistrar { literal("block") { literal("set") { string("id") { + suggests { prefix -> + manager.listDisplays() + .filter { it.kind == DisplayKind.BLOCK } + .map { it.id } + .filter { it.startsWith(prefix, ignoreCase = true) } + } string("state") { executes { val id = argument("id") @@ -276,6 +309,12 @@ object CommandRegistrar { literal("item") { literal("set") { string("id") { + suggests { prefix -> + manager.listDisplays() + .filter { it.kind == DisplayKind.ITEM } + .map { it.id } + .filter { it.startsWith(prefix, ignoreCase = true) } + } string("material") { executes { val id = argument("id") @@ -298,25 +337,67 @@ object CommandRegistrar { } 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("id") + val visible = argument("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("add") { string("id") { + suggests { prefix -> + manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) } + } string("permission") { executes { val id = argument("id") val perm = argument("permission") try { manager.addPermissionAudience(id, perm) - sender.success("Permission audience '$perm' added to '$id'.") + sender.success("Permission audience '$perm' added to '$id' (ADD).") } catch (ex: DisplayOperationException) { sender.failure(ex.message ?: "Failed to add permission audience.") } } + string("action") { + suggests { listOf("ADD", "EXCLUDE") } + executes { + val id = argument("id") + val perm = argument("permission") + val actionName = argument("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") { string("id") { + suggests { prefix -> + manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) } + } string("permission") { executes { val id = argument("id") @@ -337,23 +418,100 @@ object CommandRegistrar { literal("near") { literal("set") { string("id") { + suggests { prefix -> + manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) } + } float("radius", min = 1.0) { executes { val id = argument("id") val radius = argument("radius") try { 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) { sender.failure(ex.message ?: "Failed to update radius audience.") } } + string("action") { + suggests { listOf("ADD", "EXCLUDE") } + executes { + val id = argument("id") + val radius = argument("radius") + val actionName = argument("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("id") + val targets = argument>("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("id") + val targets = argument>("targets") + val actionName = argument("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("id") + val targets = argument>("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") { string("id") { + suggests { prefix -> + manager.listDisplays().map { it.id }.filter { it.startsWith(prefix, ignoreCase = true) } + } executes { val id = argument("id") try { @@ -379,8 +537,11 @@ private fun CommandSender.showUsage() { info(" /ghostdisplay list | info | delete ") } -private fun Player.anchorLocation(): Location = - eyeLocation.clone().add(direction.normalize().multiply(1.5)) +private fun Player.anchorLocation(): Location { + val eye = eyeLocation.clone() + val forward = eye.direction.normalize().multiply(1.5) + return eye.add(forward) +} private fun parseBlockData(state: String): BlockData = Bukkit.createBlockData(state) diff --git a/main/kotlin/net/hareworks/ghostdisplays/display/DisplayManager.kt b/src/main/kotlin/net/hareworks/ghostdisplays/display/DisplayManager.kt similarity index 83% rename from main/kotlin/net/hareworks/ghostdisplays/display/DisplayManager.kt rename to src/main/kotlin/net/hareworks/ghostdisplays/display/DisplayManager.kt index 41348d9..cfea6bc 100644 --- a/main/kotlin/net/hareworks/ghostdisplays/display/DisplayManager.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/display/DisplayManager.kt @@ -6,6 +6,7 @@ import java.util.concurrent.ConcurrentHashMap import java.util.logging.Logger import net.hareworks.ghostdisplays.api.DisplayService import net.hareworks.ghostdisplays.api.InteractionOptions +import net.hareworks.ghostdisplays.api.audience.AudienceAction import net.hareworks.ghostdisplays.api.audience.AudiencePredicates import net.hareworks.ghostdisplays.display.DisplayManager.DisplayOperationException import net.kyori.adventure.text.Component @@ -79,7 +80,7 @@ class DisplayManager( ensureIdAvailable(normalized) val safeLocation = location.clone() 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( id = normalized, controller = controller, @@ -115,7 +116,7 @@ class DisplayManager( fun updateItem(id: String, itemStack: ItemStack) { val display = requireItem(id) val clone = itemStack.clone() - display.controller.applyEntityUpdate { it.itemStack = clone } + display.controller.applyEntityUpdate { it.setItemStack(clone) } display.itemStack = clone } @@ -142,17 +143,22 @@ class DisplayManager( 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 key = "perm:${permission.lowercase()}" if (display.removeAudienceBinding(key)) { 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( AudienceBinding( key = key, - description = "permission:$permission", + description = "permission:$permission [${action.name}]", registration = registration ) ) @@ -164,24 +170,45 @@ class DisplayManager( 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." } val display = requireDisplay(id) val key = "near" 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( AudienceBinding( key = key, - description = "radius:${String.format("%.2f", radius)}", + description = "radius:${String.format("%.2f", radius)} [${action.name}]", 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) { val display = requireDisplay(id) display.clearAudiences() + display.controller.clearAudienceRules() } fun destroyAll() { diff --git a/main/kotlin/net/hareworks/ghostdisplays/display/EditSessionManager.kt b/src/main/kotlin/net/hareworks/ghostdisplays/display/EditSessionManager.kt similarity index 96% rename from main/kotlin/net/hareworks/ghostdisplays/display/EditSessionManager.kt rename to src/main/kotlin/net/hareworks/ghostdisplays/display/EditSessionManager.kt index ba5979c..363b2bd 100644 --- a/main/kotlin/net/hareworks/ghostdisplays/display/EditSessionManager.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/display/EditSessionManager.kt @@ -37,7 +37,7 @@ class EditSessionManager( event.player.sendMessage("GhostDisplays: editing for '$displayId' cancelled.") return } - Bukkit.getScheduler().runTask(plugin) { + Bukkit.getScheduler().runTask(plugin, Runnable { try { manager.updateText(displayId, message) event.player.sendMessage("GhostDisplays: updated text for '$displayId'.") @@ -46,7 +46,7 @@ class EditSessionManager( } finally { sessions.remove(event.player.uniqueId) } - } + }) } @EventHandler diff --git a/main/kotlin/net/hareworks/ghostdisplays/display/ManagedDisplay.kt b/src/main/kotlin/net/hareworks/ghostdisplays/display/ManagedDisplay.kt similarity index 100% rename from main/kotlin/net/hareworks/ghostdisplays/display/ManagedDisplay.kt rename to src/main/kotlin/net/hareworks/ghostdisplays/display/ManagedDisplay.kt diff --git a/main/kotlin/net/hareworks/ghostdisplays/internal/DefaultDisplayService.kt b/src/main/kotlin/net/hareworks/ghostdisplays/internal/DefaultDisplayService.kt similarity index 96% rename from main/kotlin/net/hareworks/ghostdisplays/internal/DefaultDisplayService.kt rename to src/main/kotlin/net/hareworks/ghostdisplays/internal/DefaultDisplayService.kt index 2e329f7..1a88a76 100644 --- a/main/kotlin/net/hareworks/ghostdisplays/internal/DefaultDisplayService.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/internal/DefaultDisplayService.kt @@ -46,7 +46,7 @@ internal class DefaultDisplayService( interaction: InteractionOptions, builder: ItemDisplay.() -> Unit ): DisplayController = spawnDisplay(location, ItemDisplay::class.java, interaction) { - it.itemStack = itemStack.clone() + it.setItemStack(itemStack.clone()) builder(it) } @@ -94,7 +94,7 @@ internal class DefaultDisplayService( action() } else { val future = CompletableFuture() - Bukkit.getScheduler().runTask(plugin) { future.complete(action()) } + Bukkit.getScheduler().runTask(plugin, Runnable { future.complete(action()) }) future.join() } } diff --git a/main/kotlin/net/hareworks/ghostdisplays/internal/controller/BaseDisplayController.kt b/src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/BaseDisplayController.kt similarity index 66% rename from main/kotlin/net/hareworks/ghostdisplays/internal/controller/BaseDisplayController.kt rename to src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/BaseDisplayController.kt index 5ed0dda..9e8d3f5 100644 --- a/main/kotlin/net/hareworks/ghostdisplays/internal/controller/BaseDisplayController.kt +++ b/src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/BaseDisplayController.kt @@ -7,6 +7,7 @@ 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.AudienceAction import net.hareworks.ghostdisplays.api.audience.AudiencePredicate import net.hareworks.ghostdisplays.api.click.ClickPriority import net.hareworks.ghostdisplays.api.click.ClickSurface @@ -30,7 +31,11 @@ internal class BaseDisplayController( private val destroyed = AtomicBoolean(false) private val viewerCounts = ConcurrentHashMap() private val handlers = CopyOnWriteArrayList() - private val audiences = CopyOnWriteArrayList() + + // Audience Management + private var baseVisibility: Boolean = false + private val audienceRules = CopyOnWriteArrayList() + private val autoVisiblePlayers = ConcurrentHashMap.newKeySet() override fun show(player: Player) { runSync { @@ -64,7 +69,6 @@ internal class BaseDisplayController( override fun applyEntityUpdate(mutator: (T) -> Unit) { callSync { mutator(display) - display.updateDisplay(false) } } @@ -82,8 +86,8 @@ internal class BaseDisplayController( viewerCounts.clear() } handlers.clear() - audiences.forEach { it.clear() } - audiences.clear() + audienceRules.clear() + autoVisiblePlayers.clear() } override fun onClick(priority: ClickPriority, handler: DisplayClickHandler): HandlerRegistration { @@ -94,26 +98,67 @@ internal class BaseDisplayController( } } - override fun addAudience(predicate: AudiencePredicate): HandlerRegistration { - val binding = AudienceBindingImpl(predicate) - audiences += binding - refreshAudienceInternal(binding = binding) - return HandlerRegistration { binding.unregister() } + override fun setBaseVisibility(visible: Boolean) { + this.baseVisibility = visible + refreshAudience() } - 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 { val players = target?.let { listOf(it) } ?: Bukkit.getOnlinePlayers() - val targets = binding?.let { listOf(it) } ?: audiences - if (players.isEmpty() || targets.isEmpty()) return@runSync + if (players.isEmpty()) return@runSync + 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?) { - refreshAudienceInternal(target, null) + private fun evaluateVisibility(player: Player): Boolean { + 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) { @@ -136,14 +181,17 @@ internal class BaseDisplayController( internal fun handlePlayerQuit(player: Player) { val uuid = player.uniqueId 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) { if (Bukkit.isPrimaryThread()) { action() } else { - Bukkit.getScheduler().runTask(plugin, action) + Bukkit.getScheduler().runTask(plugin, Runnable { action() }) } } @@ -152,45 +200,15 @@ internal class BaseDisplayController( action() } else { val future = CompletableFuture() - Bukkit.getScheduler().runTask(plugin) { future.complete(action()) } + Bukkit.getScheduler().runTask(plugin, Runnable { future.complete(action()) }) future.join() } } - private inner class AudienceBindingImpl( - private val predicate: AudiencePredicate - ) { - private val activeViewers = ConcurrentHashMap.newKeySet() - - 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 RuleEntry( + val predicate: AudiencePredicate, + val action: AudienceAction + ) private data class HandlerEntry( val priority: ClickPriority, diff --git a/main/kotlin/net/hareworks/ghostdisplays/internal/controller/DisplayRegistry.kt b/src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/DisplayRegistry.kt similarity index 100% rename from main/kotlin/net/hareworks/ghostdisplays/internal/controller/DisplayRegistry.kt rename to src/main/kotlin/net/hareworks/ghostdisplays/internal/controller/DisplayRegistry.kt