feat: ビルド通らない問題

This commit is contained in:
Keisuke Hirata 2025-12-06 05:07:25 +09:00
parent 5f88a0cc7a
commit dd69f06346
9 changed files with 899 additions and 26 deletions

View File

@ -14,8 +14,8 @@ repositories {
dependencies { dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT") compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT")
implementation("org.jetbrains.kotlin:kotlin-stdlib") implementation("org.jetbrains.kotlin:kotlin-stdlib")
implementation("net.hareworks:kommand-lib:1.1") implementation("net.hareworks:kommand-lib")
implementation("net.hareworks:permits-lib:1.1") implementation("net.hareworks:permits-lib")
implementation("net.kyori:adventure-text-minimessage:4.17.0") implementation("net.kyori:adventure-text-minimessage:4.17.0")
implementation("net.kyori:adventure-text-serializer-plain:4.17.0") implementation("net.kyori:adventure-text-serializer-plain:4.17.0")
} }

View File

@ -1,24 +0,0 @@
package net.hareworks.npc_mannequin
import net.hareworks.kommand_lib.KommandLib
import net.hareworks.permits_lib.PermitsLib
import org.bukkit.plugin.ServicePriority
import org.bukkit.plugin.java.JavaPlugin
class GhostDisplaysPlugin : JavaPlugin() {
private var permissionSession: MutationSession? = null
private var kommand: KommandLib? = null
override fun onEnable() {
instance = this
}
override fun onDisable() {
instance = null
}
companion object {
@Volatile
private var instance: GhostDisplaysPlugin? = null
}
}

View File

@ -0,0 +1,28 @@
package net.hareworks.npc_mannequin
import net.hareworks.kommand_lib.KommandLib
import net.hareworks.npc_mannequin.commands.MannequinCommands
import net.hareworks.npc_mannequin.service.MannequinController
import net.hareworks.npc_mannequin.service.MannequinRegistry
import net.hareworks.npc_mannequin.storage.MannequinStorage
import org.bukkit.plugin.ServicePriority
import org.bukkit.plugin.java.JavaPlugin
class Plugin : JavaPlugin() {
private var kommand: KommandLib? = null
private lateinit var registry: MannequinRegistry
override fun onEnable() {
val storage = MannequinStorage(this)
val controller = MannequinController(this)
registry = MannequinRegistry(this, storage, controller)
kommand = MannequinCommands(this, registry).register()
server.servicesManager.register(MannequinRegistry::class.java, registry, this, ServicePriority.Normal)
logger.info("Loaded ${registry.all().size} mannequin definitions.")
}
override fun onDisable() {
server.servicesManager.unregisterAll(this)
kommand?.unregister()
}
}

View File

