241 lines
9.3 KiB
Kotlin
241 lines
9.3 KiB
Kotlin
package net.hareworks.ghostdisplays.display
|
|
|
|
import java.time.Instant
|
|
import java.util.UUID
|
|
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.AudiencePredicates
|
|
import net.hareworks.ghostdisplays.display.DisplayManager.DisplayOperationException
|
|
import net.kyori.adventure.text.Component
|
|
import net.kyori.adventure.text.minimessage.MiniMessage
|
|
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer
|
|
import org.bukkit.Bukkit
|
|
import org.bukkit.Location
|
|
import org.bukkit.block.data.BlockData
|
|
import org.bukkit.entity.BlockDisplay
|
|
import org.bukkit.entity.Display
|
|
import org.bukkit.entity.ItemDisplay
|
|
import org.bukkit.entity.Player
|
|
import org.bukkit.entity.TextDisplay
|
|
import org.bukkit.inventory.ItemStack
|
|
import org.bukkit.plugin.java.JavaPlugin
|
|
|
|
class DisplayManager(
|
|
private val plugin: JavaPlugin,
|
|
private val service: DisplayService,
|
|
private val miniMessage: MiniMessage
|
|
) {
|
|
private val displays = ConcurrentHashMap<String, ManagedDisplay<*>>()
|
|
private val idPattern = Regex("^[a-z0-9_\\-]{1,32}$")
|
|
private val logger: Logger = plugin.logger
|
|
private val plainSerializer = PlainTextComponentSerializer.plainText()
|
|
|
|
fun createTextDisplay(id: String, location: Location, initialContent: String, creator: Player?): ManagedDisplay.Text {
|
|
val normalized = normalizeId(id)
|
|
ensureIdAvailable(normalized)
|
|
val safeLocation = location.clone()
|
|
val controller = service.createTextDisplay(safeLocation, INTERACTION_DEFAULT)
|
|
val component = parseMiniMessage(initialContent.ifBlank { "<gray>${normalized}" })
|
|
controller.applyEntityUpdate { textDisplay ->
|
|
textDisplay.text(component)
|
|
}
|
|
val managed = ManagedDisplay.Text(
|
|
id = normalized,
|
|
controller = controller,
|
|
location = safeLocation,
|
|
createdAt = Instant.now(),
|
|
createdBy = creator?.uniqueId,
|
|
rawContent = initialContent,
|
|
component = component
|
|
)
|
|
displays[normalized] = managed
|
|
creator?.let { controller.show(it) }
|
|
return managed
|
|
}
|
|
|
|
fun createBlockDisplay(id: String, location: Location, blockData: BlockData, creator: Player?): ManagedDisplay.Block {
|
|
val normalized = normalizeId(id)
|
|
ensureIdAvailable(normalized)
|
|
val safeLocation = location.clone()
|
|
val controller = service.createBlockDisplay(safeLocation, INTERACTION_DEFAULT)
|
|
controller.applyEntityUpdate { it.block = blockData.clone() }
|
|
val managed = ManagedDisplay.Block(
|
|
id = normalized,
|
|
controller = controller,
|
|
location = safeLocation,
|
|
createdAt = Instant.now(),
|
|
createdBy = creator?.uniqueId,
|
|
blockData = blockData.clone()
|
|
)
|
|
displays[normalized] = managed
|
|
creator?.let { controller.show(it) }
|
|
return managed
|
|
}
|
|
|
|
fun createItemDisplay(id: String, location: Location, itemStack: ItemStack, creator: Player?): ManagedDisplay.Item {
|
|
val normalized = normalizeId(id)
|
|
ensureIdAvailable(normalized)
|
|
val safeLocation = location.clone()
|
|
val controller = service.createItemDisplay(safeLocation, itemStack.clone(), INTERACTION_DEFAULT)
|
|
controller.applyEntityUpdate { it.itemStack = itemStack.clone() }
|
|
val managed = ManagedDisplay.Item(
|
|
id = normalized,
|
|
controller = controller,
|
|
location = safeLocation,
|
|
createdAt = Instant.now(),
|
|
createdBy = creator?.uniqueId,
|
|
itemStack = itemStack.clone()
|
|
)
|
|
displays[normalized] = managed
|
|
creator?.let { controller.show(it) }
|
|
return managed
|
|
}
|
|
|
|
fun listDisplays(): List<ManagedDisplay<*>> = displays.values.sortedBy { it.id }
|
|
|
|
fun findDisplay(id: String): ManagedDisplay<*>? = displays[normalizeId(id)]
|
|
|
|
fun updateText(id: String, content: String): Component {
|
|
val display = requireText(id)
|
|
val component = parseMiniMessage(content)
|
|
display.controller.applyEntityUpdate { it.text(component) }
|
|
display.rawContent = content
|
|
display.component = component
|
|
return component
|
|
}
|
|
|
|
fun updateBlock(id: String, blockData: BlockData) {
|
|
val display = requireBlock(id)
|
|
display.controller.applyEntityUpdate { it.block = blockData.clone() }
|
|
display.blockData = blockData.clone()
|
|
}
|
|
|
|
fun updateItem(id: String, itemStack: ItemStack) {
|
|
val display = requireItem(id)
|
|
val clone = itemStack.clone()
|
|
display.controller.applyEntityUpdate { it.itemStack = clone }
|
|
display.itemStack = clone
|
|
}
|
|
|
|
fun delete(id: String) {
|
|
val normalized = normalizeId(id)
|
|
val display = displays.remove(normalized) ?: throw DisplayOperationException("Display '$id' does not exist.")
|
|
display.clearAudiences()
|
|
display.controller.destroy()
|
|
}
|
|
|
|
fun showToPlayers(id: String, players: Collection<Player>) {
|
|
val display = requireDisplay(id)
|
|
players.forEach { display.controller.show(it) }
|
|
}
|
|
|
|
fun hideFromPlayers(id: String, players: Collection<Player>) {
|
|
val display = requireDisplay(id)
|
|
players.forEach { display.controller.hide(it) }
|
|
}
|
|
|
|
fun clearViewers(id: String) {
|
|
val display = requireDisplay(id)
|
|
val viewers = display.controller.viewerIds().mapNotNull { Bukkit.getPlayer(it) }
|
|
viewers.forEach { display.controller.hide(it) }
|
|
}
|
|
|
|
fun addPermissionAudience(id: String, permission: String) {
|
|
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))
|
|
display.registerAudienceBinding(
|
|
AudienceBinding(
|
|
key = key,
|
|
description = "permission:$permission",
|
|
registration = registration
|
|
)
|
|
)
|
|
}
|
|
|
|
fun removePermissionAudience(id: String, permission: String): Boolean {
|
|
val display = requireDisplay(id)
|
|
val key = "perm:${permission.lowercase()}"
|
|
return display.removeAudienceBinding(key)
|
|
}
|
|
|
|
fun setNearAudience(id: String, radius: Double) {
|
|
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))
|
|
display.registerAudienceBinding(
|
|
AudienceBinding(
|
|
key = key,
|
|
description = "radius:${String.format("%.2f", radius)}",
|
|
registration = registration
|
|
)
|
|
)
|
|
}
|
|
|
|
fun clearAudiences(id: String) {
|
|
val display = requireDisplay(id)
|
|
display.clearAudiences()
|
|
}
|
|
|
|
fun destroyAll() {
|
|
displays.values.forEach {
|
|
runCatching { it.clearAudiences() }
|
|
runCatching { it.controller.destroy() }
|
|
}
|
|
displays.clear()
|
|
}
|
|
|
|
fun describeContent(display: ManagedDisplay<*>): String =
|
|
when (display) {
|
|
is ManagedDisplay.Text -> plainSerializer.serialize(display.component)
|
|
is ManagedDisplay.Block -> display.blockData.asString
|
|
is ManagedDisplay.Item -> display.itemStack.type.name.lowercase()
|
|
}
|
|
|
|
private fun requireDisplay(id: String): ManagedDisplay<*> =
|
|
displays[normalizeId(id)] ?: throw DisplayOperationException("Display '$id' does not exist.")
|
|
|
|
private fun requireText(id: String): ManagedDisplay.Text =
|
|
findDisplay(id) as? ManagedDisplay.Text
|
|
?: throw DisplayOperationException("Display '$id' is not a text display.")
|
|
|
|
private fun requireBlock(id: String): ManagedDisplay.Block =
|
|
findDisplay(id) as? ManagedDisplay.Block
|
|
?: throw DisplayOperationException("Display '$id' is not a block display.")
|
|
|
|
private fun requireItem(id: String): ManagedDisplay.Item =
|
|
findDisplay(id) as? ManagedDisplay.Item
|
|
?: throw DisplayOperationException("Display '$id' is not an item display.")
|
|
|
|
private fun normalizeId(id: String): String =
|
|
id.trim().lowercase()
|
|
|
|
private fun ensureIdAvailable(id: String) {
|
|
if (!idPattern.matches(id)) {
|
|
throw DisplayOperationException("ID must match ${idPattern.pattern} and be 1-32 characters.")
|
|
}
|
|
if (displays.containsKey(id)) {
|
|
throw DisplayOperationException("Display '$id' already exists.")
|
|
}
|
|
}
|
|
|
|
private fun parseMiniMessage(raw: String): Component =
|
|
runCatching { miniMessage.deserialize(if (raw.isBlank()) "<gray>empty" else raw) }
|
|
.getOrElse { ex ->
|
|
throw DisplayOperationException("MiniMessage parse error: ${ex.message ?: ex.javaClass.simpleName}")
|
|
}
|
|
|
|
companion object {
|
|
private val INTERACTION_DEFAULT = InteractionOptions.enabled(width = 0.8, height = 0.8)
|
|
}
|
|
|
|
class DisplayOperationException(message: String) : RuntimeException(message)
|
|
}
|