feat: マイグレーションを作成

This commit is contained in:
Keisuke Hirata 2025-11-27 03:48:19 +09:00
parent f497ef1ee2
commit 3be9a59370
13 changed files with 211 additions and 79 deletions

1
.gitignore vendored
View File

@ -22,3 +22,4 @@ gradle-app.setting
.classpath .classpath
.direnv .direnv
bin

View File

@ -17,7 +17,7 @@ repositories {
maven("https://repo.codemc.io/repository/maven-public/") maven("https://repo.codemc.io/repository/maven-public/")
} }
dependencies { 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-api:4.17.0")
implementation("net.kyori:adventure-text-minimessage: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")
@ -27,7 +27,7 @@ dependencies {
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion") implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
implementation("org.jetbrains.exposed:exposed-kotlin-datetime:$exposedVersion") implementation("org.jetbrains.exposed:exposed-kotlin-datetime:$exposedVersion")
compileOnly("de.tr7zw:item-nbt-api-plugin:2.14.0") compileOnly("de.tr7zw:item-nbt-api-plugin:2.15.3")
implementation("com.michael-bull.kotlin-result:kotlin-result:2.0.0") implementation("com.michael-bull.kotlin-result:kotlin-result:2.0.0")
} }
tasks { tasks {
@ -42,7 +42,7 @@ bukkit {
name = "Simply-Minecraft-DB" name = "Simply-Minecraft-DB"
description = "It provides a simple way to manage player data through a database." description = "It provides a simple way to manage player data through a database."
version = getVersion().toString() version = getVersion().toString()
apiVersion = "1.21.3" apiVersion = "1.21.10"
authors = authors =
listOf("Hare-K02") listOf("Hare-K02")
depend = listOf("NBTAPI") depend = listOf("NBTAPI")

View File

@ -25,6 +25,7 @@
packages = with pkgs; [ packages = with pkgs; [
jdk21 jdk21
gradle gradle
kotlin
git git
unzip unzip
]; ];

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 distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

34
gradlew vendored
View File

@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (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. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@ -83,10 +85,9 @@ done
# This is normally unused # This is normally unused
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # 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
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. ' "$PWD" ) || exit
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@ -133,10 +134,13 @@ location of your Java installation."
fi fi
else else
JAVACMD=java 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 Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # 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 ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # 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" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@ -197,11 +201,15 @@ if "$cygwin" || "$msys" ; then
done done
fi fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
# shell script including quotes and variable substitutions, so put them in DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded. # 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 -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-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 See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail

View File

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

View File

@ -72,6 +72,91 @@ public val smcdb =
sender.sendMessage("database reset.") 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, _ -> Route("check") { sender, _ ->
sender.sendMM( sender.sendMM(
"${when (App.instance.enabled) { "${when (App.instance.enabled) {

View File

@ -1,24 +1,26 @@
package net.hareworks.simplymcdb package net.hareworks.simplymcdb
import de.tr7zw.nbtapi.NBT import de.tr7zw.nbtapi.NBT
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.util.Base64 import java.util.Base64
import java.util.function.Function import java.util.function.Function
import kotlinx.serialization.SerialName import io.papermc.paper.registry.RegistryAccess
import io.papermc.paper.registry.RegistryKey
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json 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.entity.Player as BukkitPlayer
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
import org.bukkit.potion.PotionEffect import org.bukkit.potion.PotionEffect
import org.bukkit.potion.PotionEffectType import org.bukkit.potion.PotionEffectType
import org.bukkit.util.io.BukkitObjectInputStream
import org.bukkit.util.io.BukkitObjectOutputStream const val PLAYER_DATA_CURRENT_VERSION = 1
@Serializable @Serializable
data class PlayerSnapshot( data class PlayerSnapshot(
val version: Int = CURRENT_VERSION, val version: Int = PLAYER_DATA_CURRENT_VERSION,
val health: Double, val health: Double,
val foodLevel: Int, val foodLevel: Int,
val xpProgress: Float, val xpProgress: Float,
@ -38,15 +40,15 @@ data class PotionEffectSnapshot(
val icon: Boolean val icon: Boolean
) )
@Serializable @Serializable data class ItemStackSnapshot(val payload: String)
data class ItemStackSnapshot(@SerialName("value") val encodedItem: String)
private const val CURRENT_VERSION = 1
private val json = private val json =
Json { Json {
encodeDefaults = true encodeDefaults = true
ignoreUnknownKeys = true ignoreUnknownKeys = true
} }
private val mobEffectRegistry: Registry<PotionEffectType>?
get() = RegistryAccess.registryAccess().getRegistry(RegistryKey.MOB_EFFECT)
object PlayerSerializer { object PlayerSerializer {
fun serialize(player: BukkitPlayer): String { fun serialize(player: BukkitPlayer): String {
@ -67,10 +69,10 @@ object PlayerSerializer {
val snapshot = val snapshot =
try { try {
json.decodeFromString(PlayerSnapshot.serializer(), data) json.decodeFromString(PlayerSnapshot.serializer(), data)
} catch (_: SerializationException) { } catch (ex: SerializationException) {
return LegacySerializer.deserialize(player, data) if (LegacySerializer.deserialize(player, data)) return else throw ex
} catch (_: IllegalArgumentException) { } catch (ex: IllegalArgumentException) {
return LegacySerializer.deserialize(player, data) if (LegacySerializer.deserialize(player, data)) return else throw ex
} }
applySnapshot(player, migrateIfNeeded(snapshot)) applySnapshot(player, migrateIfNeeded(snapshot))
} }
@ -78,7 +80,7 @@ object PlayerSerializer {
private fun migrateIfNeeded(snapshot: PlayerSnapshot): PlayerSnapshot { private fun migrateIfNeeded(snapshot: PlayerSnapshot): PlayerSnapshot {
var current = snapshot var current = snapshot
var version = snapshot.version var version = snapshot.version
while (version < CURRENT_VERSION) { while (version < PLAYER_DATA_CURRENT_VERSION) {
current = migrateOnce(version, current) current = migrateOnce(version, current)
version++ version++
} }
@ -93,18 +95,19 @@ object PlayerSerializer {
} }
private fun applySnapshot(player: BukkitPlayer, snapshot: PlayerSnapshot) { private fun applySnapshot(player: BukkitPlayer, snapshot: PlayerSnapshot) {
player.health = snapshot.health.coerceIn(0.0, player.maxHealth) 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.foodLevel = snapshot.foodLevel.coerceIn(0, 20)
player.exp = snapshot.xpProgress.coerceIn(0f, 1f) player.exp = snapshot.xpProgress.coerceIn(0f, 1f)
player.inventory.heldItemSlot = player.inventory.heldItemSlot =
snapshot.selectedItemSlot.coerceIn(0, player.inventory.contents.size - 1) snapshot.selectedItemSlot.coerceIn(0, player.inventory.contents.size - 1)
val appliedTypes = player.activePotionEffects.map { it.type }.toSet() player.activePotionEffects.forEach { player.removePotionEffect(it.type) }
appliedTypes.forEach { it?.let(player::removePotionEffect) }
snapshot.potionEffects.forEach { eff -> snapshot.potionEffects.forEach { eff ->
val type = PotionEffectType.getByName(eff.type) val typeKey = NamespacedKey.fromString(eff.type)
val type = typeKey?.let { key -> mobEffectRegistry?.get(key) }
if (type == null) { if (type == null) {
App.instance.logger.warning("Unknown potion effect type during restore: ${eff.type}") App.instance.logger.warning("Unknown potion effect key during restore: ${eff.type}")
return@forEach return@forEach
} }
val potion = PotionEffect(type, eff.duration, eff.amplifier, eff.ambient, eff.particles, eff.icon) val potion = PotionEffect(type, eff.duration, eff.amplifier, eff.ambient, eff.particles, eff.icon)
@ -124,8 +127,9 @@ object PlayerSerializer {
} }
private fun serializePotionEffect(effect: PotionEffect): PotionEffectSnapshot { private fun serializePotionEffect(effect: PotionEffect): PotionEffectSnapshot {
val typeKey = effect.type.key().toString()
return PotionEffectSnapshot( return PotionEffectSnapshot(
type = effect.type.name ?: "", type = typeKey,
amplifier = effect.amplifier, amplifier = effect.amplifier,
duration = effect.duration, duration = effect.duration,
ambient = effect.isAmbient, ambient = effect.isAmbient,
@ -135,42 +139,41 @@ private fun serializePotionEffect(effect: PotionEffect): PotionEffectSnapshot {
} }
private fun serializeItemStack(item: ItemStack): ItemStackSnapshot { private fun serializeItemStack(item: ItemStack): ItemStackSnapshot {
val byteArray = val bytes = item.ensureServerConversions().serializeAsBytes()
ByteArrayOutputStream().use { byteStream -> return ItemStackSnapshot(Base64.getEncoder().encodeToString(bytes))
BukkitObjectOutputStream(byteStream).use { out -> out.writeObject(item) }
byteStream.toByteArray()
}
return ItemStackSnapshot(Base64.getEncoder().encodeToString(byteArray))
} }
private fun deserializeItemStack(snapshot: ItemStackSnapshot): ItemStack { private fun deserializeItemStack(snapshot: ItemStackSnapshot): ItemStack {
val data = Base64.getDecoder().decode(snapshot.encodedItem) val data = Base64.getDecoder().decode(snapshot.payload)
return ByteArrayInputStream(data).use { byteStream -> return ItemStack.deserializeBytes(data)
BukkitObjectInputStream(byteStream).use { input -> input.readObject() as ItemStack }
}
} }
private object LegacySerializer { private object LegacySerializer {
fun deserialize(player: BukkitPlayer, data: String) { fun deserialize(player: BukkitPlayer, data: String): Boolean {
NBT.modify( return try {
player, NBT.modify(
Function { nbt -> player,
val input = NBT.parseNBT(data) Function { nbt ->
nbt.setFloat("Health", input.getFloat("Health")) val input = NBT.parseNBT(data)
nbt.setInteger("foodLevel", input.getInteger("foodLevel")) nbt.setFloat("Health", input.getFloat("Health"))
nbt.setFloat("XpP", input.getFloat("XpP")) nbt.setInteger("foodLevel", input.getInteger("foodLevel"))
nbt.setInteger("SelectedItemSlot", input.getInteger("SelectedItemSlot")) nbt.setFloat("XpP", input.getFloat("XpP"))
val activeEffects = nbt.getCompoundList("active_effects") nbt.setInteger("SelectedItemSlot", input.getInteger("SelectedItemSlot"))
activeEffects.clear() val activeEffects = nbt.getCompoundList("active_effects")
input.getCompoundList("active_effects").forEach { activeEffects.addCompound(it) } activeEffects.clear()
val inventory = nbt.getCompoundList("Inventory") input.getCompoundList("active_effects").forEach { activeEffects.addCompound(it) }
inventory.clear() val inventory = nbt.getCompoundList("Inventory")
input.getCompoundList("Inventory").forEach { inventory.addCompound(it) } inventory.clear()
val enderchest = nbt.getCompoundList("EnderItems") input.getCompoundList("Inventory").forEach { inventory.addCompound(it) }
enderchest.clear() val enderchest = nbt.getCompoundList("EnderItems")
input.getCompoundList("EnderItems").forEach { enderchest.addCompound(it) } enderchest.clear()
} input.getCompoundList("EnderItems").forEach { enderchest.addCompound(it) }
) }
App.instance.logger.info("Legacy player data applied; will be migrated on next save.") )
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 lastIp = varchar("last_ip", 15)
val data = text("data").default("") val data = text("data").default("")
val dataVersion = integer("data_version").default(0)
override val primaryKey = PrimaryKey(uuid) override val primaryKey = PrimaryKey(uuid)
} }
@ -39,6 +40,7 @@ public fun register(player: BukkitPlayer) {
it[firstLogin] = System.currentTimeMillis() it[firstLogin] = System.currentTimeMillis()
it[lastOnline] = System.currentTimeMillis() it[lastOnline] = System.currentTimeMillis()
it[lastIp] = player.address?.address?.hostAddress ?: "unknown" it[lastIp] = player.address?.address?.hostAddress ?: "unknown"
it[dataVersion] = 0
} }
} }
} }
@ -52,6 +54,7 @@ public fun update(player: BukkitPlayer) {
// player.sendMessage(dat) // player.sendMessage(dat)
it[data] = dat it[data] = dat
it[dataVersion] = PLAYER_DATA_CURRENT_VERSION
} }
} }
} }
@ -68,3 +71,25 @@ public fun fetch(player: BukkitPlayer) {
// player.sendMessage(dat) // player.sendMessage(dat)
PlayerSerializer.deserialize(player, 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 if (instance == null) return
App.instance.logger.info("Database connected: $host:$port/$database") App.instance.logger.info("Database connected: $host:$port/$database")
transaction(instance) { SchemaUtils.createMissingTablesAndColumns(Players) }
} }
public fun disconnect() { public fun disconnect() {
instance?.let { instance?.let {