@ -0,0 +1,368 @@
package net.hareworks.npc_mannequin.commands
import net.hareworks.kommand_lib.KommandLib
import net.hareworks.kommand_lib.context.KommandContext
import net.hareworks.kommand_lib.kommand
import net.hareworks.npc_mannequin.mannequin.MannequinHiddenLayer
import net.hareworks.npc_mannequin.mannequin.MannequinSettings
import net.hareworks.npc_mannequin.mannequin.StoredLocation
import net.hareworks.npc_mannequin.mannequin.StoredProfile
import net.hareworks.npc_mannequin.service.MannequinRegistry
import net.hareworks.npc_mannequin.text.TextSerializers
import net.hareworks.permits_lib.PermitsLib
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
import org.bukkit.entity.Entity
import org.bukkit.entity.Mannequin
import org.bukkit.entity.Player
import org.bukkit.entity.Pose
import org.bukkit.inventory.MainHand
import org.bukkit.plugin.java.JavaPlugin
import java.util.Locale
class MannequinCommands(
private val plugin: JavaPlugin,
private val registry: MannequinRegistry
) {
fun register(): KommandLib = kommand(plugin) {
permissions {
namespace = "hareworks"
rootSegment = "command"
defaultDescription { ctx ->
"Allows /${ctx.commandName} ${ctx.path.joinToString(" ")}"
}
session { PermitsLib.session(it) }
}
command("mannequin", listOf("mnpc", "mnq", "npcmannequin")) {
description = "Register and manage mannequin NPCs"
permission = "hareworks.command.mannequin"
executes { listMannequins(registry) }
literal("list") {
executes { listMannequins(registry) }
}
literal("register") {
string("id") {
selector("target") {
executes {
val id = argument<String>("id")
val entity = argument<List<Entity>>("target").firstOrNull { it is Mannequin } as? Mannequin
if (entity == null) {
error("Selector must target at least one mannequin entity.")
return@executes
}
runCatching {
registry.register(id, entity, overwrite = false)
}.onSuccess {
success("Registered mannequin '$id' from entity ${entity.uniqueId}.")
}.onFailure {
error(it.message ?: "Failed to register mannequin.")
}
}
literal("--overwrite") {
executes {
val id = argument<String>("id")
val entity = argument<List<Entity>>("target").firstOrNull { it is Mannequin } as? Mannequin
if (entity == null) {
error("Selector must target at least one mannequin entity.")
return@executes
}
registry.register(id, entity, overwrite = true)
success("Replaced mannequin '$id' with entity ${entity.uniqueId}.")
}
}
}
}
}
literal("create") {
string("id") {
executes {
val player = requirePlayer() ?: return@executes
val id = argument<String>("id")
runCatching {
registry.create(id, player.location, MannequinSettings())
}.onSuccess {
success("Spawned mannequin '$id' at ${formatLocation(it.location)}.")
}.onFailure {
error(it.message ?: "Failed to create mannequin.")
}
}
}
}
literal("move") {
string("id") {
executes {
val player = requirePlayer() ?: return@executes
val id = argument<String>("id")
runCatching {
registry.relocate(id, player.location)
}.onSuccess {
success("Updated location of '$id' to ${formatLocation(it.location)}.")
}.onFailure {
error(it.message ?: "Failed to move mannequin.")
}
}
}
}
literal("apply") {
string("id") {
executes {
val id = argument<String>("id")
runCatching {
registry.apply(id, spawnIfMissing = true)
?: throw IllegalStateException("Mannequin '$id' is not spawned and has no stored position.")
}.onSuccess {
success("Applied stored settings to '$id'.")
}.onFailure {
error(it.message ?: "Failed to apply mannequin settings.")
}
}
}
}
literal("remove") {
string("id") {
executes {
val id = argument<String>("id")
runCatching { registry.remove(id, deleteEntity = false) }
.onSuccess { success("Removed mannequin '$id' but kept the entity in the world.") }
.onFailure { error(it.message ?: "Failed to remove mannequin.") }
}
literal("--delete-entity") {
executes {
val id = argument<String>("id")
runCatching { registry.remove(id, deleteEntity = true) }
.onSuccess { success("Removed mannequin '$id' and deleted its entity.") }
.onFailure { error(it.message ?: "Failed to remove mannequin.") }
}
}
}
}
literal("set") {
string("id") {
literal("pose") {
string("pose") {
suggests { prefix -> Pose.entries.map { it.name.lowercase() }.filter { it.startsWith(prefix.lowercase()) } }
executes {
val id = argument<String>("id")
val poseToken = argument<String>("pose")
val pose = runCatching { Pose.valueOf(poseToken.uppercase()) }.getOrNull()
if (pose == null) {
error("Unknown pose '$poseToken'.")
return@executes
}
registry.updateSettings(id) { it.copy(pose = pose) }
success("Updated pose for '$id' to ${pose.name.lowercase()}.")
}
}
}
literal("mainhand") {
string("hand") {
suggests { prefix -> MainHand.entries.map { it.name.lowercase() }.filter { it.startsWith(prefix.lowercase()) } }
executes {
val id = argument<String>("id")
val handToken = argument<String>("hand")
val mainHand = runCatching { MainHand.valueOf(handToken.uppercase()) }.getOrNull()
if (mainHand == null) {
error("Unknown hand '$handToken'.")
return@executes
}
registry.updateSettings(id) { it.copy(mainHand = mainHand) }
success("Updated main hand for '$id' to ${mainHand.name.lowercase()}.")
}
}
}
literal("immovable") {
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(immovable = state) }
success("Set immovable for '$id' to $state.")
}
}
}
literal("description") {
literal("text") {
executes {
val id = argument<String>("id")
val payload = remainingInput(DESCRIPTION_TEXT_OFFSET)
if (payload.isNullOrBlank()) {
error("Provide MiniMessage text after the command, e.g. /mannequin set $id description text <text>")
return@executes
}
val component = runCatching { TextSerializers.miniMessage(payload) }
.onFailure { error("MiniMessage parse failed: ${it.message}") }
.getOrNull()
if (component == null) {
return@executes
}
registry.updateSettings(id) { it.copy(description = component, hideDescription = false) }
success("Updated description for '$id'.")
}
}
literal("clear") {
executes {
val id = argument<String>("id")
registry.updateSettings(id) { it.copy(description = null, hideDescription = false) }
success("Cleared custom description for '$id'.")
}
}
literal("hide") {
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(hideDescription = state) }
success(if (state) "Description hidden for '$id'." else "Description visible for '$id'.")
}
}
}
}
literal("layers") {
literal("hide") {
string("layer") {
suggests { prefix -> layerSuggestions(prefix) }
executes {
val id = argument<String>("id")
val layerName = argument<String>("layer")
val layer = MannequinHiddenLayer.fromKey(layerName)
if (layer == null) {
error("Unknown layer '$layerName'.")
return@executes
}
registry.updateSettings(id) { it.copy(hiddenLayers = it.hiddenLayers + layer) }
success("Hid layer ${layer.key} for '$id'.")
}
}
}
literal("show") {
string("layer") {
suggests { prefix -> layerSuggestions(prefix) }
executes {
val id = argument<String>("id")
val layerName = argument<String>("layer")
val layer = MannequinHiddenLayer.fromKey(layerName)
if (layer == null) {
error("Unknown layer '$layerName'.")
return@executes
}
registry.updateSettings(id) { it.copy(hiddenLayers = it.hiddenLayers - layer) }
success("Enabled layer ${layer.key} for '$id'.")
}
}
}
literal("clear") {
executes {
val id = argument<String>("id")
registry.updateSettings(id) { it.copy(hiddenLayers = emptySet()) }
success("Cleared hidden layers for '$id'.")
}
}
}
literal("profile") {
literal("player") {
player("source") {
executes {
val id = argument<String>("id")
val source = argument<Player>("source")
val stored = StoredProfile.from(source.playerProfile)
registry.updateSettings(id) { it.copy(profile = stored) }
success("Copied profile from ${source.name} into '$id'.")
}
}
}
literal("clear") {
executes {
val id = argument<String>("id")
registry.updateSettings(id) { it.copy(profile = null) }
success("Cleared stored profile for '$id'.")
}
}
}
}
}
}
}
}
private const val DESCRIPTION_TEXT_OFFSET = 4
private fun KommandContext.listMannequins(registry: MannequinRegistry) {
val entries = registry.all()
if (entries.isEmpty()) {
sender.sendMessage(Component.text("No mannequins registered.", NamedTextColor.YELLOW))
return
}
sender.sendMessage(Component.text("Registered mannequins (${entries.size}):", NamedTextColor.GRAY))
entries.forEach { record ->
val status = if (registry.locate(record.id) != null) "active" else "offline"
val location = formatLocation(record.location)
sender.sendMessage(
Component.text("- ${record.id}: ", NamedTextColor.WHITE)
.append(Component.text(status, if (status == "active") NamedTextColor.GREEN else NamedTextColor.DARK_GRAY))
.append(Component.text(" @ $location", NamedTextColor.GRAY))
)
}
}
private fun KommandContext.requirePlayer(): Player? {
return sender as? Player ?: run {
sender.sendMessage(Component.text("This command can only be run by a player.", NamedTextColor.RED))
null
}
}
private fun KommandContext.success(message: String) {
sender.sendMessage(Component.text(message, NamedTextColor.GREEN))
}
private fun KommandContext.error(message: String) {
sender.sendMessage(Component.text(message, NamedTextColor.RED))
}
private fun KommandContext.remainingInput(offset: Int): String? {
if (args.size <= offset) return null
return args.drop(offset).joinToString(" ").trim().ifEmpty { null }
}
private fun layerSuggestions(prefix: String): List<String> =
MannequinHiddenLayer.entries.map { it.key }.filter { it.startsWith(prefix.lowercase()) }
private fun parseBoolean(input: String): Boolean? = when (input.lowercase()) {
"true", "on", "yes", "1" -> true
"false", "off", "no", "0" -> false
else -> null
}
private fun formatLocation(location: StoredLocation?): String =
location?.let {
val x = String.format(Locale.US, "%.2f", it.x)
val y = String.format(Locale.US, "%.2f", it.y)
val z = String.format(Locale.US, "%.2f", it.z)
"${it.world} ($x, $y, $z)"
} ?: "unknown location"

