From f497ef1ee20e718be8ad0229174b9757ee50a362 Mon Sep 17 00:00:00 2001 From: Hare Date: Wed, 26 Nov 2025 23:02:43 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20NTBAPI=E9=9D=9E=E4=BE=9D=E5=AD=98?= =?UTF-8?q?=E3=82=B7=E3=83=AA=E3=82=A2=E3=83=A9=E3=82=A4=E3=82=BA=E3=83=BB?= =?UTF-8?q?=E3=83=87=E3=82=B7=E3=83=AA=E3=82=A2=E3=83=A9=E3=82=A4=E3=82=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .envrc | 1 + .gitignore | 1 + build.gradle.kts | 2 +- flake.lock | 61 ++++++ flake.nix | 46 +++++ .../kotlin/net/hareworks/simplymcdb/Event.kt | 19 +- .../hareworks/simplymcdb/PlayerSerializer.kt | 180 ++++++++++++++---- 7 files changed, 262 insertions(+), 48 deletions(-) create mode 100644 .envrc create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index c72d2ea..916be9d 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ gradle-app.setting # JDT-specific (Eclipse Java Development Tools) .classpath +.direnv \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 6291811..17de1e8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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") diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..742cdd6 --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..16e047d --- /dev/null +++ b/flake.nix @@ -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 + ''; + }; + } + ); +} diff --git a/src/main/kotlin/net/hareworks/simplymcdb/Event.kt b/src/main/kotlin/net/hareworks/simplymcdb/Event.kt index fb10fbe..a4dbcb6 100644 --- a/src/main/kotlin/net/hareworks/simplymcdb/Event.kt +++ b/src/main/kotlin/net/hareworks/simplymcdb/Event.kt @@ -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( - "[SMCDB] Welcome, ${event.player.name}." + - "SMCDB is active but you have already played before." + - "Run /smcdb register to register yourself." - ) + else if (event.player.hasPlayedBefore()) register(event.player) + else event.player.sendMessage( + MiniMessage.miniMessage() + .deserialize( + "[SMCDB] Welcome, ${event.player.name}." + + "SMCDB is active but you have already played before." + + "Run /smcdb register to register yourself." ) - } + ) } @EventHandler diff --git a/src/main/kotlin/net/hareworks/simplymcdb/PlayerSerializer.kt b/src/main/kotlin/net/hareworks/simplymcdb/PlayerSerializer.kt index 23c3ae8..4613528 100644 --- a/src/main/kotlin/net/hareworks/simplymcdb/PlayerSerializer.kt +++ b/src/main/kotlin/net/hareworks/simplymcdb/PlayerSerializer.kt @@ -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, + val inventory: List, + val enderChest: List ) +@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.") } }