Compare commits

..

2 Commits

Author SHA1 Message Date
f3ea947310 feat: エディットセッションを削除 2025-12-07 07:20:30 +09:00
b3bdc95f63 feat: 最適化とKommandアップデート 2025-12-07 07:14:46 +09:00
9 changed files with 57 additions and 111 deletions

@ -1 +1 @@
Subproject commit e0613cd05278222b0a2ac1c79a2d4ef317c9cea1 Subproject commit 3835c9b9e2d488681b39f4cfd827e3e3371a1e1b

View File

@ -3,7 +3,6 @@ package net.hareworks.ghostdisplays
import net.hareworks.ghostdisplays.api.DisplayService import net.hareworks.ghostdisplays.api.DisplayService
import net.hareworks.ghostdisplays.command.CommandRegistrar import net.hareworks.ghostdisplays.command.CommandRegistrar
import net.hareworks.ghostdisplays.display.DisplayManager import net.hareworks.ghostdisplays.display.DisplayManager
import net.hareworks.ghostdisplays.display.EditSessionManager
import net.hareworks.ghostdisplays.internal.DefaultDisplayService import net.hareworks.ghostdisplays.internal.DefaultDisplayService
import net.hareworks.ghostdisplays.internal.controller.DisplayRegistry import net.hareworks.ghostdisplays.internal.controller.DisplayRegistry
import net.hareworks.kommand_lib.KommandLib import net.hareworks.kommand_lib.KommandLib
@ -17,7 +16,6 @@ class GhostDisplaysPlugin : JavaPlugin() {
private lateinit var displayRegistry: DisplayRegistry private lateinit var displayRegistry: DisplayRegistry
private lateinit var serviceImpl: DisplayService private lateinit var serviceImpl: DisplayService
private lateinit var displayManager: DisplayManager private lateinit var displayManager: DisplayManager
private lateinit var editSessions: EditSessionManager
private lateinit var miniMessage: MiniMessage private lateinit var miniMessage: MiniMessage
private var permissionSession: MutationSession? = null private var permissionSession: MutationSession? = null
private var kommand: KommandLib? = null private var kommand: KommandLib? = null
@ -29,26 +27,22 @@ class GhostDisplaysPlugin : JavaPlugin() {
} }
serviceImpl = DefaultDisplayService(this, displayRegistry) serviceImpl = DefaultDisplayService(this, displayRegistry)
displayManager = DisplayManager(this, serviceImpl, miniMessage) displayManager = DisplayManager(this, serviceImpl, miniMessage)
editSessions = EditSessionManager(this, displayManager).also {
server.pluginManager.registerEvents(it, this)
}
server.servicesManager.register(DisplayService::class.java, serviceImpl, this, ServicePriority.Normal) server.servicesManager.register(DisplayService::class.java, serviceImpl, this, ServicePriority.Normal)
permissionSession = PermitsLib.session(this) permissionSession = PermitsLib.session(this)
kommand = CommandRegistrar.register(this, displayManager, editSessions, permissionSession!!) kommand = CommandRegistrar.register(this, displayManager, permissionSession!!)
instance = this instance = this
logger.info("GhostDisplays ready: ${displayRegistry.controllerCount()} controllers active.") logger.info("GhostDisplays ready: ${displayRegistry.controllerCount()} controllers active.")
server.scheduler.runTaskTimer(this, Runnable { server.scheduler.runTaskTimer(this, Runnable {
displayRegistry.tick() displayRegistry.updateAudiences()
}, 10L, 10L) }, 20L, 20L)
} }
override fun onDisable() { override fun onDisable() {
kommand?.unregister() kommand?.unregister()
kommand = null kommand = null
runCatching { editSessions.shutdown() }
runCatching { displayManager.destroyAll() } runCatching { displayManager.destroyAll() }
runCatching { serviceImpl.destroyAll() } runCatching { serviceImpl.destroyAll() }
permissionSession = null permissionSession = null

View File

@ -44,6 +44,12 @@ interface DisplayController<T : Display> {
fun refreshAudience(target: Player? = null) fun refreshAudience(target: Player? = null)
/**
* 定期的な更新チェックが必要かどうかを返します
* trueの場合サーバーのtick毎または定期タスクにrefreshAudienceが呼び出されます
*/
fun needsPeriodicUpdate(): Boolean = false
fun destroy() fun destroy()
fun onClick(priority: ClickPriority = ClickPriority.NORMAL, handler: DisplayClickHandler): HandlerRegistration fun onClick(priority: ClickPriority = ClickPriority.NORMAL, handler: DisplayClickHandler): HandlerRegistration

View File

@ -7,4 +7,12 @@ import org.bukkit.entity.Player
*/ */
fun interface AudiencePredicate { fun interface AudiencePredicate {
fun test(player: Player): Boolean fun test(player: Player): Boolean
/**
* 表示対象判定が動的時間経過や移動で変化するかどうかを返します
* trueの場合定期的な再評価の対象となります
*/
fun isDynamic(): Boolean {
return false
}
} }

View File

@ -28,12 +28,15 @@ object AudiencePredicates {
} }
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" }
return AudiencePredicate { player ->
player.world.name == worldName && player.location.distanceSquared(location) <= radiusSq return object : AudiencePredicate {
override fun test(player: Player): Boolean {
return player.world.name == worldName && player.location.distanceSquared(location) <= radiusSq
}
override fun isDynamic(): Boolean = true
} }
} }
} }

