feat: NTBAPI非依存シリアライズ・デシリアライズ

This commit is contained in:
Keisuke Hirata 2025-11-26 23:02:43 +09:00
parent 5f805ad0a7
commit f497ef1ee2
7 changed files with 262 additions and 48 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake

1
.gitignore vendored
View File

@ -21,3 +21,4 @@ gradle-app.setting
# JDT-specific (Eclipse Java Development Tools)
.classpath
.direnv

View File

@ -20,7 +20,7 @@ dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.3-R0.1-SNAPSHOT")
implementation("net.kyori:adventure-api:4.17.0")
implementation("net.kyori:adventure-text-minimessage:4.17.0")
// implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
implementation("org.postgresql:postgresql:42.7.1")
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")

61
flake.lock Normal file
View File

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1764020296,
"narHash": "sha256-6zddwDs2n+n01l+1TG6PlyokDdXzu/oBmEejcH5L5+A=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a320ce8e6e2cc6b4397eef214d202a50a4583829",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

46
flake.nix Normal file
View File

@ -0,0 +1,46 @@
{
description = "Minecraft dev environment with JDK 21 and Gradle";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{
self,
nixpkgs,
flake-utils,
...
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs {
inherit system;
};
in
{
devShells.default = pkgs.mkShell {
packages = with pkgs; [
jdk21
gradle
git
unzip
];
# 必要に応じて環境変数を設定
shellHook = ''
export JAVA_HOME=${pkgs.jdk21}/lib/openjdk
export PATH="$JAVA_HOME/bin:$PATH"
export GRADLE_USER_HOME="$PWD/.gradle"
echo "Loaded Minecraft dev env (JDK 21 + Gradle)"
java -version || true
gradle --version || true
'';
};
}
);
}

View File