View File

@ -0,0 +1,165 @@
package net.hareworks.npc_mannequin.mannequin
import com.destroystokyo.paper.SkinParts
import com.destroystokyo.paper.profile.PlayerProfile
import com.destroystokyo.paper.profile.ProfileProperty
import io.papermc.paper.datacomponent.item.ResolvableProfile
import net.kyori.adventure.text.Component
import org.bukkit.Location
import org.bukkit.entity.Mannequin
import org.bukkit.entity.Pose
import org.bukkit.inventory.MainHand
import java.util.UUID
/**
* Immutable snapshot of a mannequin definition that can be persisted and replayed.
*/
data class MannequinSettings(
val pose: Pose = Pose.STANDING,
val mainHand: MainHand = MainHand.RIGHT,
val immovable: Boolean = false,
val description: Component? = null,
val hideDescription: Boolean = false,
val hiddenLayers: Set<MannequinHiddenLayer> = emptySet(),
val profile: StoredProfile? = null
) {
companion object {
fun from(entity: Mannequin): MannequinSettings {
val skinParts = entity.skinParts
return MannequinSettings(
pose = entity.pose,
mainHand = entity.mainHand,
immovable = entity.isImmovable,
description = entity.description,
hideDescription = entity.description == Component.empty(),
hiddenLayers = MannequinHiddenLayer.fromSkinParts(skinParts),
profile = StoredProfile.from(entity.profile)
)
}
}
}
/**
* Named layers that can individually be hidden on top of the default mannequin skin.
*/
enum class MannequinHiddenLayer(val key: String) {
CAPE("cape"),
JACKET("jacket"),
LEFT_SLEEVE("left_sleeve"),
RIGHT_SLEEVE("right_sleeve"),
LEFT_PANTS("left_pants_leg"),
RIGHT_PANTS("right_pants_leg"),
HAT("hat");
companion object {
fun fromKey(input: String): MannequinHiddenLayer? =
entries.firstOrNull { it.key.equals(input, ignoreCase = true) }
fun fromSkinParts(parts: SkinParts): Set<MannequinHiddenLayer> {
val hidden = mutableSetOf<MannequinHiddenLayer>()
if (!parts.hasCapeEnabled()) hidden += CAPE
if (!parts.hasJacketEnabled()) hidden += JACKET
if (!parts.hasLeftSleeveEnabled()) hidden += LEFT_SLEEVE
if (!parts.hasRightSleeveEnabled()) hidden += RIGHT_SLEEVE
if (!parts.hasLeftPantsEnabled()) hidden += LEFT_PANTS
if (!parts.hasRightPantsEnabled()) hidden += RIGHT_PANTS
if (!parts.hasHatsEnabled()) hidden += HAT
return hidden
}
}
}
/**
* Serializable skeleton of a player/mannequin profile (skin/cape/model definition).
*/
data class StoredProfile(
val name: String?,
val uuid: UUID?,
val properties: List<StoredProfileProperty>,
) {
fun toResolvable(): ResolvableProfile {
val builder = ResolvableProfile.resolvableProfile()
name?.let { builder.name(it) }
uuid?.let { builder.uuid(it) }
if (properties.isEmpty()) {
builder.addProperties(emptyList())
} else {
properties.forEach { builder.addProperty(ProfileProperty(it.name, it.value, it.signature)) }
}
return builder.build()
}
companion object {
fun from(profile: ResolvableProfile): StoredProfile {
val props = profile.properties()
.map { StoredProfileProperty(it.name, it.value, it.signature) }
return StoredProfile(
name = profile.name(),
uuid = profile.uuid(),
properties = props
)
}
fun from(profile: PlayerProfile): StoredProfile {
val props = profile.properties.map { StoredProfileProperty(it.name, it.value, it.signature) }
return StoredProfile(
name = profile.name,
uuid = profile.id,
properties = props
)
}
}
}
data class StoredProfileProperty(
val name: String,
val value: String,
val signature: String?
)
/**
* Small serializable wrapper around a Bukkit location so we can persist mannequins across restarts.
*/
data class StoredLocation(
val world: String,
val x: Double,
val y: Double,
val z: Double,
val yaw: Float,
val pitch: Float
) {
fun toLocation(base: org.bukkit.Server): Location? {
val worldObj = base.getWorld(world) ?: return null
return Location(worldObj, x, y, z, yaw, pitch)
}
companion object {
fun from(location: Location): StoredLocation = StoredLocation(
world = location.world?.name ?: throw IllegalStateException("Location has no world"),
x = location.x,
y = location.y,
z = location.z,
yaw = location.yaw,
pitch = location.pitch
)
}
}
/**
* Full registry entry containing the data snapshot and bookkeeping metadata.
*/
data class MannequinRecord(
val id: String,
val settings: MannequinSettings,
val location: StoredLocation?,
val entityId: UUID?
) {
fun updateSettings(next: MannequinSettings): MannequinRecord =
copy(settings = next)
fun updateLocation(next: StoredLocation?): MannequinRecord =
copy(location = next)
fun updateEntityId(next: UUID?): MannequinRecord =
copy(entityId = next)
}

