GhostDisplays/main/kotlin/net/hareworks/ghostdisplays/command/CommandRegistrar.kt
2025-12-06 04:40:18 +09:00

418 lines
20 KiB
Kotlin

package net.hareworks.ghostdisplays.command
import java.time.format.DateTimeFormatter
import java.util.Locale
import java.util.UUID
import net.hareworks.ghostdisplays.display.DisplayManager
import net.hareworks.ghostdisplays.display.DisplayManager.DisplayOperationException
import net.hareworks.ghostdisplays.display.DisplayKind
import net.hareworks.ghostdisplays.display.EditSessionManager
import net.hareworks.kommand_lib.KommandLib
import net.hareworks.kommand_lib.kommand
import net.hareworks.permits_lib.bukkit.MutationSession
import org.bukkit.Bukkit
import org.bukkit.Location
import org.bukkit.Material
import org.bukkit.block.data.BlockData
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
import org.bukkit.permissions.PermissionDefault
import org.bukkit.plugin.java.JavaPlugin
object CommandRegistrar {
fun register(
plugin: JavaPlugin,
manager: DisplayManager,
editSessions: EditSessionManager,
permissionSession: MutationSession
): KommandLib =
kommand(plugin) {
permissions {
namespace = "ghostdisplays"
defaultValue = PermissionDefault.OP
wildcard = true
session(permissionSession)
}
command("ghostdisplay", listOf("gdisplay", "gdisp")) {
description = "Manage GhostDisplays entities"
permission = "ghostdisplays.command"
executes { sender.showUsage() }
literal("help") {
executes { sender.showUsage() }
}
literal("create") {
literal("text") {
string("id") {
executes {
val player = sender.requirePlayer() ?: return@executes
val id = argument<String>("id")
val location = player.anchorLocation()
try {
val display = manager.createTextDisplay(id, location, "", player)
editSessions.begin(player, display.id)
sender.success("Text display '${display.id}' created at ${describe(location)}. Type text in chat (or 'cancel') to set content.")
} catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to create text display.")
}
}
}
}
literal("block") {
string("id") {
string("state") {
executes {
val player = sender.requirePlayer() ?: return@executes
val id = argument<String>("id")
val state = argument<String>("state")
try {
val blockData = parseBlockData(state)
val display = manager.createBlockDisplay(id, player.anchorLocation(), blockData, player)
sender.success("Block display '${display.id}' created with $state at ${describe(display.location)}.")
} catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to create block display.")
} catch (ex: IllegalArgumentException) {
sender.failure("Invalid block data '$state': ${ex.message}")
}
}
}
}
}
literal("item") {
string("id") {
string("material") {
executes {
val player = sender.requirePlayer() ?: return@executes
val id = argument<String>("id")
val materialToken = argument<String>("material")
val material = Material.matchMaterial(materialToken.uppercase(Locale.ROOT))
?: return@executes sender.failure("Unknown material '$materialToken'.")
if (!material.isItem) {
return@executes sender.failure("$materialToken cannot be used as an item display.")
}
try {
val item = ItemStack(material)
val display = manager.createItemDisplay(id, player.anchorLocation(), item, player)
sender.success("Item display '${display.id}' created with ${material.name.lowercase()} at ${describe(display.location)}.")
} catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to create item display.")
}
}
}
}
}
}
literal("delete") {
string("id") {
executes {
val id = argument<String>("id")
try {
manager.delete(id)
sender.success("Display '$id' removed.")
} catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to delete '$id'.")
}
}
}
}
literal("list") {
executes {
val entries = manager.listDisplays()
if (entries.isEmpty()) {
sender.info("No displays registered.")
return@executes
}
sender.info("Displays (${entries.size}):")
entries.forEach {
sender.info(" - ${it.id} [${it.kind}] @ ${describe(it.location)} viewers=${it.controller.viewerIds().size}")
}
}
}
literal("info") {
string("id") {
executes {
val id = argument<String>("id")
val display = manager.findDisplay(id)
if (display == null) {
sender.failure("Display '$id' not found.")
return@executes
}
sender.info("Display '${display.id}' (${display.kind})")
sender.info(" Location: ${describe(display.location)}")
sender.info(" Created: ${DATE_FORMAT.format(display.createdAt)} by ${display.createdBy?.let { Bukkit.getOfflinePlayer(it).name ?: it } ?: "unknown"}")
val viewers = display.controller.viewerIds()
sender.info(" Viewers (${viewers.size}): ${resolveNames(viewers).ifEmpty { "none" }}")
val audiences = display.audienceBindings()
sender.info(" Audiences (${audiences.size}): ${if (audiences.isEmpty()) "none" else audiences.joinToString { it.description }}")
sender.info(" Content: ${manager.describeContent(display)}")
}
}
}
literal("viewer") {
literal("add") {
string("id") {
players("targets") {
executes {
val id = argument<String>("id")
val targets = argument<List<Player>>("targets")
try {
manager.showToPlayers(id, targets)
sender.success("Showing '$id' to ${targets.size} player(s).")
} catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to update viewers.")
}
}
}
}
}
literal("remove") {
string("id") {
players("targets") {
executes {
val id = argument<String>("id")
val targets = argument<List<Player>>("targets")
try {
manager.hideFromPlayers(id, targets)
sender.success("Hid '$id' from ${targets.size} player(s).")
} catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to update viewers.")
}
}
}
}
}
literal("clear") {
string("id") {
executes {
val id = argument<String>("id")
try {
manager.clearViewers(id)
sender.success("Cleared active viewers for '$id'.")
} catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to clear viewers.")
}
}
}
}
}
literal("text") {
literal("edit") {
string("id") {
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") {
string("id") {
string("content") {
executes {
val id = argument<String>("id")
val token = argument<String>("content")
val raw = token.replace('_', ' ').replace("\\n", "\n")
try {
manager.updateText(id, raw)
sender.success("Updated text for '$id'. Use /ghostdisplay text edit $id for multi-line content.")
} catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to update text.")
}
}
}
}
}
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("set") {
string("id") {
string("state") {
executes {
val id = argument<String>("id")
val state = argument<String>("state")
try {
val blockData = parseBlockData(state)
manager.updateBlock(id, blockData)
sender.success("Block data for '$id' updated to $state.")
} catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to update block display.")
} catch (ex: IllegalArgumentException) {
sender.failure("Invalid block data '$state': ${ex.message}")
}
}
}
}
}
}
literal("item") {
literal("set") {
string("id") {
string("material") {
executes {
val id = argument<String>("id")
val materialToken = argument<String>("material")
val material = Material.matchMaterial(materialToken.uppercase(Locale.ROOT))
?: return@executes sender.failure("Unknown material '$materialToken'.")
if (!material.isItem) {
return@executes sender.failure("$materialToken cannot be used as an item display.")
}
try {
manager.updateItem(id, ItemStack(material))
sender.success("Item for '$id' set to ${material.name.lowercase()}.")
} catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to update item display.")
}
}
}
}
}
}
literal("audience") {
literal("permission") {
literal("add") {
string("id") {
string("permission") {
executes {
val id = argument<String>("id")
val perm = argument<String>("permission")
try {
manager.addPermissionAudience(id, perm)
sender.success("Permission audience '$perm' added to '$id'.")
} catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to add permission audience.")
}
}
}
}
}
literal("remove") {
string("id") {
string("permission") {
executes {
val id = argument<String>("id")
val perm = argument<String>("permission")
val removed = runCatching { manager.removePermissionAudience(id, perm) }
.onFailure { sender.failure(it.message ?: "Failed to remove permission audience.") }
.getOrNull()
if (removed == true) {
sender.success("Permission audience '$perm' removed from '$id'.")
} else if (removed == false) {
sender.failure("Permission audience '$perm' not found for '$id'.")
}
}
}
}
}
}
literal("near") {
literal("set") {
string("id") {
float("radius", min = 1.0) {
executes {
val id = argument<String>("id")
val radius = argument<Double>("radius")
try {
manager.setNearAudience(id, radius)
sender.success("Radius audience for '$id' set to ${String.format("%.1f", radius)} block(s).")
} catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to update radius audience.")
}
}
}
}
}
}
literal("clear") {
string("id") {
executes {
val id = argument<String>("id")
try {
manager.clearAudiences(id)
sender.success("Audiences cleared for '$id'.")
} catch (ex: DisplayOperationException) {
sender.failure(ex.message ?: "Failed to clear audiences.")
}
}
}
}
}
}
}
}
private fun CommandSender.showUsage() {
info("GhostDisplays commands:")
info(" /ghostdisplay create <text|block|item> ...")
info(" /ghostdisplay text edit <id> - edit text via chat")
info(" /ghostdisplay viewer <add|remove|clear> ...")
info(" /ghostdisplay audience <permission|near|clear> ...")
info(" /ghostdisplay list | info <id> | delete <id>")
}
private fun Player.anchorLocation(): Location =
eyeLocation.clone().add(direction.normalize().multiply(1.5))
private fun parseBlockData(state: String): BlockData =
Bukkit.createBlockData(state)
private fun describe(location: Location): String =
"${location.world?.name ?: "unknown"} @ ${location.blockX},${location.blockY},${location.blockZ}"
private fun CommandSender.requirePlayer(): Player? {
val player = this as? Player
if (player == null) {
failure("This command can only be used by players.")
}
return player
}
private fun CommandSender.info(message: String) {
sendMessage("§7$message")
}
private fun CommandSender.success(message: String) {
sendMessage("§a$message")
}
private fun CommandSender.failure(message: String) {
sendMessage("§c$message")
}
private fun resolveNames(ids: Collection<UUID>): String {
if (ids.isEmpty()) return ""
return ids.joinToString { Bukkit.getOfflinePlayer(it).name ?: it.toString() }
}
private val DATE_FORMAT: DateTimeFormatter =
DateTimeFormatter.ISO_INSTANT