@ -46,18 +46,15 @@ public object EventListener : Listener {
| not | confirm register |
+------------+------------------*/
if (isRegistered(event.player.uniqueId)) fetch(event.player)
else if (event.player.hasPlayedBefore()) {
register(event.player)
} else {
event.player.sendMessage(
MiniMessage.miniMessage()
.deserialize(
"<gray>[SMCDB] Welcome, ${event.player.name}.<newline>" +
"SMCDB is active but you have already played before.<newline>" +
"Run <green>/smcdb register<gray> to register yourself."
)
else if (event.player.hasPlayedBefore()) register(event.player)
else event.player.sendMessage(
MiniMessage.miniMessage()
.deserialize(
"<gray>[SMCDB] Welcome, ${event.player.name}.<newline>" +
"SMCDB is active but you have already played before.<newline>" +
"Run <green>/smcdb register<gray> to register yourself."
)
}
)
}
@EventHandler

View File

@ -1,61 +1,168 @@
package net.hareworks.simplymcdb
import de.tr7zw.nbtapi.NBT
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.util.Base64
import java.util.function.Function
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import org.bukkit.entity.Player as BukkitPlayer
import org.bukkit.inventory.ItemStack
import org.bukkit.potion.PotionEffect
import org.bukkit.potion.PotionEffectType
import org.bukkit.util.io.BukkitObjectInputStream
import org.bukkit.util.io.BukkitObjectOutputStream
data class PlayerData(
val health: Float,
val hunger: Int,
val exp: Float,
val effects: String,
val inv: String,
val enderchest: String
@Serializable
data class PlayerSnapshot(
val version: Int = CURRENT_VERSION,
val health: Double,
val foodLevel: Int,
val xpProgress: Float,
val selectedItemSlot: Int,
val potionEffects: List<PotionEffectSnapshot>,
val inventory: List<ItemStackSnapshot?>,
val enderChest: List<ItemStackSnapshot?>
)
@Serializable
data class PotionEffectSnapshot(
val type: String,
val amplifier: Int,
val duration: Int,
val ambient: Boolean,
val particles: Boolean,
val icon: Boolean
)
@Serializable
data class ItemStackSnapshot(@SerialName("value") val encodedItem: String)
private const val CURRENT_VERSION = 1
private val json =
Json {
encodeDefaults = true
ignoreUnknownKeys = true
}
object PlayerSerializer {
fun serialize(player: BukkitPlayer): String {
val result =
NBT.get(
player,
Function { nbt ->
var output = NBT.createNBTObject()
output.setFloat("Health", player.health.toFloat())
output.setInteger("foodLevel", player.foodLevel)
output.setFloat("XpP", player.exp)
output.setInteger("SelectedItemSlot", player.inventory.heldItemSlot)
var active_effects = output.getCompoundList("active_effects")
nbt.getCompoundList("active_effects").forEach {
active_effects.addCompound(it)
}
var inventory = output.getCompoundList("Inventory")
nbt.getCompoundList("Inventory").forEach { inventory.addCompound(it) }
var enderchest = output.getCompoundList("EnderItems")
nbt.getCompoundList("EnderItems").forEach { enderchest.addCompound(it) }
output.toString()
}
val snapshot =
PlayerSnapshot(
health = player.health,
foodLevel = player.foodLevel,
xpProgress = player.exp,
selectedItemSlot = player.inventory.heldItemSlot,
potionEffects = player.activePotionEffects.map(::serializePotionEffect),
inventory = player.inventory.contents.map { it?.let(::serializeItemStack) },
enderChest = player.enderChest.contents.map { it?.let(::serializeItemStack) }
)
return result
return json.encodeToString(PlayerSnapshot.serializer(), snapshot)
}
fun deserialize(player: BukkitPlayer, data: String) {
val snapshot =
try {
json.decodeFromString(PlayerSnapshot.serializer(), data)
} catch (_: SerializationException) {
return LegacySerializer.deserialize(player, data)
} catch (_: IllegalArgumentException) {
return LegacySerializer.deserialize(player, data)
}
applySnapshot(player, migrateIfNeeded(snapshot))
}
private fun migrateIfNeeded(snapshot: PlayerSnapshot): PlayerSnapshot {
var current = snapshot
var version = snapshot.version
while (version < CURRENT_VERSION) {
current = migrateOnce(version, current)
version++
}
return current
}
private fun migrateOnce(fromVersion: Int, snapshot: PlayerSnapshot): PlayerSnapshot {
return when (fromVersion) {
1 -> snapshot
else -> snapshot
}
}
private fun applySnapshot(player: BukkitPlayer, snapshot: PlayerSnapshot) {
player.health = snapshot.health.coerceIn(0.0, player.maxHealth)
player.foodLevel = snapshot.foodLevel.coerceIn(0, 20)
player.exp = snapshot.xpProgress.coerceIn(0f, 1f)
player.inventory.heldItemSlot =
snapshot.selectedItemSlot.coerceIn(0, player.inventory.contents.size - 1)
val appliedTypes = player.activePotionEffects.map { it.type }.toSet()
appliedTypes.forEach { it?.let(player::removePotionEffect) }
snapshot.potionEffects.forEach { eff ->
val type = PotionEffectType.getByName(eff.type)
if (type == null) {
App.instance.logger.warning("Unknown potion effect type during restore: ${eff.type}")
return@forEach
}
val potion = PotionEffect(type, eff.duration, eff.amplifier, eff.ambient, eff.particles, eff.icon)
player.addPotionEffect(potion)
}
player.inventory.clear()
snapshot.inventory.forEachIndexed { index, item ->
player.inventory.setItem(index, item?.let(::deserializeItemStack))
}
player.enderChest.clear()
snapshot.enderChest.forEachIndexed { index, item ->
player.enderChest.setItem(index, item?.let(::deserializeItemStack))
}
}
}
private fun serializePotionEffect(effect: PotionEffect): PotionEffectSnapshot {
return PotionEffectSnapshot(
type = effect.type.name ?: "",
amplifier = effect.amplifier,
duration = effect.duration,
ambient = effect.isAmbient,
particles = effect.hasParticles(),
icon = effect.hasIcon()
)
}
private fun serializeItemStack(item: ItemStack): ItemStackSnapshot {
val byteArray =
ByteArrayOutputStream().use { byteStream ->
BukkitObjectOutputStream(byteStream).use { out -> out.writeObject(item) }
byteStream.toByteArray()
}
return ItemStackSnapshot(Base64.getEncoder().encodeToString(byteArray))
}
private fun deserializeItemStack(snapshot: ItemStackSnapshot): ItemStack {
val data = Base64.getDecoder().decode(snapshot.encodedItem)
return ByteArrayInputStream(data).use { byteStream ->
BukkitObjectInputStream(byteStream).use { input -> input.readObject() as ItemStack }
}
}
private object LegacySerializer {
fun deserialize(player: BukkitPlayer, data: String) {
NBT.modify(
player,
Function { nbt ->
val input = NBT.parseNBT(data)
App.instance.logger.info("Deserializing player data: $data")
App.instance.logger.info("deserialized: ${input.toString()}")
nbt.setFloat("Health", input.getFloat("Health"))
nbt.setInteger("foodLevel", input.getInteger("foodLevel"))
nbt.setFloat("XpP", input.getFloat("XpP"))
nbt.setInteger("SelectedItemSlot", input.getInteger("SelectedItemSlot"))
val active_effects = nbt.getCompoundList("active_effects")
active_effects.clear()
input.getCompoundList("active_effects").forEach { active_effects.addCompound(it) }
val activeEffects = nbt.getCompoundList("active_effects")
activeEffects.clear()
input.getCompoundList("active_effects").forEach { activeEffects.addCompound(it) }
val inventory = nbt.getCompoundList("Inventory")
inventory.clear()
input.getCompoundList("Inventory").forEach { inventory.addCompound(it) }
@ -64,5 +171,6 @@ object PlayerSerializer {
input.getCompoundList("EnderItems").forEach { enderchest.addCompound(it) }
}
)
App.instance.logger.info("Legacy player data applied; will be migrated on next save.")
}
}