View File

@ -0,0 +1,57 @@
package net.hareworks.npc_mannequin.service
import com.destroystokyo.paper.SkinParts
import net.hareworks.npc_mannequin.mannequin.MannequinHiddenLayer
import net.hareworks.npc_mannequin.mannequin.MannequinRecord
import net.hareworks.npc_mannequin.mannequin.MannequinSettings
import net.kyori.adventure.text.Component
import org.bukkit.Location
import org.bukkit.entity.Mannequin
import org.bukkit.plugin.java.JavaPlugin
class MannequinController(private val plugin: JavaPlugin) {
fun extractSettings(entity: Mannequin): MannequinSettings = MannequinSettings.from(entity)
fun applySettings(entity: Mannequin, settings: MannequinSettings) {
entity.pose = settings.pose
entity.mainHand = settings.mainHand
entity.isImmovable = settings.immovable
val skinParts = entity.skinParts.mutableCopy()
applyLayers(settings.hiddenLayers, skinParts)
entity.setSkinParts(skinParts)
val description = when {
settings.hideDescription -> Component.empty()
settings.description != null -> settings.description
else -> Mannequin.defaultDescription()
}
entity.description = description
val profile = settings.profile?.toResolvable() ?: Mannequin.defaultProfile()
entity.profile = profile
}
fun spawn(location: Location, settings: MannequinSettings): Mannequin {
val world = location.world ?: throw IllegalStateException("Cannot spawn mannequin without world")
return world.spawn(location, Mannequin::class.java) { mannequin ->
applySettings(mannequin, settings)
}
}
fun locate(record: MannequinRecord): Mannequin? {
record.entityId?.let { plugin.server.getEntity(it) as? Mannequin }?.let { return it }
val location = record.location?.toLocation(plugin.server) ?: return null
val world = location.world ?: return null
val results = world.getNearbyEntitiesByType(Mannequin::class.java, location, 0.75, 0.75) { true }
return results.minByOrNull { it.location.distanceSquared(location) }
}
private fun applyLayers(hidden: Set<MannequinHiddenLayer>, parts: SkinParts.Mutable) {
parts.setCapeEnabled(!hidden.contains(MannequinHiddenLayer.CAPE))
parts.setJacketEnabled(!hidden.contains(MannequinHiddenLayer.JACKET))
parts.setLeftSleeveEnabled(!hidden.contains(MannequinHiddenLayer.LEFT_SLEEVE))
parts.setRightSleeveEnabled(!hidden.contains(MannequinHiddenLayer.RIGHT_SLEEVE))
parts.setLeftPantsEnabled(!hidden.contains(MannequinHiddenLayer.LEFT_PANTS))
parts.setRightPantsEnabled(!hidden.contains(MannequinHiddenLayer.RIGHT_PANTS))
parts.setHatsEnabled(!hidden.contains(MannequinHiddenLayer.HAT))
}
}

