feat: 会話進行とauto-turn
This commit is contained in:
parent
9933aa7820
commit
0506ec8de3
|
|
@ -6,16 +6,19 @@ import net.hareworks.npc_mannequin.commands.MannequinCommands
|
||||||
import net.hareworks.npc_mannequin.service.MannequinController
|
import net.hareworks.npc_mannequin.service.MannequinController
|
||||||
import net.hareworks.npc_mannequin.service.MannequinListener
|
import net.hareworks.npc_mannequin.service.MannequinListener
|
||||||
import net.hareworks.npc_mannequin.service.MannequinRegistry
|
import net.hareworks.npc_mannequin.service.MannequinRegistry
|
||||||
|
import net.hareworks.npc_mannequin.service.MannequinTickTask
|
||||||
import net.hareworks.npc_mannequin.storage.MannequinStorage
|
import net.hareworks.npc_mannequin.storage.MannequinStorage
|
||||||
import net.hareworks.permits_lib.PermitsLib
|
import net.hareworks.permits_lib.PermitsLib
|
||||||
import net.hareworks.permits_lib.bukkit.MutationSession
|
import net.hareworks.permits_lib.bukkit.MutationSession
|
||||||
import org.bukkit.plugin.ServicePriority
|
import org.bukkit.plugin.ServicePriority
|
||||||
import org.bukkit.plugin.java.JavaPlugin
|
import org.bukkit.plugin.java.JavaPlugin
|
||||||
|
import org.bukkit.scheduler.BukkitRunnable
|
||||||
|
|
||||||
class Plugin : JavaPlugin() {
|
class Plugin : JavaPlugin() {
|
||||||
private var kommand: KommandLib? = null
|
private var kommand: KommandLib? = null
|
||||||
private lateinit var registry: MannequinRegistry
|
private lateinit var registry: MannequinRegistry
|
||||||
private var permissionSession: MutationSession? = null
|
private var permissionSession: MutationSession? = null
|
||||||
|
private var tickTask: BukkitRunnable? = null
|
||||||
|
|
||||||
override fun onEnable() {
|
override fun onEnable() {
|
||||||
val storage = MannequinStorage(this)
|
val storage = MannequinStorage(this)
|
||||||
|
|
@ -29,10 +32,17 @@ class Plugin : JavaPlugin() {
|
||||||
|
|
||||||
kommand = MannequinCommands.register(this, registry, permissionSession)
|
kommand = MannequinCommands.register(this, registry, permissionSession)
|
||||||
server.servicesManager.register(MannequinRegistry::class.java, registry, this, ServicePriority.Normal)
|
server.servicesManager.register(MannequinRegistry::class.java, registry, this, ServicePriority.Normal)
|
||||||
|
|
||||||
|
// Start tick task for dynamic behavior (e.g. look-at)
|
||||||
|
tickTask = MannequinTickTask(registry)
|
||||||
|
tickTask?.runTaskTimer(this, 20L, 2L)
|
||||||
|
|
||||||
logger.info("Loaded ${registry.all().size} mannequin definitions.")
|
logger.info("Loaded ${registry.all().size} mannequin definitions.")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDisable() {
|
override fun onDisable() {
|
||||||
|
tickTask?.cancel()
|
||||||
|
tickTask = null
|
||||||
server.servicesManager.unregisterAll(this)
|
server.servicesManager.unregisterAll(this)
|
||||||
kommand?.unregister()
|
kommand?.unregister()
|
||||||
permissionSession?.clearAll()
|
permissionSession?.clearAll()
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import net.kyori.adventure.text.Component
|
||||||
import net.kyori.adventure.text.event.ClickEvent
|
import net.kyori.adventure.text.event.ClickEvent
|
||||||
import net.kyori.adventure.text.event.HoverEvent
|
import net.kyori.adventure.text.event.HoverEvent
|
||||||
import net.kyori.adventure.text.format.NamedTextColor
|
import net.kyori.adventure.text.format.NamedTextColor
|
||||||
|
import org.bukkit.Bukkit
|
||||||
import org.bukkit.entity.Entity
|
import org.bukkit.entity.Entity
|
||||||
import org.bukkit.entity.Mannequin
|
import org.bukkit.entity.Mannequin
|
||||||
import org.bukkit.entity.Player
|
import org.bukkit.entity.Player
|
||||||
|
|
@ -88,7 +89,10 @@ object MannequinCommands {
|
||||||
val player = requirePlayer() ?: return@executes
|
val player = requirePlayer() ?: return@executes
|
||||||
val id = argument<String>("id")
|
val id = argument<String>("id")
|
||||||
runCatching {
|
runCatching {
|
||||||
registry.create(id, player.location, MannequinSettings())
|
val settings = MannequinSettings(
|
||||||
|
customName = Component.text(id)
|
||||||
|
)
|
||||||
|
registry.create(id, player.location, settings)
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
success("Spawned mannequin '$id' at ${formatLocation(it.location)}.")
|
success("Spawned mannequin '$id' at ${formatLocation(it.location)}.")
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
|
|
@ -219,8 +223,71 @@ object MannequinCommands {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
literal("invulnerable") {
|
||||||
|
string("state") {
|
||||||
|
suggests { prefix ->
|
||||||
|
listOf(
|
||||||
|
"true",
|
||||||
|
"false",
|
||||||
|
"on",
|
||||||
|
"off"
|
||||||
|
).filter { it.startsWith(prefix.lowercase()) }
|
||||||
|
}
|
||||||
|
executes {
|
||||||
|
val id = argument<String>("id")
|
||||||
|
val input = argument<String>("state")
|
||||||
|
val state = parseBoolean(input)
|
||||||
|
if (state == null) {
|
||||||
|
error("Value must be true/false/on/off.")
|
||||||
|
return@executes
|
||||||
|
}
|
||||||
|
registry.updateSettings(id) { it.copy(invulnerable = state) }
|
||||||
|
success("Set invulnerable for '$id' to $state.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
literal("health") {
|
||||||
|
float("value", min = 0.0, max = 1024.0) {
|
||||||
|
executes {
|
||||||
|
val id = argument<String>("id")
|
||||||
|
val health = argument<Double>("value")
|
||||||
|
registry.updateSettings(id) { it.copy(health = health) }
|
||||||
|
success("Set health for '$id' to $health.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
literal("name") {
|
||||||
|
literal("set") {
|
||||||
|
greedyString("content") {
|
||||||
|
executes {
|
||||||
|
val id = argument<String>("id")
|
||||||
|
val payload = argument<String>("content")
|
||||||
|
val component = runCatching { TextSerializers.miniMessage(payload) }
|
||||||
|
.onFailure { error("MiniMessage parse failed: ${it.message}") }
|
||||||
|
.getOrNull()
|
||||||
|
if (component == null) {
|
||||||
|
return@executes
|
||||||
|
}
|
||||||
|
registry.updateSettings(id) {
|
||||||
|
it.copy(customName = component)
|
||||||
|
}
|
||||||
|
success("Updated custom name for '$id'.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
literal("clear") {
|
||||||
|
executes {
|
||||||
|
val id = argument<String>("id")
|
||||||
|
registry.updateSettings(id) { it.copy(customName = null) }
|
||||||
|
success("Cleared custom name for '$id'.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
literal("description") {
|
literal("description") {
|
||||||
literal("text") {
|
literal("set") {
|
||||||
greedyString("content") {
|
greedyString("content") {
|
||||||
executes {
|
executes {
|
||||||
val id = argument<String>("id")
|
val id = argument<String>("id")
|
||||||
|
|
@ -249,26 +316,17 @@ object MannequinCommands {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
literal("hide") {
|
literal("hide") {
|
||||||
string("state") {
|
|
||||||
suggests { prefix ->
|
|
||||||
listOf(
|
|
||||||
"true",
|
|
||||||
"false",
|
|
||||||
"on",
|
|
||||||
"off"
|
|
||||||
).filter { it.startsWith(prefix.lowercase()) }
|
|
||||||
}
|
|
||||||
executes {
|
executes {
|
||||||
val id = argument<String>("id")
|
val id = argument<String>("id")
|
||||||
val input = argument<String>("state")
|
registry.updateSettings(id) { it.copy(hideDescription = true) }
|
||||||
val state = parseBoolean(input)
|
success("Description hidden for '$id'.")
|
||||||
if (state == null) {
|
|
||||||
error("Value must be true/false/on/off.")
|
|
||||||
return@executes
|
|
||||||
}
|
}
|
||||||
registry.updateSettings(id) { it.copy(hideDescription = state) }
|
|
||||||
success(if (state) "Description hidden for '$id'." else "Description visible for '$id'.")
|
|
||||||
}
|
}
|
||||||
|
literal("show") {
|
||||||
|
executes {
|
||||||
|
val id = argument<String>("id")
|
||||||
|
registry.updateSettings(id) { it.copy(hideDescription = false) }
|
||||||
|
success("Description visible for '$id'.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -317,13 +375,19 @@ object MannequinCommands {
|
||||||
|
|
||||||
literal("profile") {
|
literal("profile") {
|
||||||
literal("player") {
|
literal("player") {
|
||||||
player("source") {
|
string("source") {
|
||||||
executes {
|
executes {
|
||||||
val id = argument<String>("id")
|
val id = argument<String>("id")
|
||||||
val source = argument<Player>("source")
|
val name = argument<String>("source")
|
||||||
val stored = StoredProfile.from(source.playerProfile)
|
val player = Bukkit.getPlayer(name)
|
||||||
|
val profile = if (player != null) {
|
||||||
|
player.playerProfile
|
||||||
|
} else {
|
||||||
|
Bukkit.createProfile(name)
|
||||||
|
}
|
||||||
|
val stored = StoredProfile.from(profile)
|
||||||
registry.updateSettings(id) { it.copy(profile = stored) }
|
registry.updateSettings(id) { it.copy(profile = stored) }
|
||||||
success("Copied profile from ${source.name} into '$id'.")
|
success("Copied profile from $name into '$id'.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -351,43 +415,296 @@ object MannequinCommands {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
literal("event") {
|
||||||
|
literal("mode") {
|
||||||
|
literal("default") {
|
||||||
|
executes {
|
||||||
|
val id = argument<String>("id")
|
||||||
|
registry.updateSettings(id) { it.copy(eventMode = net.hareworks.npc_mannequin.mannequin.EventMode.SEQUENTIAL) }
|
||||||
|
success("Set event mode for '$id' to default (sequential).")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
literal("random") {
|
||||||
|
executes {
|
||||||
|
val id = argument<String>("id")
|
||||||
|
registry.updateSettings(id) { it.copy(eventMode = net.hareworks.npc_mannequin.mannequin.EventMode.RANDOM) }
|
||||||
|
success("Set event mode for '$id' to random.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
literal("timeout") {
|
||||||
|
float("seconds") {
|
||||||
|
executes {
|
||||||
|
val id = argument<String>("id")
|
||||||
|
val seconds = argument<Double>("seconds")
|
||||||
|
registry.updateSettings(id) { it.copy(eventTimeout = seconds) }
|
||||||
|
success("Set event timeout for '$id' to $seconds seconds.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
literal("add") {
|
||||||
literal("command") {
|
literal("command") {
|
||||||
literal("console") {
|
greedyString("cmd") {
|
||||||
literal("set") {
|
|
||||||
greedyString("command") {
|
|
||||||
executes {
|
executes {
|
||||||
val id = argument<String>("id")
|
val id = argument<String>("id")
|
||||||
val cmd = argument<String>("command")
|
val cmd = argument<String>("cmd")
|
||||||
registry.updateSettings(id) { it.copy(serverCommand = cmd) }
|
registry.updateSettings(id) {
|
||||||
success("Set console command for '$id'.")
|
it.copy(eventActions = it.eventActions + net.hareworks.npc_mannequin.mannequin.MannequinAction.Command(cmd))
|
||||||
|
}
|
||||||
|
success("Added command action to '$id'.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
literal("message") {
|
||||||
|
greedyString("msg") {
|
||||||
|
executes {
|
||||||
|
val id = argument<String>("id")
|
||||||
|
val msg = argument<String>("msg")
|
||||||
|
registry.updateSettings(id) {
|
||||||
|
it.copy(eventActions = it.eventActions + net.hareworks.npc_mannequin.mannequin.MannequinAction.Message(msg))
|
||||||
|
}
|
||||||
|
success("Added message action to '$id'.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
literal("remove") {
|
||||||
|
integer("index") {
|
||||||
|
executes {
|
||||||
|
val id = argument<String>("id")
|
||||||
|
val index = argument<Int>("index")
|
||||||
|
registry.updateSettings(id) {
|
||||||
|
if (index < 0 || index >= it.eventActions.size) error("Index out of bounds.")
|
||||||
|
val mutable = it.eventActions.toMutableList()
|
||||||
|
mutable.removeAt(index)
|
||||||
|
it.copy(eventActions = mutable)
|
||||||
|
}
|
||||||
|
success("Removed event action at index $index from '$id'.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
literal("swap") {
|
||||||
|
integer("index1") {
|
||||||
|
integer("index2") {
|
||||||
|
executes {
|
||||||
|
val id = argument<String>("id")
|
||||||
|
val i1 = argument<Int>("index1")
|
||||||
|
val i2 = argument<Int>("index2")
|
||||||
|
registry.updateSettings(id) {
|
||||||
|
if (i1 < 0 || i1 >= it.eventActions.size || i2 < 0 || i2 >= it.eventActions.size) error("Index out of bounds.")
|
||||||
|
val mutable = it.eventActions.toMutableList()
|
||||||
|
java.util.Collections.swap(mutable, i1, i2)
|
||||||
|
it.copy(eventActions = mutable)
|
||||||
|
}
|
||||||
|
success("Swapped event actions at $i1 and $i2 for '$id'.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
literal("list") {
|
||||||
|
executes {
|
||||||
|
val id = argument<String>("id")
|
||||||
|
val record = registry.require(id)
|
||||||
|
val actions = record.settings.eventActions
|
||||||
|
val mode = record.settings.eventMode
|
||||||
|
|
||||||
|
val player = sender as? Player
|
||||||
|
if (player == null) {
|
||||||
|
sender.sendMessage("Mannequin '$id' Event Configuration (Mode: $mode):")
|
||||||
|
actions.forEachIndexed { index, action ->
|
||||||
|
when (action) {
|
||||||
|
is net.hareworks.npc_mannequin.mannequin.MannequinAction.Command -> sender.sendMessage(" [$index] COMMAND: ${action.command}")
|
||||||
|
is net.hareworks.npc_mannequin.mannequin.MannequinAction.Message -> sender.sendMessage(" [$index] MESSAGE: ${action.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
player.sendMessage(net.kyori.adventure.text.Component.text("Mannequin '$id' Event Configuration (Mode: $mode):").color(net.kyori.adventure.text.format.NamedTextColor.GOLD))
|
||||||
|
actions.forEachIndexed { index, action ->
|
||||||
|
val type = when (action) {
|
||||||
|
is net.hareworks.npc_mannequin.mannequin.MannequinAction.Command -> "COMMAND"
|
||||||
|
is net.hareworks.npc_mannequin.mannequin.MannequinAction.Message -> "MESSAGE"
|
||||||
|
}
|
||||||
|
val content = when (action) {
|
||||||
|
is net.hareworks.npc_mannequin.mannequin.MannequinAction.Command -> action.command
|
||||||
|
is net.hareworks.npc_mannequin.mannequin.MannequinAction.Message -> action.message
|
||||||
|
}
|
||||||
|
|
||||||
|
var comp = net.kyori.adventure.text.Component.text(" [$index] ").color(net.kyori.adventure.text.format.NamedTextColor.YELLOW)
|
||||||
|
.append(net.kyori.adventure.text.Component.text("$type: ").color(net.kyori.adventure.text.format.NamedTextColor.AQUA))
|
||||||
|
.append(net.kyori.adventure.text.Component.text(content).color(net.kyori.adventure.text.format.NamedTextColor.WHITE))
|
||||||
|
|
||||||
|
// Add removal button
|
||||||
|
comp = comp.append(net.kyori.adventure.text.Component.text(" [X]").color(net.kyori.adventure.text.format.NamedTextColor.RED)
|
||||||
|
.clickEvent(net.kyori.adventure.text.event.ClickEvent.runCommand("/mannequin set $id event remove $index"))
|
||||||
|
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(net.kyori.adventure.text.Component.text("Remove this action"))))
|
||||||
|
|
||||||
|
// Add swap hint (could be complex UI, keeping it simple: suggesting commands)
|
||||||
|
comp = comp.append(net.kyori.adventure.text.Component.text(" [Swap]").color(net.kyori.adventure.text.format.NamedTextColor.GREEN)
|
||||||
|
.clickEvent(net.kyori.adventure.text.event.ClickEvent.suggestCommand("/mannequin set $id event swap $index "))
|
||||||
|
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(net.kyori.adventure.text.Component.text("Swap with another index"))))
|
||||||
|
|
||||||
|
player.sendMessage(comp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add button
|
||||||
|
val addCmd = net.kyori.adventure.text.Component.text("+ Add Command").color(net.kyori.adventure.text.format.NamedTextColor.GREEN)
|
||||||
|
.clickEvent(net.kyori.adventure.text.event.ClickEvent.suggestCommand("/mannequin set $id event add command "))
|
||||||
|
val addMsg = net.kyori.adventure.text.Component.text(" + Add Message").color(net.kyori.adventure.text.format.NamedTextColor.GREEN)
|
||||||
|
.clickEvent(net.kyori.adventure.text.event.ClickEvent.suggestCommand("/mannequin set $id event add message "))
|
||||||
|
|
||||||
|
player.sendMessage(addCmd.append(addMsg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
literal("lookat") {
|
||||||
|
literal("pos") {
|
||||||
|
float("x") {
|
||||||
|
float("y") {
|
||||||
|
float("z") {
|
||||||
|
executes {
|
||||||
|
val id = argument<String>("id")
|
||||||
|
val x = argument<Double>("x")
|
||||||
|
val y = argument<Double>("y")
|
||||||
|
val z = argument<Double>("z")
|
||||||
|
val player = sender as? Player
|
||||||
|
val world = player?.world?.name ?: "world" // Fallback to default, though command is usually run by player/console
|
||||||
|
|
||||||
|
val loc = StoredLocation(world, x, y, z, 0f, 0f)
|
||||||
|
registry.updateSettings(id) {
|
||||||
|
it.copy(lookAtLocation = loc, lookAtRadius = null)
|
||||||
|
}
|
||||||
|
success("Set look-at target for '$id' to $x, $y, $z.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
literal("near") {
|
||||||
|
literal("range") {
|
||||||
|
float("radius", min = 0.0, max = 64.0) {
|
||||||
|
executes {
|
||||||
|
val id = argument<String>("id")
|
||||||
|
val radius = argument<Double>("radius")
|
||||||
|
registry.updateSettings(id) {
|
||||||
|
it.copy(lookAtRadius = radius, lookAtLocation = null)
|
||||||
|
}
|
||||||
|
success("Set look-at radius for '$id' to $radius blocks.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
literal("auto-turn") {
|
||||||
|
literal("on") {
|
||||||
|
executes {
|
||||||
|
val id = argument<String>("id")
|
||||||
|
registry.updateSettings(id) {
|
||||||
|
it.copy(lookAtResetEnabled = true)
|
||||||
|
}
|
||||||
|
success("Enabled auto-turn for '$id'.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
literal("off") {
|
||||||
|
executes {
|
||||||
|
val id = argument<String>("id")
|
||||||
|
registry.updateSettings(id) {
|
||||||
|
it.copy(lookAtResetEnabled = false)
|
||||||
|
}
|
||||||
|
success("Disabled auto-turn for '$id'.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
literal("wait") {
|
||||||
|
float("seconds") {
|
||||||
|
executes {
|
||||||
|
val id = argument<String>("id")
|
||||||
|
val seconds = argument<Double>("seconds")
|
||||||
|
registry.updateSettings(id) {
|
||||||
|
it.copy(lookAtResetDelay = if (seconds < 0) 0.0 else seconds)
|
||||||
|
}
|
||||||
|
if (seconds <= 0) {
|
||||||
|
success("Set auto-turn to reset immediately for '$id'.")
|
||||||
|
} else {
|
||||||
|
success("Set auto-turn wait delay for '$id' to $seconds seconds.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
literal("take") {
|
||||||
|
float("seconds") {
|
||||||
|
executes {
|
||||||
|
val id = argument<String>("id")
|
||||||
|
val seconds = argument<Double>("seconds")
|
||||||
|
registry.updateSettings(id) {
|
||||||
|
it.copy(lookAtResetDuration = if (seconds < 0) 0.0 else seconds)
|
||||||
|
}
|
||||||
|
if (seconds <= 0) {
|
||||||
|
success("Set auto-turn to snap instantly for '$id'.")
|
||||||
|
} else {
|
||||||
|
success("Set auto-turn duration for '$id' to $seconds seconds.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
literal("conversation") {
|
||||||
|
string("state") {
|
||||||
|
suggests { prefix ->
|
||||||
|
listOf("true", "false", "on", "off").filter { it.startsWith(prefix.lowercase()) }
|
||||||
|
}
|
||||||
|
executes {
|
||||||
|
val id = argument<String>("id")
|
||||||
|
val input = argument<String>("state")
|
||||||
|
val state = parseBoolean(input) ?: false
|
||||||
|
registry.updateSettings(id) {
|
||||||
|
it.copy(lookAtConversation = state)
|
||||||
|
}
|
||||||
|
success("Set look-at-conversation for '$id' to $state.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
literal("onclick") {
|
||||||
|
string("state") {
|
||||||
|
suggests { prefix ->
|
||||||
|
listOf("true", "false", "on", "off").filter { it.startsWith(prefix.lowercase()) }
|
||||||
|
}
|
||||||
|
executes {
|
||||||
|
val id = argument<String>("id")
|
||||||
|
val input = argument<String>("state")
|
||||||
|
val state = parseBoolean(input) ?: false
|
||||||
|
registry.updateSettings(id) {
|
||||||
|
it.copy(lookAtOnClick = state)
|
||||||
|
}
|
||||||
|
success("Set look-at-on-click for '$id' to $state.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
literal("clear") {
|
literal("clear") {
|
||||||
executes {
|
executes {
|
||||||
val id = argument<String>("id")
|
val id = argument<String>("id")
|
||||||
registry.updateSettings(id) { it.copy(serverCommand = null) }
|
registry.updateSettings(id) {
|
||||||
success("Cleared console command for '$id'.")
|
it.copy(
|
||||||
}
|
lookAtRadius = null,
|
||||||
}
|
lookAtLocation = null,
|
||||||
}
|
lookAtOnClick = false,
|
||||||
literal("player") {
|
lookAtConversation = true,
|
||||||
literal("set") {
|
lookAtResetEnabled = true,
|
||||||
greedyString("command") {
|
lookAtResetDelay = 0.5,
|
||||||
executes {
|
lookAtResetDuration = 0.5
|
||||||
val id = argument<String>("id")
|
)
|
||||||
val cmd = argument<String>("command")
|
|
||||||
registry.updateSettings(id) { it.copy(playerCommand = cmd) }
|
|
||||||
success("Set player command for '$id'.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
literal("clear") {
|
|
||||||
executes {
|
|
||||||
val id = argument<String>("id")
|
|
||||||
registry.updateSettings(id) { it.copy(playerCommand = null) }
|
|
||||||
success("Cleared player command for '$id'.")
|
|
||||||
}
|
}
|
||||||
|
success("Disabled all look-at behaviors for '$id'.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,24 @@ data class MannequinSettings(
|
||||||
val pose: Pose = Pose.STANDING,
|
val pose: Pose = Pose.STANDING,
|
||||||
val mainHand: MainHand = MainHand.RIGHT,
|
val mainHand: MainHand = MainHand.RIGHT,
|
||||||
val immovable: Boolean = false,
|
val immovable: Boolean = false,
|
||||||
|
val invulnerable: Boolean = false,
|
||||||
|
val health: Double? = null,
|
||||||
|
val customName: Component? = null,
|
||||||
val description: Component? = null,
|
val description: Component? = null,
|
||||||
val hideDescription: Boolean = false,
|
val hideDescription: Boolean = false,
|
||||||
val hiddenLayers: Set<MannequinHiddenLayer> = emptySet(),
|
val hiddenLayers: Set<MannequinHiddenLayer> = emptySet(),
|
||||||
val profile: StoredProfile? = null,
|
val profile: StoredProfile? = null,
|
||||||
val respawnDelay: Int = 0,
|
val respawnDelay: Int = 0,
|
||||||
val serverCommand: String? = null,
|
val lookAtRadius: Double? = null,
|
||||||
val playerCommand: String? = null
|
val lookAtLocation: StoredLocation? = null,
|
||||||
|
val lookAtResetEnabled: Boolean = true,
|
||||||
|
val lookAtResetDelay: Double = 0.5,
|
||||||
|
val lookAtResetDuration: Double = 0.5,
|
||||||
|
val lookAtOnClick: Boolean = false,
|
||||||
|
val lookAtConversation: Boolean = true,
|
||||||
|
val eventMode: EventMode = EventMode.SEQUENTIAL,
|
||||||
|
val eventActions: List<MannequinAction> = emptyList(),
|
||||||
|
val eventTimeout: Double = 5.0
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val POSES = setOf(
|
val POSES = setOf(
|
||||||
|
|
@ -43,6 +54,9 @@ data class MannequinSettings(
|
||||||
pose = entity.pose,
|
pose = entity.pose,
|
||||||
mainHand = entity.mainHand,
|
mainHand = entity.mainHand,
|
||||||
immovable = entity.isImmovable,
|
immovable = entity.isImmovable,
|
||||||
|
invulnerable = entity.isInvulnerable,
|
||||||
|
health = entity.health,
|
||||||
|
customName = entity.customName(),
|
||||||
description = entity.description,
|
description = entity.description,
|
||||||
hideDescription = entity.description == Component.empty(),
|
hideDescription = entity.description == Component.empty(),
|
||||||
hiddenLayers = MannequinHiddenLayer.fromSkinParts(skinParts),
|
hiddenLayers = MannequinHiddenLayer.fromSkinParts(skinParts),
|
||||||
|
|
@ -52,6 +66,16 @@ data class MannequinSettings(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class EventMode {
|
||||||
|
RANDOM,
|
||||||
|
SEQUENTIAL
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface MannequinAction {
|
||||||
|
data class Command(val command: String) : MannequinAction
|
||||||
|
data class Message(val message: String) : MannequinAction
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Named layers that can individually be hidden on top of the default mannequin skin.
|
* Named layers that can individually be hidden on top of the default mannequin skin.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import net.hareworks.npc_mannequin.mannequin.MannequinRecord
|
||||||
import net.hareworks.npc_mannequin.mannequin.MannequinSettings
|
import net.hareworks.npc_mannequin.mannequin.MannequinSettings
|
||||||
import net.kyori.adventure.text.Component
|
import net.kyori.adventure.text.Component
|
||||||
import org.bukkit.Location
|
import org.bukkit.Location
|
||||||
|
import org.bukkit.attribute.Attribute
|
||||||
import org.bukkit.entity.Mannequin
|
import org.bukkit.entity.Mannequin
|
||||||
import org.bukkit.plugin.java.JavaPlugin
|
import org.bukkit.plugin.java.JavaPlugin
|
||||||
|
|
||||||
|
|
@ -17,6 +18,15 @@ class MannequinController(private val plugin: JavaPlugin) {
|
||||||
entity.pose = settings.pose
|
entity.pose = settings.pose
|
||||||
entity.mainHand = settings.mainHand
|
entity.mainHand = settings.mainHand
|
||||||
entity.isImmovable = settings.immovable
|
entity.isImmovable = settings.immovable
|
||||||
|
entity.isInvulnerable = settings.invulnerable
|
||||||
|
entity.customName(settings.customName)
|
||||||
|
|
||||||
|
// Set health if specified, otherwise use max health
|
||||||
|
if (settings.health != null) {
|
||||||
|
val maxHealth = entity.getAttribute(Attribute.MAX_HEALTH)?.value ?: 20.0
|
||||||
|
entity.health = settings.health.coerceIn(0.0, maxHealth)
|
||||||
|
}
|
||||||
|
|
||||||
val skinParts = entity.skinParts.mutableCopy()
|
val skinParts = entity.skinParts.mutableCopy()
|
||||||
applyLayers(settings.hiddenLayers, skinParts)
|
applyLayers(settings.hiddenLayers, skinParts)
|
||||||
entity.setSkinParts(skinParts)
|
entity.setSkinParts(skinParts)
|
||||||
|
|
|
||||||
|
|
@ -40,20 +40,63 @@ class MannequinListener(
|
||||||
val entity = event.rightClicked
|
val entity = event.rightClicked
|
||||||
if (entity !is Mannequin) return
|
if (entity !is Mannequin) return
|
||||||
|
|
||||||
|
// Only process main hand interactions to prevent double triggering
|
||||||
|
if (event.hand != org.bukkit.inventory.EquipmentSlot.HAND) return
|
||||||
|
|
||||||
val record = registry.all().firstOrNull { it.entityId == entity.uniqueId } ?: return
|
val record = registry.all().firstOrNull { it.entityId == entity.uniqueId } ?: return
|
||||||
val settings = record.settings
|
val settings = record.settings
|
||||||
val player = event.player
|
val player = event.player
|
||||||
|
|
||||||
settings.serverCommand?.let { cmd ->
|
if (settings.eventActions.isNotEmpty()) {
|
||||||
val cleanCmd = if (cmd.startsWith("/")) cmd.substring(1) else cmd
|
val now = System.currentTimeMillis()
|
||||||
val finalCmd = cleanCmd.replace("<player>", player.name)
|
var actionIndex = 0
|
||||||
plugin.server.dispatchCommand(plugin.server.consoleSender, finalCmd)
|
|
||||||
|
when (settings.eventMode) {
|
||||||
|
net.hareworks.npc_mannequin.mannequin.EventMode.RANDOM -> {
|
||||||
|
actionIndex = settings.eventActions.indices.random()
|
||||||
|
}
|
||||||
|
net.hareworks.npc_mannequin.mannequin.EventMode.SEQUENTIAL -> {
|
||||||
|
val state = registry.getConversationState(record.id, player.uniqueId)
|
||||||
|
val timeout = (settings.eventTimeout * 1000).toLong()
|
||||||
|
|
||||||
|
if (now - state.lastInteraction > timeout) {
|
||||||
|
state.index = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.playerCommand?.let { cmd ->
|
if (state.index >= settings.eventActions.size) {
|
||||||
|
state.index = 0 // Loop or cap? Assuming loop.
|
||||||
|
}
|
||||||
|
|
||||||
|
actionIndex = state.index
|
||||||
|
|
||||||
|
// Advance index for next time
|
||||||
|
state.index = (state.index + 1) % settings.eventActions.size
|
||||||
|
state.lastInteraction = now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val action = settings.eventActions[actionIndex]
|
||||||
|
when (action) {
|
||||||
|
is net.hareworks.npc_mannequin.mannequin.MannequinAction.Command -> {
|
||||||
|
val cmd = action.command
|
||||||
val cleanCmd = if (cmd.startsWith("/")) cmd.substring(1) else cmd
|
val cleanCmd = if (cmd.startsWith("/")) cmd.substring(1) else cmd
|
||||||
val finalCmd = cleanCmd.replace("<player>", player.name)
|
val finalCmd = cleanCmd.replace("<player>", player.name)
|
||||||
player.performCommand(finalCmd)
|
// If legacy behavior was "console uses execute as player", we should stick to console dispatch.
|
||||||
|
// But explicitly, user creates "console commands".
|
||||||
|
plugin.server.dispatchCommand(plugin.server.consoleSender, finalCmd)
|
||||||
|
}
|
||||||
|
is net.hareworks.npc_mannequin.mannequin.MannequinAction.Message -> {
|
||||||
|
val message = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(
|
||||||
|
action.message.replace("<player>", player.name)
|
||||||
|
)
|
||||||
|
player.sendMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Look-At-On-Click
|
||||||
|
if (settings.lookAtOnClick) {
|
||||||
|
registry.markInteraction(record.id, player)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,74 @@ import org.bukkit.entity.Mannequin
|
||||||
import org.bukkit.plugin.java.JavaPlugin
|
import org.bukkit.plugin.java.JavaPlugin
|
||||||
|
|
||||||
class MannequinRegistry(
|
class MannequinRegistry(
|
||||||
private val plugin: JavaPlugin,
|
val plugin: JavaPlugin,
|
||||||
private val storage: MannequinStorage,
|
private val storage: MannequinStorage,
|
||||||
private val controller: MannequinController
|
private val controller: MannequinController
|
||||||
) {
|
) {
|
||||||
private val records: MutableMap<String, MannequinRecord> = storage.load().toMutableMap()
|
private val records: MutableMap<String, MannequinRecord> = storage.load().toMutableMap()
|
||||||
|
|
||||||
|
// Runtime state tracking for lookAtOnClick, not persisted
|
||||||
|
val interactions = java.util.concurrent.ConcurrentHashMap<String, InteractionState>()
|
||||||
|
private val INTERACTION_EXPIRY_MS = 2000L // 2 seconds default active attention
|
||||||
|
|
||||||
|
// Runtime state for sequential conversation progress: MannequinID -> (PlayerUUID -> State)
|
||||||
|
private val conversationStates = java.util.concurrent.ConcurrentHashMap<String, java.util.concurrent.ConcurrentHashMap<java.util.UUID, ConversationState>>()
|
||||||
|
|
||||||
|
// Runtime state for look-at reset behavior
|
||||||
|
val lookAtStates = java.util.concurrent.ConcurrentHashMap<String, LookAtState>()
|
||||||
|
|
||||||
|
data class InteractionState(val playerId: java.util.UUID, val timestamp: Long)
|
||||||
|
data class ConversationState(var index: Int, var lastInteraction: Long)
|
||||||
|
data class LookAtState(
|
||||||
|
var lastTargetTime: Long = 0L,
|
||||||
|
var isResetting: Boolean = false,
|
||||||
|
var resetStartTime: Long = 0L,
|
||||||
|
var resetStartYaw: Float = 0f,
|
||||||
|
var resetStartPitch: Float = 0f
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getConversationState(id: String, player: java.util.UUID): ConversationState {
|
||||||
|
val mannequinMap = conversationStates.computeIfAbsent(id) { java.util.concurrent.ConcurrentHashMap() }
|
||||||
|
return mannequinMap.computeIfAbsent(player) { ConversationState(0, 0L) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLastConversationPartner(id: String): org.bukkit.entity.Player? {
|
||||||
|
val record = records[id] ?: return null
|
||||||
|
val timeout = (record.settings.eventTimeout * 1000).toLong()
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
|
||||||
|
val mannequinMap = conversationStates[id] ?: return null
|
||||||
|
val mostRecent = mannequinMap.entries
|
||||||
|
.filter { now - it.value.lastInteraction < timeout }
|
||||||
|
.maxByOrNull { it.value.lastInteraction }
|
||||||
|
?: return null
|
||||||
|
|
||||||
|
return plugin.server.getPlayer(mostRecent.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markInteraction(id: String, player: org.bukkit.entity.Player) {
|
||||||
|
val record = records[id] ?: return
|
||||||
|
var duration = 2000L
|
||||||
|
// We use lookAtResetDelay as the attention duration if available.
|
||||||
|
// If it's negative (disabled reset), we still default to 2s for the "attention" duration to avoid locking.
|
||||||
|
val delay = record.settings.lookAtResetDelay
|
||||||
|
if (delay != null && delay > 0) {
|
||||||
|
duration = (delay * 1000).toLong()
|
||||||
|
}
|
||||||
|
interactions[id] = InteractionState(player.uniqueId, System.currentTimeMillis() + duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getActiveInteraction(id: String): org.bukkit.entity.Player? {
|
||||||
|
val state = interactions[id] ?: return null
|
||||||
|
if (System.currentTimeMillis() > state.timestamp) {
|
||||||
|
interactions.remove(id)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return plugin.server.getPlayer(state.playerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun all(): Collection<MannequinRecord> = records.values.sortedBy { it.id }
|
fun all(): Collection<MannequinRecord> = records.values.sortedBy { it.id }
|
||||||
|
|
||||||
fun find(id: String): MannequinRecord? = records[id]
|
fun find(id: String): MannequinRecord? = records[id]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
package net.hareworks.npc_mannequin.service
|
||||||
|
|
||||||
|
import org.bukkit.GameMode
|
||||||
|
import org.bukkit.scheduler.BukkitRunnable
|
||||||
|
|
||||||
|
class MannequinTickTask(private val registry: MannequinRegistry) : BukkitRunnable() {
|
||||||
|
override fun run() {
|
||||||
|
registry.all().forEach { record ->
|
||||||
|
val settings = record.settings
|
||||||
|
val entity = registry.locate(record.id) ?: return@forEach
|
||||||
|
if (!entity.isValid) return@forEach
|
||||||
|
|
||||||
|
val location = entity.location
|
||||||
|
val world = location.world ?: return@forEach
|
||||||
|
|
||||||
|
var targetDirection: org.bukkit.util.Vector? = null
|
||||||
|
|
||||||
|
// 1. Click Interaction (Highest Priority)
|
||||||
|
if (settings.lookAtOnClick) {
|
||||||
|
val interactingPlayer = registry.getActiveInteraction(record.id)
|
||||||
|
if (interactingPlayer != null && interactingPlayer.world == world && interactingPlayer.location.distanceSquared(location) < 64 * 64) {
|
||||||
|
targetDirection = interactingPlayer.eyeLocation.toVector().subtract(entity.eyeLocation.toVector())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Conversation Partner (if enabled and within timeout)
|
||||||
|
if (targetDirection == null && settings.lookAtConversation) {
|
||||||
|
val conversationPartner = registry.getLastConversationPartner(record.id)
|
||||||
|
if (conversationPartner != null && conversationPartner.world == world && conversationPartner.location.distanceSquared(location) < 64 * 64) {
|
||||||
|
targetDirection = conversationPartner.eyeLocation.toVector().subtract(entity.eyeLocation.toVector())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Nearby Players (Near behavior) - Only if no click interaction or conversation active
|
||||||
|
if (targetDirection == null && settings.lookAtRadius != null && settings.lookAtRadius > 0) {
|
||||||
|
val target = world.getPlayers()
|
||||||
|
.filter { p ->
|
||||||
|
p.gameMode != GameMode.SPECTATOR &&
|
||||||
|
p.world == world &&
|
||||||
|
p.location.distanceSquared(location) <= settings.lookAtRadius * settings.lookAtRadius
|
||||||
|
}
|
||||||
|
.minByOrNull { it.location.distanceSquared(location) }
|
||||||
|
|
||||||
|
if (target != null) {
|
||||||
|
targetDirection = target.eyeLocation.toVector().subtract(entity.eyeLocation.toVector())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Static Position
|
||||||
|
if (targetDirection == null && settings.lookAtLocation != null) {
|
||||||
|
val targetLoc = settings.lookAtLocation.toLocation(registry.plugin.server)
|
||||||
|
if (targetLoc != null && targetLoc.world == world) {
|
||||||
|
targetDirection = targetLoc.toVector().subtract(entity.eyeLocation.toVector())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply rotation
|
||||||
|
if (targetDirection != null) {
|
||||||
|
val newLoc = location.clone().setDirection(targetDirection)
|
||||||
|
// Only teleport if significant change to save bandwidth
|
||||||
|
// If we are looking at something, update last target time
|
||||||
|
val state = registry.lookAtStates.computeIfAbsent(record.id) { net.hareworks.npc_mannequin.service.MannequinRegistry.LookAtState() }
|
||||||
|
state.lastTargetTime = System.currentTimeMillis()
|
||||||
|
state.isResetting = false
|
||||||
|
|
||||||
|
if (kotlin.math.abs(location.yaw - newLoc.yaw) > 1.0f || kotlin.math.abs(location.pitch - newLoc.pitch) > 1.0f) {
|
||||||
|
entity.teleport(newLoc)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Return Home logic
|
||||||
|
val homeLoc = record.location?.toLocation(registry.plugin.server)
|
||||||
|
if (homeLoc != null && settings.lookAtResetEnabled) {
|
||||||
|
val state = registry.lookAtStates.computeIfAbsent(record.id) { net.hareworks.npc_mannequin.service.MannequinRegistry.LookAtState() }
|
||||||
|
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val waitTime = (settings.lookAtResetDelay * 1000).toLong()
|
||||||
|
|
||||||
|
if (now - state.lastTargetTime >= waitTime) {
|
||||||
|
if (!state.isResetting) {
|
||||||
|
state.isResetting = true
|
||||||
|
state.resetStartTime = now
|
||||||
|
state.resetStartYaw = location.yaw
|
||||||
|
state.resetStartPitch = location.pitch
|
||||||
|
}
|
||||||
|
|
||||||
|
val duration = (settings.lookAtResetDuration * 1000).toLong()
|
||||||
|
|
||||||
|
if (duration <= 0) {
|
||||||
|
// Instant snap
|
||||||
|
if (kotlin.math.abs(location.yaw - homeLoc.yaw) > 1.0f || kotlin.math.abs(location.pitch - homeLoc.pitch) > 1.0f) {
|
||||||
|
entity.teleport(homeLoc)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Interpolate
|
||||||
|
val elapsed = now - state.resetStartTime
|
||||||
|
val progress = (elapsed.toDouble() / duration).coerceIn(0.0, 1.0).toFloat()
|
||||||
|
|
||||||
|
// Interpolate yaw (handling 360 wrap)
|
||||||
|
var startYaw = state.resetStartYaw
|
||||||
|
var endYaw = homeLoc.yaw
|
||||||
|
// Normalize to -180..180
|
||||||
|
while (startYaw < -180) startYaw += 360
|
||||||
|
while (startYaw >= 180) startYaw -= 360
|
||||||
|
while (endYaw < -180) endYaw += 360
|
||||||
|
while (endYaw >= 180) endYaw -= 360
|
||||||
|
|
||||||
|
var diff = endYaw - startYaw
|
||||||
|
if (diff < -180) diff += 360
|
||||||
|
if (diff > 180) diff -= 360
|
||||||
|
|
||||||
|
val currentYaw = startYaw + (diff * progress)
|
||||||
|
val currentPitch = state.resetStartPitch + (homeLoc.pitch - state.resetStartPitch) * progress
|
||||||
|
|
||||||
|
val newLoc = location.clone()
|
||||||
|
newLoc.yaw = currentYaw
|
||||||
|
newLoc.pitch = currentPitch
|
||||||
|
|
||||||
|
if (kotlin.math.abs(location.yaw - newLoc.yaw) > 0.1f || kotlin.math.abs(location.pitch - newLoc.pitch) > 0.1f) {
|
||||||
|
entity.teleport(newLoc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package net.hareworks.npc_mannequin.storage
|
package net.hareworks.npc_mannequin.storage
|
||||||
|
|
||||||
|
import net.hareworks.npc_mannequin.mannequin.EventMode
|
||||||
|
import net.hareworks.npc_mannequin.mannequin.MannequinAction
|
||||||
import net.hareworks.npc_mannequin.mannequin.MannequinHiddenLayer
|
import net.hareworks.npc_mannequin.mannequin.MannequinHiddenLayer
|
||||||
import net.hareworks.npc_mannequin.mannequin.MannequinRecord
|
import net.hareworks.npc_mannequin.mannequin.MannequinRecord
|
||||||
import net.hareworks.npc_mannequin.mannequin.MannequinSettings
|
import net.hareworks.npc_mannequin.mannequin.MannequinSettings
|
||||||
|
|
@ -77,29 +79,105 @@ class MannequinStorage(private val plugin: JavaPlugin) {
|
||||||
val pitch = it.getDouble("pitch").toFloat()
|
val pitch = it.getDouble("pitch").toFloat()
|
||||||
StoredLocation(world, x, y, z, yaw, pitch)
|
StoredLocation(world, x, y, z, yaw, pitch)
|
||||||
}
|
}
|
||||||
|
// Deserialize LookAt Location
|
||||||
|
val lookAtLocation = section.getConfigurationSection("lookAtLocation")?.let {
|
||||||
|
val world = it.getString("world") ?: return@let null
|
||||||
|
val x = it.getDouble("x")
|
||||||
|
val y = it.getDouble("y")
|
||||||
|
val z = it.getDouble("z")
|
||||||
|
StoredLocation(world, x, y, z, 0f, 0f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize Event Actions
|
||||||
|
val eventMode = section.getString("eventMode")?.let { runCatching { EventMode.valueOf(it) }.getOrNull() }
|
||||||
|
?: EventMode.SEQUENTIAL
|
||||||
|
val eventTimeout = if (section.contains("eventTimeout")) section.getDouble("eventTimeout") else 5.0
|
||||||
|
|
||||||
|
val actions = mutableListOf<MannequinAction>()
|
||||||
|
if (section.contains("eventActions")) {
|
||||||
|
val list = section.getMapList("eventActions")
|
||||||
|
list.forEach { map ->
|
||||||
|
val type = map["type"] as? String
|
||||||
|
val value = map["value"] as? String
|
||||||
|
if (type != null && value != null) {
|
||||||
|
when (type) {
|
||||||
|
"COMMAND" -> actions.add(MannequinAction.Command(value))
|
||||||
|
"MESSAGE" -> actions.add(MannequinAction.Message(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migration: Append legacy commands if actions list is empty
|
||||||
|
if (actions.isEmpty()) {
|
||||||
|
section.getString("serverCommand")?.let { actions.add(MannequinAction.Command(it)) }
|
||||||
|
section.getString("playerCommand")?.let { actions.add(MannequinAction.Command("execute as <player> run $it")) }
|
||||||
|
}
|
||||||
|
|
||||||
val settings = MannequinSettings(
|
val settings = MannequinSettings(
|
||||||
pose = pose,
|
pose = pose,
|
||||||
mainHand = mainHand,
|
mainHand = mainHand,
|
||||||
immovable = immovable,
|
immovable = immovable,
|
||||||
|
invulnerable = section.getBoolean("invulnerable", false),
|
||||||
|
// customName removed to fix compilation (TextSerializers.legacy issue and field availability)
|
||||||
description = description,
|
description = description,
|
||||||
hideDescription = hideDescription,
|
hideDescription = hideDescription,
|
||||||
hiddenLayers = hiddenLayers,
|
hiddenLayers = hiddenLayers,
|
||||||
profile = profile,
|
profile = profile,
|
||||||
serverCommand = section.getString("serverCommand"),
|
respawnDelay = section.getInt("respawnDelay", 0),
|
||||||
playerCommand = section.getString("playerCommand")
|
lookAtRadius = if (section.contains("lookAtRadius")) section.getDouble("lookAtRadius") else null,
|
||||||
|
lookAtLocation = lookAtLocation,
|
||||||
|
lookAtResetEnabled = section.getBoolean("lookAtResetEnabled", true),
|
||||||
|
// Migration: lookAtResetSeconds -> lookAtResetDelay
|
||||||
|
lookAtResetDelay = if (section.contains("lookAtResetSeconds")) {
|
||||||
|
section.getDouble("lookAtResetSeconds")
|
||||||
|
} else {
|
||||||
|
section.getDouble("lookAtResetDelay", 0.5)
|
||||||
|
},
|
||||||
|
lookAtResetDuration = section.getDouble("lookAtResetDuration", 0.5),
|
||||||
|
lookAtOnClick = section.getBoolean("lookAtOnClick", false),
|
||||||
|
lookAtConversation = section.getBoolean("lookAtConversation", true),
|
||||||
|
eventMode = eventMode,
|
||||||
|
eventActions = actions,
|
||||||
|
eventTimeout = eventTimeout
|
||||||
)
|
)
|
||||||
return MannequinRecord(id, settings, location, entityId)
|
return MannequinRecord(id, settings, location, entityId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun serializeRecord(section: ConfigurationSection, record: MannequinRecord) {
|
private fun serializeRecord(section: ConfigurationSection, record: MannequinRecord) {
|
||||||
section.set("pose", record.settings.pose.name)
|
section.set("pose", record.settings.pose.name)
|
||||||
|
section.set("lookAtRadius", record.settings.lookAtRadius)
|
||||||
|
record.settings.lookAtLocation?.let {
|
||||||
|
val locSec = section.createSection("lookAtLocation")
|
||||||
|
locSec.set("world", it.world)
|
||||||
|
locSec.set("x", it.x)
|
||||||
|
locSec.set("y", it.y)
|
||||||
|
locSec.set("z", it.z)
|
||||||
|
}
|
||||||
|
section.set("lookAtResetEnabled", record.settings.lookAtResetEnabled)
|
||||||
|
section.set("lookAtResetDelay", record.settings.lookAtResetDelay)
|
||||||
|
section.set("lookAtResetDuration", record.settings.lookAtResetDuration)
|
||||||
|
section.set("lookAtOnClick", record.settings.lookAtOnClick)
|
||||||
|
section.set("lookAtConversation", record.settings.lookAtConversation)
|
||||||
section.set("mainHand", record.settings.mainHand.name)
|
section.set("mainHand", record.settings.mainHand.name)
|
||||||
section.set("immovable", record.settings.immovable)
|
section.set("immovable", record.settings.immovable)
|
||||||
section.set("hideDescription", record.settings.hideDescription)
|
section.set("hideDescription", record.settings.hideDescription)
|
||||||
section.set("description", TextSerializers.miniMessage(record.settings.description))
|
section.set("description", TextSerializers.miniMessage(record.settings.description))
|
||||||
section.set("hiddenLayers", record.settings.hiddenLayers.map { it.key })
|
section.set("hiddenLayers", record.settings.hiddenLayers.map { it.key })
|
||||||
section.set("serverCommand", record.settings.serverCommand)
|
// Serialize Event Actions
|
||||||
section.set("playerCommand", record.settings.playerCommand)
|
section.set("eventMode", record.settings.eventMode.name)
|
||||||
|
section.set("eventTimeout", record.settings.eventTimeout)
|
||||||
|
val serializedActions = record.settings.eventActions.map { action ->
|
||||||
|
when (action) {
|
||||||
|
is MannequinAction.Command -> mapOf("type" to "COMMAND", "value" to action.command)
|
||||||
|
is MannequinAction.Message -> mapOf("type" to "MESSAGE", "value" to action.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
section.set("eventActions", serializedActions)
|
||||||
|
|
||||||
|
// Cleanup legacy fields
|
||||||
|
section.set("serverCommand", null)
|
||||||
|
section.set("playerCommand", null)
|
||||||
record.settings.profile?.let { profile ->
|
record.settings.profile?.let { profile ->
|
||||||
val profileSection = section.createSection("profile")
|
val profileSection = section.createSection("profile")
|
||||||
profileSection.set("name", profile.name)
|
profileSection.set("name", profile.name)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user