Compare commits

..

3 Commits

Author SHA1 Message Date
f61c95f3ab 1.1 2025-11-27 07:40:32 +09:00
3be9a59370 feat: マイグレーションを作成 2025-11-27 03:48:19 +09:00
f497ef1ee2 feat: NTBAPI非依存シリアライズ・デシリアライズ 2025-11-26 23:02:43 +09:00
17 changed files with 444 additions and 94 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake

2
.gitignore vendored
View File

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

View File

@ -1,7 +1,7 @@
import net.minecrell.pluginyml.bukkit.BukkitPluginDescription
group = "net.hareworks"
version = "1.0"
version = "1.1"
val exposedVersion = "0.54.0"
@ -9,7 +9,7 @@ plugins {
kotlin("jvm") version "2.0.20"
kotlin("plugin.serialization") version "2.0.20"
id("net.minecrell.plugin-yml.bukkit") version "0.6.0"
id("com.github.johnrengelman.shadow") version "8.1.1"
id("com.gradleup.shadow") version "9.2.2"
}
repositories {
mavenCentral()
@ -17,23 +17,28 @@ repositories {
maven("https://repo.codemc.io/repository/maven-public/")
}
dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.3-R0.1-SNAPSHOT")
compileOnly("io.papermc.paper:paper-api:1.21.10-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")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
implementation("org.jetbrains.exposed:exposed-kotlin-datetime:$exposedVersion")
compileOnly("de.tr7zw:item-nbt-api-plugin:2.14.0")
implementation("de.tr7zw:item-nbt-api:2.15.3")
implementation("com.michael-bull.kotlin-result:kotlin-result:2.0.0")
}
tasks {
shadowJar {
archiveBaseName.set("SimplyMCDB")
archiveClassifier.set("")
relocate("de.tr7zw.changeme.nbtapi", "net.hareworks.simplymcdb.libs.nbtapi")
}
build {
dependsOn(shadowJar)
}
}
@ -42,10 +47,9 @@ bukkit {
name = "Simply-Minecraft-DB"
description = "It provides a simple way to manage player data through a database."
version = getVersion().toString()
apiVersion = "1.21.3"
apiVersion = "1.21.10"
authors =
listOf("Hare-K02")
depend = listOf("NBTAPI")
permissions {
register("simplydb.*") {
children = listOf("simplydb.command", "simplydb.admin")

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
}

47
flake.nix Normal file
View File

@ -0,0 +1,47 @@
{
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
kotlin
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

@ -0,0 +1,2 @@
#This file is generated by updateDaemonJvm
toolchainVersion=21

Binary file not shown.

View File

@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

34
gradlew vendored
View File

@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -83,10 +85,9 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -133,10 +134,13 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@ -197,11 +201,15 @@ if "$cygwin" || "$msys" ; then
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \

22
gradlew.bat vendored
View File

@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail

View File

@ -18,10 +18,12 @@ public class App : JavaPlugin() {
field = value
Config.config.set("enabled", !value.equals(State.DISABLED))
}
companion object {
lateinit var instance: App
private set
}
lateinit var command: KommandLib
private set
@ -34,6 +36,7 @@ public class App : JavaPlugin() {
if (Config.check()) enable()
}
override fun onDisable() {
enabled = State.DISABLED
Database.disconnect()

View File

@ -72,6 +72,91 @@ public val smcdb =
sender.sendMessage("database reset.")
},
),
Route("migrate") { sender, _ ->
if (sender !is Player) {
sender.sendMM("<red>[SMCDB] This command can only be run by players.")
return@Route
}
when (App.instance.enabled) {
State.DISABLED -> {
sender.sendMM("<red>[SMCDB] simplymcdb is disabled.")
return@Route
}
State.DISCONNECTED -> {
sender.sendMM("<yellow>[SMCDB] Database disconnected. Try again later.")
return@Route
}
else -> {}
}
if (!isRegistered(sender.uniqueId)) {
sender.sendMM("<red>[SMCDB] You are not registered in the database.")
return@Route
}
try {
sender.sendMM("<gray>[SMCDB] Applying legacy data...")
fetch(sender)
update(sender)
sender.sendMM("<green>[SMCDB] Migration complete. Data updated to the latest format.")
} catch (e: Exception) {
App.instance.logger.warning("Failed to migrate data for ${sender.uniqueId}: ${e.message}")
sender.sendMM("<red>[SMCDB] Migration failed. Check server logs.")
}
}.addArgs(
Route("all") { sender, _ ->
if (sender !is Player) {
sender.sendMM("<red>[SMCDB] This command can only be run by players.")
return@Route
}
when (App.instance.enabled) {
State.DISABLED -> {
sender.sendMM("<red>[SMCDB] simplymcdb is disabled.")
return@Route
}
State.DISCONNECTED -> {
sender.sendMM(
"<yellow>[SMCDB] Database disconnected. Try again later."
)
return@Route
}
else -> {}
}
val targets = findPlayersNeedingMigration()
if (targets.isEmpty()) {
sender.sendMM("<gray>[SMCDB] No legacy data found.")
return@Route
}
sender.sendMM(
"<gray>[SMCDB] Migrating ${targets.size} legacy profiles... Please wait."
)
val backup = PlayerSerializer.serialize(sender)
var migrated = 0
try {
targets.forEach { entry ->
try {
PlayerSerializer.deserialize(sender, entry.serialized)
val updatedSnapshot = PlayerSerializer.serialize(sender)
overwritePlayerData(entry.uuid, updatedSnapshot)
migrated++
} catch (ex: Exception) {
App.instance.logger.warning(
"Failed to migrate data for ${entry.uuid}: ${ex.message}"
)
}
}
} finally {
try {
PlayerSerializer.deserialize(sender, backup)
} catch (restoreEx: Exception) {
App.instance.logger.warning(
"Failed to restore migration executor state: ${restoreEx.message}"
)
}
}
sender.sendMM(
"<green>[SMCDB] Migration finished ($migrated/${targets.size}). Check logs for failures."
)
}
),
Route("check") { sender, _ ->
sender.sendMM(
"${when (App.instance.enabled) {

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,68 +1,179 @@
package net.hareworks.simplymcdb
import de.tr7zw.nbtapi.NBT
import de.tr7zw.changeme.nbtapi.NBT
import java.util.Base64
import java.util.function.Function
import io.papermc.paper.registry.RegistryAccess
import io.papermc.paper.registry.RegistryKey
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import org.bukkit.NamespacedKey
import org.bukkit.Registry
import org.bukkit.attribute.Attribute
import org.bukkit.entity.Player as BukkitPlayer
import org.bukkit.inventory.ItemStack
import org.bukkit.potion.PotionEffect
import org.bukkit.potion.PotionEffectType
data class PlayerData(
val health: Float,
val hunger: Int,
val exp: Float,
val effects: String,
val inv: String,
val enderchest: String
const val PLAYER_DATA_CURRENT_VERSION = 1
@Serializable
data class PlayerSnapshot(
val version: Int = PLAYER_DATA_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(val payload: String)
private val json =
Json {
encodeDefaults = true
ignoreUnknownKeys = true
}
private val mobEffectRegistry: Registry<PotionEffectType>?
get() = RegistryAccess.registryAccess().getRegistry(RegistryKey.MOB_EFFECT)
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) {
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 inventory = nbt.getCompoundList("Inventory")
inventory.clear()
input.getCompoundList("Inventory").forEach { inventory.addCompound(it) }
val enderchest = nbt.getCompoundList("EnderItems")
enderchest.clear()
input.getCompoundList("EnderItems").forEach { enderchest.addCompound(it) }
val snapshot =
try {
json.decodeFromString(PlayerSnapshot.serializer(), data)
} catch (ex: SerializationException) {
if (LegacySerializer.deserialize(player, data)) return else throw ex
} catch (ex: IllegalArgumentException) {
if (LegacySerializer.deserialize(player, data)) return else throw ex
}
)
applySnapshot(player, migrateIfNeeded(snapshot))
}
private fun migrateIfNeeded(snapshot: PlayerSnapshot): PlayerSnapshot {
var current = snapshot
var version = snapshot.version
while (version < PLAYER_DATA_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) {
val maxHealth = player.getAttribute(Attribute.MAX_HEALTH)?.value ?: player.health
player.health = snapshot.health.coerceIn(0.0, 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)
player.activePotionEffects.forEach { player.removePotionEffect(it.type) }
snapshot.potionEffects.forEach { eff ->
val typeKey = NamespacedKey.fromString(eff.type)
val type = typeKey?.let { key -> mobEffectRegistry?.get(key) }
if (type == null) {
App.instance.logger.warning("Unknown potion effect key 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 {
val typeKey = effect.type.key().toString()
return PotionEffectSnapshot(
type = typeKey,
amplifier = effect.amplifier,
duration = effect.duration,
ambient = effect.isAmbient,
particles = effect.hasParticles(),
icon = effect.hasIcon()
)
}
private fun serializeItemStack(item: ItemStack): ItemStackSnapshot {
val bytes = item.ensureServerConversions().serializeAsBytes()
return ItemStackSnapshot(Base64.getEncoder().encodeToString(bytes))
}
private fun deserializeItemStack(snapshot: ItemStackSnapshot): ItemStack {
val data = Base64.getDecoder().decode(snapshot.payload)
return ItemStack.deserializeBytes(data)
}
private object LegacySerializer {
fun deserialize(player: BukkitPlayer, data: String): Boolean {
return try {
NBT.modify(
player,
Function { nbt ->
val input = NBT.parseNBT(data)
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 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) }
val enderchest = nbt.getCompoundList("EnderItems")
enderchest.clear()
input.getCompoundList("EnderItems").forEach { enderchest.addCompound(it) }
}
)
App.instance.logger.info("Legacy player data applied; will be migrated on next save.")
true
} catch (_: Exception) {
false
}
}
}

View File

@ -17,6 +17,7 @@ public object Players : Table() {
val lastIp = varchar("last_ip", 15)
val data = text("data").default("")
val dataVersion = integer("data_version").default(0)
override val primaryKey = PrimaryKey(uuid)
}
@ -39,6 +40,7 @@ public fun register(player: BukkitPlayer) {
it[firstLogin] = System.currentTimeMillis()
it[lastOnline] = System.currentTimeMillis()
it[lastIp] = player.address?.address?.hostAddress ?: "unknown"
it[dataVersion] = 0
}
}
}
@ -52,6 +54,7 @@ public fun update(player: BukkitPlayer) {
// player.sendMessage(dat)
it[data] = dat
it[dataVersion] = PLAYER_DATA_CURRENT_VERSION
}
}
}
@ -68,3 +71,25 @@ public fun fetch(player: BukkitPlayer) {
// player.sendMessage(dat)
PlayerSerializer.deserialize(player, dat)
}
data class PlayerDataEntry(val uuid: UUID, val serialized: String, val version: Int)
public fun findPlayersNeedingMigration(): List<PlayerDataEntry> {
return transaction(Database.instance) {
Players
.selectAll()
.where { (Players.dataVersion less PLAYER_DATA_CURRENT_VERSION) and (Players.data neq "") }
.map {
PlayerDataEntry(UUID.fromString(it[Players.uuid]), it[Players.data], it[Players.dataVersion])
}
}
}
public fun overwritePlayerData(uuid: UUID, data: String, version: Int = PLAYER_DATA_CURRENT_VERSION) {
transaction(Database.instance) {
Players.update({ Players.uuid eq uuid.toString() }) {
it[Players.data] = data
it[Players.dataVersion] = version
}
}
}

View File

@ -51,6 +51,7 @@ public object Database {
}
if (instance == null) return
App.instance.logger.info("Database connected: $host:$port/$database")
transaction(instance) { SchemaUtils.createMissingTablesAndColumns(Players) }
}
public fun disconnect() {
instance?.let {