View File

@ -0,0 +1,121 @@
package net.hareworks.npc_mannequin.service
import net.hareworks.npc_mannequin.mannequin.MannequinRecord
import net.hareworks.npc_mannequin.mannequin.MannequinSettings
import net.hareworks.npc_mannequin.mannequin.StoredLocation
import net.hareworks.npc_mannequin.storage.MannequinStorage
import org.bukkit.Location
import org.bukkit.entity.Mannequin
import org.bukkit.plugin.java.JavaPlugin
class MannequinRegistry(
private val plugin: JavaPlugin,
private val storage: MannequinStorage,
private val controller: MannequinController
) {
private val records: MutableMap<String, MannequinRecord> = storage.load().toMutableMap()
fun all(): Collection<MannequinRecord> = records.values.sortedBy { it.id }
fun find(id: String): MannequinRecord? = records[id]
fun require(id: String): MannequinRecord =
records[id] ?: error("Mannequin '$id' is not registered.")
fun register(id: String, entity: Mannequin, overwrite: Boolean = false): MannequinRecord {
if (!overwrite && records.containsKey(id)) {
error("Mannequin '$id' already exists. Use overwrite to replace it.")
}
val snapshot = controller.extractSettings(entity)
val record = MannequinRecord(
id = id,
settings = snapshot,
location = StoredLocation.from(entity.location),
entityId = entity.uniqueId
)
records[id] = record
persist()
return record
}
fun create(id: String, location: Location, template: MannequinSettings = MannequinSettings()): MannequinRecord {
if (records.containsKey(id)) {
error("Mannequin '$id' already exists.")
}
val mannequin = controller.spawn(location, template)
val record = MannequinRecord(
id = id,
settings = template,
location = StoredLocation.from(location),
entityId = mannequin.uniqueId
)
records[id] = record
persist()
return record
}
fun updateSettings(id: String, updater: (MannequinSettings) -> MannequinSettings): MannequinRecord {
val existing = require(id)
val nextSettings = updater(existing.settings)
val updated = existing.updateSettings(nextSettings)
records[id] = updated
persist()
controller.locate(updated)?.let { controller.applySettings(it, nextSettings) }
return updated
}
fun apply(id: String, spawnIfMissing: Boolean = true): Mannequin? {
val record = require(id)
var entity = controller.locate(record)
if (entity == null && spawnIfMissing) {
val location = record.location?.toLocation(plugin.server)
?: error("Mannequin '$id' does not have a saved location to respawn.")
entity = controller.spawn(location, record.settings)
}
entity?.let {
controller.applySettings(it, record.settings)
records[id] = record.updateEntityId(it.uniqueId)
persist()
}
return entity
}
fun relocate(id: String, location: Location, teleport: Boolean = true): MannequinRecord {
val record = require(id)
val stored = StoredLocation.from(location)
val updated = record.updateLocation(stored)
val entity = if (teleport) controller.locate(updated) else null
val finalRecord = if (entity != null) {
entity.teleport(location)
updated.updateEntityId(entity.uniqueId)
} else {
updated
}
records[id] = finalRecord
persist()
return finalRecord
}
fun unlink(id: String): MannequinRecord {
val record = require(id)
val updated = record.updateEntityId(null)
records[id] = updated
persist()
return updated
}
fun remove(id: String, deleteEntity: Boolean) {
val record = require(id)
if (deleteEntity) {
controller.locate(record)?.remove()
}
records.remove(id)
persist()
}
fun locate(id: String): Mannequin? = controller.locate(require(id))
private fun persist() {
storage.save(records.values)
}
}

