Compare commits

...
6 Commits
Author SHA1 Message Date
Hare ae5bc8a0f0 feat: 依存関係のimplementation、コマンド実行時の非同期化 2025-11-30 18:16:37 +09:00
Hare 47e202a613 chore: Gradleのアップデートとビルド設定の変更
- gradleをアップデート
- 依存関係をリモート化し、ファイルシステム上の依存を削除
2025-11-29 04:39:15 +09:00
Hare 302575fb29 1.2
- kommand-libの外部化
- ビルド設定の見直し
2025-11-28 09:03:48 +09:00
Hare f61c95f3ab 1.1 2025-11-27 07:40:32 +09:00
Hare 3be9a59370 feat: マイグレーションを作成 2025-11-27 03:48:19 +09:00
Hare f497ef1ee2 feat: NTBAPI非依存シリアライズ・デシリアライズ 2025-11-26 23:02:43 +09:00
21 changed files with 665 additions and 526 deletions
+1
View File
@@ -0,0 +1 @@
use flake
+3 -22
View File
@@ -1,23 +1,4 @@
# ---> Gradle .direnv
.kotlin
.gradle .gradle
**/build/ build
!src/**/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Avoid ignore Gradle wrappper properties
!gradle-wrapper.properties
# Cache of project
.gradletasknamecache
# Eclipse Gradle plugin generated files
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
+4
View File
@@ -0,0 +1,4 @@
[submodule "kommand-lib"]
path = kommand-lib
url = git@gitea.hareworks.net:Hare/kommand-lib.git
branch=master
+30 -70
View File
@@ -1,101 +1,61 @@
import net.minecrell.pluginyml.bukkit.BukkitPluginDescription import net.minecrell.pluginyml.paper.PaperPluginDescription
group = "net.hareworks" group = "net.hareworks"
version = "1.0" version = "1.2"
val exposedVersion = "0.54.0"
plugins { plugins {
kotlin("jvm") version "2.0.20" kotlin("jvm") version "2.2.21"
kotlin("plugin.serialization") version "2.0.20" kotlin("plugin.serialization") version "2.2.21"
id("net.minecrell.plugin-yml.bukkit") version "0.6.0" id("de.eldoria.plugin-yml.paper") version "0.8.0"
id("com.github.johnrengelman.shadow") version "8.1.1" id("com.gradleup.shadow") version "9.2.2"
} }
repositories { repositories {
mavenCentral() mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/") maven("https://repo.papermc.io/repository/maven-public/")
maven("https://repo.codemc.io/repository/maven-public/") maven("https://repo.codemc.io/repository/maven-public/")
} }
val exposedVersion = "0.54.0"
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("org.jetbrains.kotlin:kotlin-stdlib")
implementation("net.hareworks:kommand-lib:1.1")
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")
implementation("org.postgresql:postgresql:42.7.1") implementation("org.postgresql:postgresql:42.7.1")
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion") implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion") implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
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") implementation("de.tr7zw:item-nbt-api:2.15.3")
implementation("com.michael-bull.kotlin-result:kotlin-result:2.0.0")
} }
tasks { tasks {
shadowJar { withType<Jar> {
archiveBaseName.set("SimplyMCDB") archiveBaseName.set("SimplyMCDB")
archiveClassifier.set("") }
shadowJar {
minimize {
exclude(dependency("org.jetbrains.exposed:exposed-core"))
exclude(dependency("org.jetbrains.exposed:exposed-dao"))
exclude(dependency("org.jetbrains.exposed:exposed-jdbc"))
exclude(dependency("org.jetbrains.exposed:exposed-kotlin-datetime"))
exclude(dependency("org.postgresql:postgresql"))
}
archiveClassifier.set("min")
relocate("de.tr7zw.changeme.nbtapi", "net.hareworks.simplymcdb.libs.nbtapi")
} }
} }
bukkit { paper {
main = "net.hareworks.simplymcdb.App" main = "net.hareworks.simplymcdb.App"
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(
listOf("Hare-K02") "Hare-K02"
depend = listOf("NBTAPI") )
permissions {
register("simplydb.*") {
children = listOf("simplydb.command", "simplydb.admin")
}
register("simplydb.command") {
description = "Allows access to the /simplydb command"
default = BukkitPluginDescription.Permission.Default.TRUE
}
register("simplydb.command.*") {
children = listOf(
"simplydb.command.config",
"simplydb.command.config.*",
"simplydb.command.on",
"simplydb.command.off",
)
}
register("simplydb.command.config") {
description = "Allows access to the /simplydb config command"
default = BukkitPluginDescription.Permission.Default.OP
}
register("simplydb.command.config.*") {
children = listOf(
"simplydb.command.config.reload",
"simplydb.command.config.fetch",
"simplydb.command.config.upload",
)
}
register("simplydb.command.config.reload") {
description = "Allows access to the /simplydb config reload command"
default = BukkitPluginDescription.Permission.Default.OP
}
register("simplydb.command.config.fetch") {
description = "Allows access to the /simplydb config fetch command"
default = BukkitPluginDescription.Permission.Default.OP
}
register("simplydb.command.config.upload") {
description = "Allows access to the /simplydb config upload command"
default = BukkitPluginDescription.Permission.Default.OP
}
register("simplydb.command.on") {
description = "Allows access to the /simplydb on command"
default = BukkitPluginDescription.Permission.Default.OP
}
register("simplydb.command.off") {
description = "Allows access to the /simplydb off command"
default = BukkitPluginDescription.Permission.Default.OP
}
register("simplydb.admin") {
description = "Allows configration/manage simplydb"
default = BukkitPluginDescription.Permission.Default.OP
}
}
} }
Generated
+61
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": 1764173365,
"narHash": "sha256-JaNFPy3nywPNxSDpEgFFqvngQww5Igb6twG4NhMo8oc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2fecba9952096ba043c16b9ef40b92851ff3e5d9",
"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
}
+43
View File
@@ -0,0 +1,43 @@
{
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"
'';
};
}
);
}
+5
View File
@@ -0,0 +1,5 @@
org.gradle.configuration-cache=true
org.gradle.parallel=true
org.gradle.caching=true
kotlin.stdlib.default.dependency=false
+2
View File
@@ -0,0 +1,2 @@
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
Binary file not shown.
+2 -1
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.3-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
Vendored
+22 -15
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,8 @@ 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\n' "$PWD" ) || 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"'
# 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
@@ -114,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@@ -133,10 +133,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 +147,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 +155,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,16 +200,20 @@ 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, 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" \
-classpath "$CLASSPATH" \ -classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.
Vendored
+14 -12
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,22 +59,22 @@ 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
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
+3
View File
@@ -0,0 +1,3 @@
rootProject.name = "simply-mcdb"
includeBuild("kommand-lib")
@@ -1,147 +0,0 @@
package net.hareworks.kommandlib
import com.github.michaelbull.result.*
import kotlin.collections.listOf
import org.bukkit.Bukkit
import org.bukkit.command.CommandMap
import org.bukkit.command.CommandSender
import org.bukkit.command.PluginCommand
import org.bukkit.command.TabCompleter
import org.bukkit.plugin.java.JavaPlugin
import net.hareworks.simplymcdb.App
class KommandLib(plugin: JavaPlugin, vararg routes: Argument) {
val entries = routes.toList()
init {
val f = Bukkit.getServer().javaClass.getDeclaredField("commandMap")
f.isAccessible = true
val commandMap = f.get(Bukkit.getServer()) as CommandMap
for (route in routes) {
commandMap.register(
plugin.getName(),
(PluginCommand::class
.java
.declaredConstructors
.first()
.apply { isAccessible = true }
.newInstance(route.name, plugin) as
PluginCommand)
.apply {
this.name = name
this.setExecutor { sender, _, alias, args ->
val routeargs = routeTreeSearch(arrayOf(alias, *args))
if (routeargs.size == args.size + 1)
routeargs.last().onCommand(sender, args)
true
}
this.tabCompleter = TabCompleter { sender, _, alias, args ->
val routeargs = routeTreeSearch(arrayOf(alias, *args))
if (routeargs.size == args.size)
routeargs.last().getCompletList(sender, args)
else listOf()
}
}
)
}
}
fun routeTreeSearch(args: Array<String>): List<Argument> {
val list =
mutableListOf<Argument>(
entries.find { it.name == args[0] } ?: throw Exception("Invalid command")
)
var i = 1
while (i + 1 <= args.size) {
if (list.last().routes.isEmpty()) break
val route = list.last().routes.sortedBy { it.priority }.find { it.name == args[i] } ?: break
list.add(route)
i += route.unit
}
return list
}
fun unregister() {
val f = Bukkit.getServer().javaClass.getDeclaredField("commandMap")
f.isAccessible = true
val commandMap = f.get(Bukkit.getServer()) as CommandMap
for (route in entries) {
commandMap.getCommand(route.name)?.unregister(commandMap)
}
}
}
abstract class Argument(
val argname: String,
vararg routes: Argument,
val execute: (CommandSender, Array<String>) -> Unit
) {
val name = argname
var routes = routes.toList()
var permission: String = ""
set(value) {
field = value
if (value.isEmpty()) return
for (route in routes) {
route.permission = value + "." + route.name
}
}
var condition: (CommandSender) -> Boolean = { true }
public fun addArgs(vararg routes: Argument): Argument {
this.routes += routes
return this
}
abstract var priority: Int
open var unit: Int = 1
open fun onCommand(sender: CommandSender, args: Array<String>) {
execute(sender, args)
}
abstract fun suggest(sender: CommandSender, args: Array<String>): List<String>
fun getCompletList(sender: CommandSender, args: Array<String>): List<String> {
return routes
.filter { sender.hasPermission(it.permission) && it.condition(sender) }
.map { it.suggest(sender, args) }
.flatten()
}
}
class Route(
name: String,
vararg routes: Argument,
execute: (CommandSender, Array<String>) -> Unit
) : Argument(name, *routes, execute = execute) {
override var priority: Int = 2
override fun suggest(sender: CommandSender, args: Array<String>): List<String> {
return if (sender.hasPermission(this.permission) &&
this.condition(sender) &&
this.name.startsWith(args.last())
)
listOf(this.name)
else listOf()
}
}
class Text(name: String, vararg routes: Argument, execute: (CommandSender, Array<String>) -> Unit) :
Argument(name, *routes, execute = execute) {
override var priority: Int = 0
override fun suggest(sender: CommandSender, args: Array<String>): List<String> {
return listOf(args.last())
}
}
class Integer(
name: String,
vararg routes: Argument,
execute: (CommandSender, Array<String>) -> Unit
) : Argument(name, *routes, execute = execute) {
override var priority: Int = 1
override fun suggest(sender: CommandSender, args: Array<String>): List<String> {
return listOf()
}
}
@@ -1,7 +1,7 @@
package net.hareworks.simplymcdb package net.hareworks.simplymcdb
import net.hareworks.kommandlib.KommandLib import net.hareworks.kommand_lib.KommandLib
import net.hareworks.simplymcdb.command.smcdb import net.hareworks.simplymcdb.command.registerCommands
import net.hareworks.simplymcdb.database.Database import net.hareworks.simplymcdb.database.Database
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
@@ -18,22 +18,25 @@ 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
override fun onEnable() { override fun onEnable() {
instance = this instance = this
Config.init() Config.init()
command = KommandLib(this, smcdb) command = registerCommands(this)
server.pluginManager.registerEvents(EventListener, this) server.pluginManager.registerEvents(EventListener, this)
if (Config.check()) enable() if (Config.check()) enable()
} }
override fun onDisable() { override fun onDisable() {
enabled = State.DISABLED enabled = State.DISABLED
Database.disconnect() Database.disconnect()
@@ -1,202 +1,281 @@
package net.hareworks.simplymcdb.command package net.hareworks.simplymcdb.command
import com.github.michaelbull.result.* import java.util.UUID
import net.hareworks.kommandlib.* import net.hareworks.kommand_lib.KommandLib
import net.hareworks.simplymcdb.* import net.hareworks.kommand_lib.kommand
import net.hareworks.simplymcdb.App
import net.hareworks.simplymcdb.Config
import net.hareworks.simplymcdb.State
import net.hareworks.simplymcdb.database.Database import net.hareworks.simplymcdb.database.Database
import net.hareworks.simplymcdb.fetch
import net.hareworks.simplymcdb.findPlayersNeedingMigration
import net.hareworks.simplymcdb.isRegistered
import net.hareworks.simplymcdb.overwritePlayerData
import net.hareworks.simplymcdb.register
import net.hareworks.simplymcdb.update
import net.hareworks.simplymcdb.PlayerSerializer
import net.kyori.adventure.audience.Audience import net.kyori.adventure.audience.Audience
import net.kyori.adventure.text.minimessage.MiniMessage import net.kyori.adventure.text.minimessage.MiniMessage
import org.bukkit.entity.Player import org.bukkit.entity.Player
import org.bukkit.plugin.java.JavaPlugin
private val miniMessage = MiniMessage.miniMessage()
private val commandBuffer = mutableMapOf<UUID, String>()
public fun Audience.sendMM(message: String) { public fun Audience.sendMM(message: String) {
this.sendMessage(MiniMessage.miniMessage().deserialize(message)) this.sendMessage(miniMessage.deserialize(message))
} }
val command_buffer = mutableMapOf<java.util.UUID, String>() private fun Audience.sendConfigHelp() {
public val smcdb = sendMM(
Route("smcdb") { sender, _ -> sender.sendMessage("simptlymcdb command") } "<red>simplymcdb config help<newline><gray>reload: <green>reload the config from config.yml<newline><gray>fetch: <green>fetch the config from the database<newline><gray>upload: <green>upload the current config to the database")
.addArgs( }
Route("config") { sender, _ ->
(sender as Player).performCommand("smcdb config help") public fun registerCommands(plugin: JavaPlugin): KommandLib =
} kommand(plugin) {
.addArgs( command("smcdb") {
Route("reload") { sender, _ -> description = "Control Simply-Minecraft-DB"
sender.sendMessage("reloading config...") permission = "simplydb.command"
Config.reload()
sender.sendMessage("reloaded.") executes {
}, sender.sendMessage("simplymcdb command")
Route("fetch") { sender, _ -> }
sender.sendMessage("fetching config...")
}, literal("config") {
Route("upload") { sender, _ -> requires("simplydb.command.config")
sender.sendMessage("uploading config...") executes { sender.sendConfigHelp() }
},
Route("help") { sender, _ -> literal("reload") {
var help = requires("simplydb.command.config.reload")
MiniMessage.miniMessage() executes {
.deserialize( sender.sendMessage("reloading config...")
"<red>simplymcdb config help<newline><gray>reload: <green>reload the config from config.yml<newline><gray>fetch: <green>fetch the config from the database<newline><gray>upload: <green>upload the current config to the database" Config.reload()
) sender.sendMessage("reloaded.")
sender.sendMessage(help) }
} }
),
Route("help") { sender, _ -> literal("fetch") {
sender.sendMM( requires("simplydb.command.config.fetch")
"<red>simplymcdb help<newline><gray>config: <green>configre the plugin<newline><gray>activate: <green>when the plugin is disabled, activate it<newline><gray>deactivate: <green>when the plugin is enabled, deactivate it" executes { sender.sendMessage("fetching config...") }
) }
},
Route("activate") { sender, _ -> literal("upload") {
if (App.instance.enabled == State.ACTIVE) { requires("simplydb.command.config.upload")
sender.sendMessage("simplymcdb is already enabled.") executes { sender.sendMessage("uploading config...") }
return@Route }
}
App.instance.enable() literal("help") { executes { sender.sendConfigHelp() } }
sender.sendMessage("simplymcdb enabled.") }
},
Route("deactivate") { sender, _ -> literal("help") {
if (App.instance.enabled == State.DISABLED) { executes {
sender.sendMessage("simplymcdb is already disabled.") sender.sendMM(
return@Route "<red>simplymcdb help<newline><gray>config: <green>configre the plugin<newline><gray>activate: <green>when the plugin is disabled, activate it<newline><gray>deactivate: <green>when the plugin is enabled, deactivate it")
} }
App.instance.disable() }
sender.sendMessage("simplymcdb disabled.")
}, literal("activate") {
Route("database") { _, _ -> } requires("simplydb.command.on")
.addArgs( executes {
Route("init") { sender, _ -> if (App.instance.enabled == State.ACTIVE) {
Database.initialize() sender.sendMessage("simplymcdb is already enabled.")
sender.sendMessage("database initialized.") return@executes
}, }
Route("reset") { sender, _ -> App.instance.enable()
Database.reset() sender.sendMessage("simplymcdb enabled.")
sender.sendMessage("database reset.") }
}, }
),
Route("check") { sender, _ -> literal("deactivate") {
sender.sendMM( requires("simplydb.command.off")
"${when (App.instance.enabled) { executes {
State.ACTIVE -> "<green>●" if (App.instance.enabled == State.DISABLED) {
State.DISCONNECTED -> "<yellow>■" sender.sendMessage("simplymcdb is already disabled.")
State.DISABLED -> "<red>○" return@executes
}}<white> simply-minecraft-database" }
) App.instance.disable()
sender.sendMM( sender.sendMessage("simplymcdb disabled.")
"status: ${when (App.instance.enabled) { }
State.ACTIVE -> "<green>active" }
State.DISCONNECTED -> "<yellow>disconnected"
State.DISABLED -> "<red>disabled"}}" literal("database") {
) literal("init") {
sender.sendMM( executes {
"<gray>- <white>database test: ${if (Database.ping()) "success" else "failed"}" Database.initialize()
) sender.sendMessage("database initialized.")
sender.sendMM( }
"<gray>- <white>config test: ${if (Config.config.getBoolean("enabled")) "enabled" else "disabled"}" }
) literal("reset") {
}, executes {
Route("register") { sender, _ -> Database.reset()
if (sender !is Player) { sender.sendMessage("database reset.")
sender.sendMM("This command is only available for players.") }
return@Route }
} else }
when (App.instance.enabled) {
State.DISABLED -> literal("migrate") {
sender.sendMM( executes {
"<red>[SMCDB] simplymcdb is disabled.<br>Run /smcdb check to check the status." val player = sender as? Player
) if (player == null) {
State.DISCONNECTED -> sender.sendMM("<red>[SMCDB] This command can only be run by players.")
sender.sendMM( return@executes
"<red>[SMCDB] simplymcdb is enabled but disconnected.<br>Run /smcdb check to check the status." }
) when (App.instance.enabled) {
else -> { State.DISABLED -> {
if (!isRegistered(sender.uniqueId)) { sender.sendMM("<red>[SMCDB] simplymcdb is disabled.")
sender.sendMM( return@executes
"<gray>[SMCDB] <red>The inventory of the other servers will be overwritten.<newline>" + }
"Are you sure you want to register?<newline>" + State.DISCONNECTED -> {
"<green>/smcdb confirm<gray> to confirm." sender.sendMM("<yellow>[SMCDB] Database disconnected. Try again later.")
) return@executes
} else { }
sender.sendMM("<gray>[SMCDB] You are already registered.") else -> {}
} }
} if (!isRegistered(player.uniqueId)) {
} sender.sendMM("<red>[SMCDB] You are not registered in the database.")
}, return@executes
Route("confirm") { sender, _ -> }
if (sender !is Player) return@Route try {
when (command_buffer[sender.uniqueId]) { sender.sendMM("<gray>[SMCDB] Applying legacy data...")
"register" -> { fetch(player)
if (App.instance.enabled == State.ACTIVE) { update(player)
register(sender) sender.sendMM("<green>[SMCDB] Migration complete. Data updated to the latest format.")
sender.sendMM("<gray>[SMCDB] Successfully registered.") } catch (e: Exception) {
} else { App.instance.logger.warning("Failed to migrate data for ${player.uniqueId}: ${e.message}")
sender.sendMM("<red>[SMCDB] simplymcdb is disabled.") sender.sendMM("<red>[SMCDB] Migration failed. Check server logs.")
} }
} }
else -> {
sender.sendMM("<red>[SMCDB] Invalid command.") literal("all") {
} executes {
} val executor = sender as? Player
command_buffer.remove(sender.uniqueId) if (executor == null) {
} sender.sendMM("<red>[SMCDB] This command can only be run by players.")
// Route("update") { sender, _ -> return@executes
// if (sender !is Player) { }
// sender.sendMM("This command is only available for players.") when (App.instance.enabled) {
// return@Route State.DISABLED -> {
// } else sender.sendMM("<red>[SMCDB] simplymcdb is disabled.")
// when (App.instance.enabled) { return@executes
// State.DISABLED -> }
// sender.sendMM( State.DISCONNECTED -> {
// "<red>[SMCDB] simplymcdb is disabled.<br>Run sender.sendMM("<yellow>[SMCDB] Database disconnected. Try again later.")
// /smcdb check to check the status." return@executes
// ) }
// State.DISCONNECTED -> else -> {}
// sender.sendMM( }
// "<red>[SMCDB] simplymcdb is enabled but val targets = findPlayersNeedingMigration()
// disconnected.<br>Run /smcdb check to check the status." if (targets.isEmpty()) {
// ) sender.sendMM("<gray>[SMCDB] No legacy data found.")
// else -> { return@executes
// if (isRegistered(sender.uniqueId)) { }
// update(sender) sender.sendMM("<gray>[SMCDB] Migrating ${targets.size} legacy profiles... Please wait.")
// sender.sendMM("<gray>[SMCDB] Successfully updated.") val backup = PlayerSerializer.serialize(executor)
// } else { var migrated = 0
// sender.sendMM("<red>[SMCDB] You are not registered.") try {
// } targets.forEach { entry ->
// } try {
// } PlayerSerializer.deserialize(executor, entry.serialized)
// }, val updatedSnapshot = PlayerSerializer.serialize(executor)
// Route("fetch") { sender, _ -> overwritePlayerData(entry.uuid, updatedSnapshot)
// if (sender !is Player) { migrated++
// sender.sendMM("This command is only available for players.") } catch (ex: Exception) {
// return@Route App.instance.logger.warning("Failed to migrate data for ${entry.uuid}: ${ex.message}")
// } else }
// when (App.instance.enabled) { }
// State.DISABLED -> } finally {
// sender.sendMM( try {
// "<red>[SMCDB] simplymcdb is disabled.<br>Run PlayerSerializer.deserialize(executor, backup)
// /smcdb check to check the status." } catch (restoreEx: Exception) {
// ) App.instance.logger.warning("Failed to restore migration executor state: ${restoreEx.message}")
// State.DISCONNECTED -> }
// sender.sendMM( }
// "<red>[SMCDB] simplymcdb is enabled but sender.sendMM("<green>[SMCDB] Migration finished ($migrated/${targets.size}). Check logs for failures.")
// disconnected.<br>Run /smcdb check to check the status." }
// ) }
// else -> { }
// if (isRegistered(sender.uniqueId)) {
// sender.sendMM( literal("check") {
// "<gray>[SMCDB] Welcome back, executes {
// ${sender.name}.<newline>Fetching your data..." val commandSender = sender
// ) commandSender.sendMM(
// fetch(sender) "${when (App.instance.enabled) {
// } else { State.ACTIVE -> "<green>●"
// sender.sendMM( State.DISCONNECTED -> "<yellow>■"
// "<gray>[SMCDB] Welcome, ${sender.name}.<newline>" State.DISABLED -> "<red>○"
// + }}<white> simply-minecraft-database")
// "SMCDB is active but you have already commandSender.sendMM(
// played before.<newline>" + "status: ${when (App.instance.enabled) {
// "Run <green>/smcdb register<gray> to State.ACTIVE -> "<green>active"
// register yourself." State.DISCONNECTED -> "<yellow>disconnected"
// ) State.DISABLED -> "<red>disabled"
// } }}")
// } commandSender.sendMM(
// } "<gray>- <white>config test: ${if (Config.config.getBoolean("enabled")) "enabled" else "disabled"}")
// }, commandSender.sendMM("<gray>- <white>database test: <yellow>checking...")
)
// Run the potentially slow ping off the main thread to avoid freezing the server thread.
App.instance.server.scheduler.runTaskAsynchronously(App.instance, Runnable {
val pingSuccess = try {
Database.ping()
} catch (ex: Exception) {
App.instance.logger.warning("Database ping failed: ${ex.message}")
false
}
App.instance.server.scheduler.runTask(App.instance, Runnable {
commandSender.sendMM(
"<gray>- <white>database test result: ${if (pingSuccess) "success" else "failed"}")
})
})
}
}
literal("register") {
executes {
val player = sender as? Player
if (player == null) {
sender.sendMM("This command is only available for players.")
return@executes
}
when (App.instance.enabled) {
State.DISABLED -> {
sender.sendMM("<red>[SMCDB] simplymcdb is disabled.<br>Run /smcdb check to check the status.")
return@executes
}
State.DISCONNECTED -> {
sender.sendMM("<red>[SMCDB] simplymcdb is enabled but disconnected.<br>Run /smcdb check to check the status.")
return@executes
}
else -> {}
}
if (!isRegistered(player.uniqueId)) {
sender.sendMM(
"<gray>[SMCDB] <red>The inventory of the other servers will be overwritten.<newline>" +
"Are you sure you want to register?<newline>" +
"<green>/smcdb confirm<gray> to confirm.")
commandBuffer[player.uniqueId] = "register"
} else {
sender.sendMM("<gray>[SMCDB] You are already registered.")
}
}
}
literal("confirm") {
executes {
val player = sender as? Player ?: return@executes
when (commandBuffer[player.uniqueId]) {
"register" -> {
if (App.instance.enabled == State.ACTIVE) {
register(player)
sender.sendMM("<gray>[SMCDB] Successfully registered.")
} else {
sender.sendMM("<red>[SMCDB] simplymcdb is disabled.")
}
}
else -> sender.sendMM("<red>[SMCDB] Invalid command.")
}
commandBuffer.remove(player.uniqueId)
}
}
}
}
@@ -46,18 +46,15 @@ public object EventListener : Listener {
| not | confirm register | | not | confirm register |
+------------+------------------*/ +------------+------------------*/
if (isRegistered(event.player.uniqueId)) fetch(event.player) if (isRegistered(event.player.uniqueId)) fetch(event.player)
else if (event.player.hasPlayedBefore()) { else if (event.player.hasPlayedBefore()) register(event.player)
register(event.player) else event.player.sendMessage(
} else { MiniMessage.miniMessage()
event.player.sendMessage( .deserialize(
MiniMessage.miniMessage() "<gray>[SMCDB] Welcome, ${event.player.name}.<newline>" +
.deserialize( "SMCDB is active but you have already played before.<newline>" +
"<gray>[SMCDB] Welcome, ${event.player.name}.<newline>" + "Run <green>/smcdb register<gray> to register yourself."
"SMCDB is active but you have already played before.<newline>" +
"Run <green>/smcdb register<gray> to register yourself."
)
) )
} )
} }
@EventHandler @EventHandler
@@ -1,68 +1,179 @@
package net.hareworks.simplymcdb 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 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.entity.Player as BukkitPlayer
import org.bukkit.inventory.ItemStack
import org.bukkit.potion.PotionEffect
import org.bukkit.potion.PotionEffectType
data class PlayerData( const val PLAYER_DATA_CURRENT_VERSION = 1
val health: Float,
val hunger: Int, @Serializable
val exp: Float, data class PlayerSnapshot(
val effects: String, val version: Int = PLAYER_DATA_CURRENT_VERSION,
val inv: String, val health: Double,
val enderchest: String 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 { object PlayerSerializer {
fun serialize(player: BukkitPlayer): String { fun serialize(player: BukkitPlayer): String {
val result = val snapshot =
NBT.get( PlayerSnapshot(
player, health = player.health,
Function { nbt -> foodLevel = player.foodLevel,
var output = NBT.createNBTObject() xpProgress = player.exp,
selectedItemSlot = player.inventory.heldItemSlot,
output.setFloat("Health", player.health.toFloat()) potionEffects = player.activePotionEffects.map(::serializePotionEffect),
output.setInteger("foodLevel", player.foodLevel) inventory = player.inventory.contents.map { it?.let(::serializeItemStack) },
output.setFloat("XpP", player.exp) enderChest = player.enderChest.contents.map { it?.let(::serializeItemStack) }
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()
}
) )
return result return json.encodeToString(PlayerSnapshot.serializer(), snapshot)
} }
fun deserialize(player: BukkitPlayer, data: String) { fun deserialize(player: BukkitPlayer, data: String) {
NBT.modify( val snapshot =
player, try {
Function { nbt -> json.decodeFromString(PlayerSnapshot.serializer(), data)
val input = NBT.parseNBT(data) } catch (ex: SerializationException) {
if (LegacySerializer.deserialize(player, data)) return else throw ex
App.instance.logger.info("Deserializing player data: $data") } catch (ex: IllegalArgumentException) {
App.instance.logger.info("deserialized: ${input.toString()}") if (LegacySerializer.deserialize(player, data)) return else throw ex
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) }
} }
) 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
}
} }
} }
@@ -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
}
}
}
@@ -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 {