init
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
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.AsyncPlayerChatEvent
|
||||
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: AsyncPlayerChatEvent) {
|
||||
val displayId = sessions[event.player.uniqueId] ?: return
|
||||
event.isCancelled = true
|
||||
val message = 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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package net.hareworks.ghostdisplays.display
|
||||
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import net.hareworks.ghostdisplays.api.DisplayController
|
||||
import net.hareworks.ghostdisplays.api.click.HandlerRegistration
|
||||
import net.kyori.adventure.text.Component
|
||||
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.TextDisplay
|
||||
import org.bukkit.inventory.ItemStack
|
||||
|
||||
enum class DisplayKind {
|
||||
TEXT,
|
||||
BLOCK,
|
||||
ITEM
|
||||
}
|
||||
|
||||
data class AudienceBinding(
|
||||
val key: String,
|
||||
val description: String,
|
||||
private val registration: HandlerRegistration
|
||||
) {
|
||||
fun unregister() {
|
||||
registration.unregister()
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ManagedDisplay<T : Display>(
|
||||
val id: String,
|
||||
val kind: DisplayKind,
|
||||
val controller: DisplayController<T>,
|
||||
location: Location,
|
||||
val createdAt: Instant,
|
||||
val createdBy: UUID?
|
||||
) {
|
||||
val location: Location = location.clone()
|
||||
private val audienceBindings = ConcurrentHashMap<String, AudienceBinding>()
|
||||
|
||||
fun audienceBindings(): Collection<AudienceBinding> = audienceBindings.values
|
||||
|
||||
fun registerAudienceBinding(binding: AudienceBinding) {
|
||||
audienceBindings.put(binding.key, binding)?.unregister()
|
||||
}
|
||||
|
||||
fun removeAudienceBinding(key: String): Boolean {
|
||||
return audienceBindings.remove(key)?.let {
|
||||
it.unregister()
|
||||
true
|
||||
} ?: false
|
||||
}
|
||||
|
||||
fun clearAudiences() {
|
||||
audienceBindings.values.forEach { it.unregister() }
|
||||
audienceBindings.clear()
|
||||
}
|
||||
|
||||
class Text(
|
||||
id: String,
|
||||
controller: DisplayController<TextDisplay>,
|
||||
location: Location,
|
||||
createdAt: Instant,
|
||||
createdBy: UUID?,
|
||||
var rawContent: String,
|
||||
var component: Component
|
||||
) : ManagedDisplay<TextDisplay>(id, DisplayKind.TEXT, controller, location, createdAt, createdBy)
|
||||
|
||||
class Block(
|
||||
id: String,
|
||||
controller: DisplayController<BlockDisplay>,
|
||||
location: Location,
|
||||
createdAt: Instant,
|
||||
createdBy: UUID?,
|
||||
var blockData: BlockData
|
||||
) : ManagedDisplay<BlockDisplay>(id, DisplayKind.BLOCK, controller, location, createdAt, createdBy)
|
||||
|
||||
class Item(
|
||||
id: String,
|
||||
controller: DisplayController<ItemDisplay>,
|
||||
location: Location,
|
||||
createdAt: Instant,
|
||||
createdBy: UUID?,
|
||||
var itemStack: ItemStack
|
||||
) : ManagedDisplay<ItemDisplay>(id, DisplayKind.ITEM, controller, location, createdAt, createdBy)
|
||||
}
|
||||
Reference in New Issue
Block a user