View File

@ -0,0 +1,139 @@
package net.hareworks.npc_mannequin.storage
import net.hareworks.npc_mannequin.mannequin.MannequinHiddenLayer
import net.hareworks.npc_mannequin.mannequin.MannequinRecord
import net.hareworks.npc_mannequin.mannequin.MannequinSettings
import net.hareworks.npc_mannequin.mannequin.StoredLocation
import net.hareworks.npc_mannequin.mannequin.StoredProfile
import net.hareworks.npc_mannequin.mannequin.StoredProfileProperty
import net.hareworks.npc_mannequin.text.TextSerializers
import org.bukkit.configuration.ConfigurationSection
import org.bukkit.configuration.file.YamlConfiguration
import org.bukkit.entity.Pose
import org.bukkit.inventory.MainHand
import org.bukkit.plugin.java.JavaPlugin
import java.io.File
import java.io.IOException
import java.util.UUID
class MannequinStorage(private val plugin: JavaPlugin) {
private val file: File by lazy {
plugin.dataFolder.mkdirs()
File(plugin.dataFolder, "mannequins.yml")
}
fun load(): Map<String, MannequinRecord> {
if (!file.exists()) return emptyMap()
val config = YamlConfiguration()
try {
config.load(file)
} catch (ex: Exception) {
plugin.logger.severe("Failed to load mannequin data: ${ex.message}")
return emptyMap()
}
val records = mutableMapOf<String, MannequinRecord>()
val section = config.getConfigurationSection("mannequins") ?: return emptyMap()
for (key in section.getKeys(false)) {
val record = section.getConfigurationSection(key)?.let { deserializeRecord(key, it) } ?: continue
records[key] = record
}
return records
}
fun save(records: Collection<MannequinRecord>) {
val config = YamlConfiguration()
val root = config.createSection("mannequins")
records.forEach { record ->
val section = root.createSection(record.id)
serializeRecord(section, record)
}
try {
config.save(file)
} catch (ex: IOException) {
plugin.logger.severe("Failed to save mannequin data: ${ex.message}")
}
}
private fun deserializeRecord(id: String, section: ConfigurationSection): MannequinRecord? {
val pose = section.getString("pose")?.let { runCatching { Pose.valueOf(it) }.getOrNull() } ?: Pose.STANDING
val mainHand = section.getString("mainHand")
?.let { runCatching { MainHand.valueOf(it) }.getOrNull() } ?: MainHand.RIGHT
val immovable = section.getBoolean("immovable", false)
val hideDescription = section.getBoolean("hideDescription", false)
val description = section.getString("description")?.let { TextSerializers.miniMessage(it) }
val hiddenLayers = section.getStringList("hiddenLayers")
.mapNotNull { MannequinHiddenLayer.fromKey(it) }
.toSet()
val profileSection = section.getConfigurationSection("profile")
val profile = profileSection?.let(::deserializeProfile)
val locationSection = section.getConfigurationSection("location")
val entityId = section.getString("entityId")?.let { runCatching { UUID.fromString(it) }.getOrNull() }
val location = locationSection?.let {
val world = it.getString("world") ?: return@let null
val x = it.getDouble("x")
val y = it.getDouble("y")
val z = it.getDouble("z")
val yaw = it.getDouble("yaw").toFloat()
val pitch = it.getDouble("pitch").toFloat()
StoredLocation(world, x, y, z, yaw, pitch)
}
val settings = MannequinSettings(
pose = pose,
mainHand = mainHand,
immovable = immovable,
description = description,
hideDescription = hideDescription,
hiddenLayers = hiddenLayers,
profile = profile
)
return MannequinRecord(id, settings, location, entityId)
}
private fun serializeRecord(section: ConfigurationSection, record: MannequinRecord) {
section.set("pose", record.settings.pose.name)
section.set("mainHand", record.settings.mainHand.name)
section.set("immovable", record.settings.immovable)
section.set("hideDescription", record.settings.hideDescription)
section.set("description", TextSerializers.miniMessage(record.settings.description))
section.set("hiddenLayers", record.settings.hiddenLayers.map { it.key })
record.settings.profile?.let { profile ->
val profileSection = section.createSection("profile")
profileSection.set("name", profile.name)
profileSection.set("uuid", profile.uuid?.toString())
val properties = profile.properties.mapIndexed { index, property ->
mapOf(
"name" to property.name,
"value" to property.value,
"signature" to property.signature
)
}
profileSection.set("properties", properties)
}
record.location?.let {
val locationSection = section.createSection("location")
locationSection.set("world", it.world)
locationSection.set("x", it.x)
locationSection.set("y", it.y)
locationSection.set("z", it.z)
locationSection.set("yaw", it.yaw)
locationSection.set("pitch", it.pitch)
}
section.set("entityId", record.entityId?.toString())
}
private fun deserializeProfile(section: ConfigurationSection): StoredProfile? {
val name = section.getString("name")
val uuid = section.getString("uuid")?.let { runCatching { UUID.fromString(it) }.getOrNull() }
val properties = section.getList("properties")
?.filterIsInstance<Map<*, *>>()
?.mapNotNull { entry ->
val propertyName = entry["name"] as? String ?: return@mapNotNull null
val value = entry["value"] as? String ?: return@mapNotNull null
val signature = entry["signature"] as? String
StoredProfileProperty(propertyName, value, signature)
}
?: emptyList()
if (name == null && uuid == null && properties.isEmpty()) return null
return StoredProfile(name, uuid, properties)
}
}

View File

@ -0,0 +1,19 @@
package net.hareworks.npc_mannequin.text
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.minimessage.MiniMessage
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer
object TextSerializers {
private val miniMessage = MiniMessage.miniMessage()
private val plain = PlainTextComponentSerializer.plainText()
fun miniMessage(serialized: String?): Component? =
serialized?.takeIf { it.isNotBlank() }?.let { miniMessage.deserialize(it) }
fun miniMessage(component: Component?): String? =
component?.let { miniMessage.serialize(it) }
fun plain(component: Component?): String =
component?.let { plain.serialize(it) }.orEmpty()
}