View File

@ -6,7 +6,6 @@ import java.util.UUID
import net.hareworks.ghostdisplays.display.DisplayManager import net.hareworks.ghostdisplays.display.DisplayManager
import net.hareworks.ghostdisplays.display.DisplayManager.DisplayOperationException import net.hareworks.ghostdisplays.display.DisplayManager.DisplayOperationException
import net.hareworks.ghostdisplays.display.DisplayKind import net.hareworks.ghostdisplays.display.DisplayKind
import net.hareworks.ghostdisplays.display.EditSessionManager
import net.hareworks.kommand_lib.KommandLib import net.hareworks.kommand_lib.KommandLib
import net.hareworks.kommand_lib.kommand import net.hareworks.kommand_lib.kommand
import net.hareworks.permits_lib.bukkit.MutationSession import net.hareworks.permits_lib.bukkit.MutationSession
@ -24,7 +23,6 @@ object CommandRegistrar {
fun register( fun register(
plugin: JavaPlugin, plugin: JavaPlugin,
manager: DisplayManager, manager: DisplayManager,
editSessions: EditSessionManager,
permissionSession: MutationSession permissionSession: MutationSession
): KommandLib = ): KommandLib =
kommand(plugin) { kommand(plugin) {
@ -54,8 +52,7 @@ object CommandRegistrar {
val location = player.anchorLocation() val location = player.anchorLocation()
try { try {
val display = manager.createTextDisplay(id, location, "", player) val display = manager.createTextDisplay(id, location, "", player)
editSessions.begin(player, display.id) sender.success("Text display '${display.id}' created at ${describe(location)}. Use /ghostdisplay text set ${display.id} <content> to set text.")
sender.success("Text display '${display.id}' created at ${describe(location)}. Type text in chat (or 'cancel') to set content.")
} catch (ex: DisplayOperationException) { } catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to create text display.") sender.failure(ex.message ?: "Failed to create text display.")
} }
@ -222,27 +219,6 @@ object CommandRegistrar {
} }
literal("text") { 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<String>("id")
val display = manager.findDisplay(id)
if (display == null || display.kind != DisplayKind.TEXT) {
sender.failure("Text display '$id' not found.")
return@executes
}
editSessions.begin(player, display.id)
sender.info("Enter new MiniMessage text in chat for '${display.id}'. Type 'cancel' to abort.")
}
}
}
literal("set") { literal("set") {
string("id") { string("id") {
suggests { prefix -> suggests { prefix ->
@ -258,7 +234,7 @@ object CommandRegistrar {
val raw = token.replace('_', ' ').replace("\\n", "\n") val raw = token.replace('_', ' ').replace("\\n", "\n")
try { try {
manager.updateText(id, raw) manager.updateText(id, raw)
sender.success("Updated text for '$id'. Use /ghostdisplay text edit $id for multi-line content.") sender.success("Updated text for '$id'.")
} catch (ex: DisplayOperationException) { } catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to update text.") sender.failure(ex.message ?: "Failed to update text.")
} }
@ -266,16 +242,6 @@ object CommandRegistrar {
} }
} }
} }
literal("cancel") {
executes {
val player = sender.requirePlayer() ?: return@executes
if (editSessions.cancel(player)) {
sender.success("Editing session cancelled.")
} else {
sender.failure("You are not editing any display.")
}
}
}
} }
literal("block") { literal("block") {
@ -531,7 +497,6 @@ object CommandRegistrar {
private fun CommandSender.showUsage() { private fun CommandSender.showUsage() {
info("GhostDisplays commands:") info("GhostDisplays commands:")
info(" /ghostdisplay create <text|block|item> ...") info(" /ghostdisplay create <text|block|item> ...")
info(" /ghostdisplay text edit <id> - edit text via chat")
info(" /ghostdisplay viewer <add|remove|clear> ...") info(" /ghostdisplay viewer <add|remove|clear> ...")
info(" /ghostdisplay audience <permission|near|clear> ...") info(" /ghostdisplay audience <permission|near|clear> ...")
info(" /ghostdisplay list | info <id> | delete <id>") info(" /ghostdisplay list | info <id> | delete <id>")

View File

@ -1,57 +0,0 @@
package net.hareworks.ghostdisplays.display
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import net.hareworks.ghostdisplays.display.DisplayManager.DisplayOperationException
import org.bukkit.Bukkit
import org.bukkit.entity.Player
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.player.PlayerQuitEvent
import org.bukkit.plugin.java.JavaPlugin
class EditSessionManager(
private val plugin: JavaPlugin,
private val manager: DisplayManager
) : Listener {
private val sessions = ConcurrentHashMap<UUID, String>()
fun begin(player: Player, displayId: String) {
sessions[player.uniqueId] = displayId
}
fun cancel(player: Player): Boolean = sessions.remove(player.uniqueId) != null
fun shutdown() {
sessions.clear()
}
@EventHandler(ignoreCancelled = true)
fun onPlayerChat(event: io.papermc.paper.event.player.AsyncChatEvent) {
val displayId = sessions[event.player.uniqueId] ?: return
event.isCancelled = true
val serializer = net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText()
val message = serializer.serialize(event.message())
if (message.equals("cancel", ignoreCase = true)) {
sessions.remove(event.player.uniqueId)
event.player.sendMessage("GhostDisplays: editing for '$displayId' cancelled.")
return
}
Bukkit.getScheduler().runTask(plugin, Runnable {
try {
manager.updateText(displayId, message)
event.player.sendMessage("GhostDisplays: updated text for '$displayId'.")
} catch (ex: DisplayOperationException) {
event.player.sendMessage("GhostDisplays: ${ex.message}")
} finally {
sessions.remove(event.player.uniqueId)
}
})
}
@EventHandler
fun onPlayerQuit(event: PlayerQuitEvent) {
sessions.remove(event.player.uniqueId)
}
}

View File

@ -32,11 +32,17 @@ internal class BaseDisplayController<T : Display>(
private val viewerCounts = ConcurrentHashMap<UUID, Int>() private val viewerCounts = ConcurrentHashMap<UUID, Int>()
private val handlers = CopyOnWriteArrayList<HandlerEntry>() private val handlers = CopyOnWriteArrayList<HandlerEntry>()
// Audience Management
// Audience Management // Audience Management
private var baseVisibility: Boolean = false private var baseVisibility: Boolean = false
private val audienceRules = CopyOnWriteArrayList<RuleEntry>() private val audienceRules = CopyOnWriteArrayList<RuleEntry>()
private val autoVisiblePlayers = ConcurrentHashMap.newKeySet<UUID>() private val autoVisiblePlayers = ConcurrentHashMap.newKeySet<UUID>()
// 最適化: 定期更新が必要なルールがあるか
private var hasDynamicRules: Boolean = false
override fun needsPeriodicUpdate(): Boolean = hasDynamicRules
override fun show(player: Player) { override fun show(player: Player) {
runSync { runSync {
val uuid = player.uniqueId val uuid = player.uniqueId
@ -110,13 +116,32 @@ internal class BaseDisplayController<T : Display>(
override fun addAudienceRule(predicate: AudiencePredicate, action: AudienceAction): HandlerRegistration { override fun addAudienceRule(predicate: AudiencePredicate, action: AudienceAction): HandlerRegistration {
val entry = RuleEntry(predicate, action) val entry = RuleEntry(predicate, action)
audienceRules.add(entry) audienceRules.add(entry)
updateDynamicStatus()
refreshAudience() refreshAudience()
return HandlerRegistration { return HandlerRegistration {
audienceRules.remove(entry) audienceRules.remove(entry)
updateDynamicStatus()
refreshAudience() refreshAudience()
} }
} }
private fun updateDynamicStatus() {
// 簡易判定: クラス名に "Near" が含まれる、または特定の実装であることを期待する
// ただし現状はラムダなので名前判定は不安定。
// AudiencePredicate 側にマーカーインターフェースをつけるのが正しいが、
// 今回は「ユーザー定義のPredicate」も含めて、動的かどうかわからない。
// -> AudiencePredicates.near() が返すオブジェクトに特徴を持たせる。
hasDynamicRules = audienceRules.any { isDynamicPredicate(it.predicate) }
}
private fun isDynamicPredicate(predicate: AudiencePredicate): Boolean {
// AudiencePredicates.near が返すクラスの実装詳細に依存するか、
// AudiencePredicate にプロパティを追加する。
// ここでは一旦、toString() 等で識別するか?いや、安全ではない。
// API変更: AudiencePredicate に default isDynamic() を追加する。
return predicate.isDynamic()
}
override fun clearAudienceRules() { override fun clearAudienceRules() {
audienceRules.clear() audienceRules.clear()
refreshAudience() refreshAudience()

View File

@ -82,16 +82,18 @@ internal class DisplayRegistry : Listener {
controllers.forEach { it.refreshAudience(player) } controllers.forEach { it.refreshAudience(player) }
} }
fun tick() { fun updateAudiences() {
val players = org.bukkit.Bukkit.getOnlinePlayers() val players = org.bukkit.Bukkit.getOnlinePlayers()
if (players.isEmpty()) return if (players.isEmpty()) return
val controllers = controllersSnapshot() val controllers = controllersSnapshot()
if (controllers.isEmpty()) return if (controllers.isEmpty()) return
// 最適化: 動的な評価が必要なコントローラーだけ抽出できれば良いが、 // 動的な評価が必要なコントローラーだけを抽出して評価する
// 現状はシンプルに全走査する。 val activeControllers = controllers.filter { it.needsPeriodicUpdate() }
if (activeControllers.isEmpty()) return
players.forEach { player -> players.forEach { player ->
controllers.forEach { controller -> activeControllers.forEach { controller ->
controller.refreshAudience(player) controller.refreshAudience(player)
} }
} }