Compare commits

..
6 Commits
Author SHA1 Message Date
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
Hare 5f805ad0a7 style: comment table 2024-12-04 19:02:57 +09:00
Hare 55f5a95d73 fix: player registration logic in EventListener 2024-12-04 19:01:51 +09:00
Hare 2e9f984bd4 sync successfull 2024-12-04 18:47:46 +09:00
22 changed files with 857 additions and 360 deletions
+1
View File
@@ -0,0 +1 @@
use flake
+2
View File
@@ -21,3 +21,5 @@ gradle-app.setting
# JDT-specific (Eclipse Java Development Tools) # JDT-specific (Eclipse Java Development Tools)
.classpath .classpath
.direnv
bin
+5
View File
@@ -1,2 +1,7 @@
# Simply-minecraft-db # Simply-minecraft-db
## Features
- [x] Configurable sharing options
- [x] Store config to database
- [x] Sync settings across multiple servers via a database
+13 -7
View File
@@ -1,7 +1,7 @@
import net.minecrell.pluginyml.bukkit.BukkitPluginDescription import net.minecrell.pluginyml.bukkit.BukkitPluginDescription
group = "net.hareworks" group = "net.hareworks"
version = "1.0" version = "1.1"
val exposedVersion = "0.54.0" val exposedVersion = "0.54.0"
@@ -9,14 +9,15 @@ plugins {
kotlin("jvm") version "2.0.20" kotlin("jvm") version "2.0.20"
kotlin("plugin.serialization") version "2.0.20" kotlin("plugin.serialization") version "2.0.20"
id("net.minecrell.plugin-yml.bukkit") version "0.6.0" 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 { 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/")
} }
dependencies { dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.1-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")
@@ -26,11 +27,18 @@ 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")
implementation("de.tr7zw:item-nbt-api:2.15.3")
implementation("com.michael-bull.kotlin-result:kotlin-result:2.0.0")
} }
tasks { tasks {
shadowJar { shadowJar {
archiveBaseName.set("SimplyMCDB") archiveBaseName.set("SimplyMCDB")
archiveClassifier.set("") archiveClassifier.set("")
relocate("de.tr7zw.changeme.nbtapi", "net.hareworks.simplymcdb.libs.nbtapi")
}
build {
dependsOn(shadowJar)
} }
} }
@@ -39,11 +47,9 @@ 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.1" apiVersion = "1.21.10"
authors = authors =
listOf( listOf("Hare-K02")
"Hare-K02",
)
permissions { permissions {
register("simplydb.*") { register("simplydb.*") {
children = listOf("simplydb.command", "simplydb.admin") children = listOf("simplydb.command", "simplydb.admin")
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": 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
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
'';
};
}
);
}
+2
View File
@@ -0,0 +1,2 @@
#This file is generated by updateDaemonJvm
toolchainVersion=21
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-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
Vendored
+21 -13
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" \
Vendored
+12 -10
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
+69 -108
View File
@@ -1,5 +1,6 @@
package net.hareworks.kommandlib package net.hareworks.kommandlib
import com.github.michaelbull.result.*
import kotlin.collections.listOf import kotlin.collections.listOf
import org.bukkit.Bukkit import org.bukkit.Bukkit
import org.bukkit.command.CommandMap import org.bukkit.command.CommandMap
@@ -7,47 +8,52 @@ import org.bukkit.command.CommandSender
import org.bukkit.command.PluginCommand import org.bukkit.command.PluginCommand
import org.bukkit.command.TabCompleter import org.bukkit.command.TabCompleter
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
import net.hareworks.simplymcdb.App
class KommandLib(plugin: JavaPlugin, vararg routes: Pair<String, Argument>) { class KommandLib(plugin: JavaPlugin, vararg routes: Argument) {
val routes = routes.toMap() val entries = routes.toList()
init { init {
val f = Bukkit.getServer().javaClass.getDeclaredField("commandMap") val f = Bukkit.getServer().javaClass.getDeclaredField("commandMap")
f.isAccessible = true f.isAccessible = true
val commandMap = f.get(Bukkit.getServer()) as CommandMap val commandMap = f.get(Bukkit.getServer()) as CommandMap
for ((name, _) in routes) { for (route in routes) {
commandMap.register( commandMap.register(
plugin.getName(), plugin.getName(),
(PluginCommand::class (PluginCommand::class
.java .java
.declaredConstructors .declaredConstructors
.first() .first()
.apply { isAccessible = true } .apply { isAccessible = true }
.newInstance(name, plugin) as .newInstance(route.name, plugin) as
PluginCommand) PluginCommand)
.apply { .apply {
this.name = name this.name = name
this.setExecutor { sender, _, alias, args -> this.setExecutor { sender, _, alias, args ->
val route = getLastRoute(arrayOf(alias, *args)) val routeargs = routeTreeSearch(arrayOf(alias, *args))
if (route.size == args.size + 1) route.last().onCommand(sender, args) if (routeargs.size == args.size + 1)
true routeargs.last().onCommand(sender, args)
} true
this.tabCompleter = TabCompleter { sender, _, alias, args -> }
val route = getLastRoute(arrayOf(alias, *args)) this.tabCompleter = TabCompleter { sender, _, alias, args ->
if (route.size == args.size) route.last().getCompletList(sender, args) val routeargs = routeTreeSearch(arrayOf(alias, *args))
else listOf() if (routeargs.size == args.size)
} routeargs.last().getCompletList(sender, args)
} else listOf()
}
}
) )
} }
} }
fun getLastRoute(args: Array<String>): List<Argument> { fun routeTreeSearch(args: Array<String>): List<Argument> {
val list = mutableListOf<Argument>(routes[args[0]] ?: throw Exception("Invalid command")) val list =
mutableListOf<Argument>(
entries.find { it.name == args[0] } ?: throw Exception("Invalid command")
)
var i = 1 var i = 1
while (i + 1 <= args.size) { while (i + 1 <= args.size) {
if (list.last().routes.isEmpty()) break if (list.last().routes.isEmpty()) break
val route = val route = list.last().routes.sortedBy { it.priority }.find { it.name == args[i] } ?: break
list.last().routes.values.sortedBy { it.priority }.find { it.typeCheck(args[i]) } ?: break
list.add(route) list.add(route)
i += route.unit i += route.unit
} }
@@ -58,127 +64,82 @@ class KommandLib(plugin: JavaPlugin, vararg routes: Pair<String, Argument>) {
val f = Bukkit.getServer().javaClass.getDeclaredField("commandMap") val f = Bukkit.getServer().javaClass.getDeclaredField("commandMap")
f.isAccessible = true f.isAccessible = true
val commandMap = f.get(Bukkit.getServer()) as CommandMap val commandMap = f.get(Bukkit.getServer()) as CommandMap
for ((name, _) in routes) { for (route in entries) {
commandMap.getCommand(name)?.unregister(commandMap) commandMap.getCommand(route.name)?.unregister(commandMap)
} }
} }
} }
abstract class Argument( abstract class Argument(
vararg routes: Pair<String, Argument>, val argname: String,
val execute: (CommandSender, Array<Any>) -> Unit vararg routes: Argument,
val execute: (CommandSender, Array<String>) -> Unit
) { ) {
val routes = routes.toMap() val name = argname
var name: String = "" var routes = routes.toList()
var permission: String = "" var permission: String = ""
set(value) { set(value) {
field = value field = value
if (value.isEmpty()) return if (value.isEmpty()) return
for ((_, route) in routes) { for (route in routes) {
route.permission = value + "." + route.name route.permission = value + "." + route.name
} }
println("permission setted to $value")
} }
var condition: (CommandSender) -> Boolean = { true } var condition: (CommandSender) -> Boolean = { true }
init { public fun addArgs(vararg routes: Argument): Argument {
for ((name, route) in routes) { this.routes += routes
route.name = name return this
}
} }
abstract var priority: Int abstract var priority: Int
open var unit: Int = 1 open var unit: Int = 1
abstract fun onCommand(sender: CommandSender, args: Array<String>) open fun onCommand(sender: CommandSender, args: Array<String>) {
abstract fun typeCheck(arg: String): Boolean execute(sender, args)
abstract fun toValue(args: Array<String>): Any }
abstract fun suggest(sender: CommandSender, args: Array<String>): List<String> abstract fun suggest(sender: CommandSender, args: Array<String>): List<String>
fun getCompletList(sender: CommandSender, args: Array<String>): List<String> { fun getCompletList(sender: CommandSender, args: Array<String>): List<String> {
return routes return routes
.values .filter { sender.hasPermission(it.permission) && it.condition(sender) }
.filter { sender.hasPermission(it.permission) && it.condition(sender) } .map { it.suggest(sender, args) }
.map { it.suggest(sender, args) } .flatten()
.flatten()
} }
} }
class Route(vararg routes: Pair<String, Argument>, execute: (CommandSender, Array<Any>) -> Unit) : class Route(
Argument(*routes, execute = execute) { name: String,
vararg routes: Argument,
execute: (CommandSender, Array<String>) -> Unit
) : Argument(name, *routes, execute = execute) {
override var priority: Int = 2 override var priority: Int = 2
override fun onCommand(sender: CommandSender, args: Array<String>) {
execute(sender, args.map { it }.toTypedArray())
}
override fun typeCheck(arg: String): Boolean {
return this.name == arg
}
override fun toValue(args: Array<String>): Any {
return args.joinToString(" ")
}
override fun suggest(sender: CommandSender, args: Array<String>): List<String> { override fun suggest(sender: CommandSender, args: Array<String>): List<String> {
return if (sender.hasPermission(this.permission) && this.condition(sender) && this.name.startsWith(args.last())) return if (sender.hasPermission(this.permission) &&
listOf(this.name) this.condition(sender) &&
this.name.startsWith(args.last())
)
listOf(this.name)
else listOf() else listOf()
} }
} }
class Text(vararg routes: Pair<String, Argument>, execute: (CommandSender, Array<Any>) -> Unit) : class Text(name: String, vararg routes: Argument, execute: (CommandSender, Array<String>) -> Unit) :
Argument(*routes, execute = execute) { Argument(name, *routes, execute = execute) {
override var priority: Int = 0 override var priority: Int = 0
override fun typeCheck(arg: String): Boolean {
return true
}
override fun toValue(args: Array<String>): Any {
return args.joinToString(" ")
}
override fun onCommand(sender: CommandSender, args: Array<String>) {
execute(sender, args.map { it }.toTypedArray())
}
override fun suggest(sender: CommandSender, args: Array<String>): List<String> { override fun suggest(sender: CommandSender, args: Array<String>): List<String> {
return listOf(args.last()) return listOf(args.last())
} }
} }
class Integer(vararg routes: Pair<String, Argument>, execute: (CommandSender, Array<Any>) -> Unit) : class Integer(
Argument(*routes, execute = execute) { name: String,
vararg routes: Argument,
execute: (CommandSender, Array<String>) -> Unit
) : Argument(name, *routes, execute = execute) {
override var priority: Int = 1 override var priority: Int = 1
override fun typeCheck(arg: String): Boolean {
return arg.toIntOrNull() != null
}
override fun onCommand(sender: CommandSender, args: Array<String>) {
execute(sender, args.map { it.toInt() }.toTypedArray())
}
override fun toValue(args: Array<String>): Any {
return args[0].toInt()
}
override fun suggest(sender: CommandSender, args: Array<String>): List<String> {
return listOf()
}
}
class Position(
vararg routes: Pair<String, Argument>,
execute: (CommandSender, Array<Any>) -> Unit
) : Argument(*routes, execute = execute) {
override var priority: Int = 3
override var unit: Int = 3
override fun typeCheck(arg: String): Boolean {
return true
}
override fun onCommand(sender: CommandSender, args: Array<String>) {
execute(sender, args.map { it }.toTypedArray())
}
override fun toValue(args: Array<String>): Any {
return Triple(args[0].toDouble(), args[1].toDouble(), args[2].toDouble())
}
override fun suggest(sender: CommandSender, args: Array<String>): List<String> { override fun suggest(sender: CommandSender, args: Array<String>): List<String> {
return listOf() return listOf()
@@ -1,11 +1,11 @@
package net.hareworks.simplymcdb package net.hareworks.simplymcdb
import net.hareworks.simplymcdb.Config
import net.hareworks.simplymcdb.database.Database
import net.hareworks.simplymcdb.command.smcdb
import net.hareworks.kommandlib.KommandLib import net.hareworks.kommandlib.KommandLib
import net.hareworks.simplymcdb.command.smcdb
import net.hareworks.simplymcdb.database.Database
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
enum class State { enum class State {
ACTIVE, ACTIVE,
DISCONNECTED, DISCONNECTED,
@@ -18,19 +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
logger.info("simplymcdb enabled.")
Config.init() Config.init()
command = KommandLib(this, "smcdb" to smcdb) command = KommandLib(this, smcdb)
server.pluginManager.registerEvents(EventListener, this)
if (Config.check()) enable()
} }
override fun onDisable() { override fun onDisable() {
enabled = State.DISABLED enabled = State.DISABLED
Database.disconnect() Database.disconnect()
@@ -1,119 +1,287 @@
package net.hareworks.simplymcdb.command package net.hareworks.simplymcdb.command
import com.github.michaelbull.result.*
import net.hareworks.kommandlib.* import net.hareworks.kommandlib.*
import net.hareworks.simplymcdb.App import net.hareworks.simplymcdb.*
import net.hareworks.simplymcdb.State
import net.hareworks.simplymcdb.config.reload as reloadConfig
import net.hareworks.simplymcdb.database.Database import net.hareworks.simplymcdb.database.Database
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
public val smcdb: Route = public fun Audience.sendMM(message: String) {
Route( this.sendMessage(MiniMessage.miniMessage().deserialize(message))
"config" to }
Route(
"reload" to val command_buffer = mutableMapOf<java.util.UUID, String>()
Route { sender, _ -> public val smcdb =
sender.sendMessage("reloading config...") Route("smcdb") { sender, _ -> sender.sendMessage("simptlymcdb command") }
reloadConfig() .addArgs(
sender.sendMessage("reloaded.") Route("config") { sender, _ ->
} (sender as Player).performCommand("smcdb config help")
.apply { }
permission = .addArgs(
"simplydb.command.config.reload" Route("reload") { sender, _ ->
}, sender.sendMessage("reloading config...")
"fetch" to Config.reload()
Route { sender, _ -> sender.sendMessage("reloaded.")
sender.sendMessage("fetching config...") },
} Route("fetch") { sender, _ ->
.apply { sender.sendMessage("fetching config...")
permission = },
"simplydb.command.config.fetch" Route("upload") { sender, _ ->
}, sender.sendMessage("uploading config...")
"upload" to },
Route { sender, _ -> Route("help") { sender, _ ->
sender.sendMessage("uploading config...") var help =
} MiniMessage.miniMessage()
.apply { .deserialize(
permission = "<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"
"simplydb.command.config.upload" )
}, sender.sendMessage(help)
"help" to }
Route { sender, _ -> ),
var help = Route("help") { sender, _ ->
MiniMessage.miniMessage() sender.sendMM(
.deserialize( "<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"
"<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" )
) },
sender.sendMessage(help) Route("activate") { sender, _ ->
} if (App.instance.enabled == State.ACTIVE) {
.apply { sender.sendMessage("simplymcdb is already enabled.")
permission = return@Route
"simplydb.command.config" }
}, App.instance.enable()
"on" to sender.sendMessage("simplymcdb enabled.")
Route { sender, _ -> },
if (App.instance.enabled == State.ACTIVE Route("deactivate") { sender, _ ->
) { if (App.instance.enabled == State.DISABLED) {
sender.sendMessage( sender.sendMessage("simplymcdb is already disabled.")
"simplymcdb is already enabled." return@Route
) }
return@Route App.instance.disable()
} sender.sendMessage("simplymcdb disabled.")
App.instance.enable() },
sender.sendMessage("simplymcdb enabled.") Route("database") { _, _ -> }
} .addArgs(
.apply { Route("init") { sender, _ ->
permission = "simplydb.command.config.on" Database.initialize()
}, sender.sendMessage("database initialized.")
"off" to },
Route { sender, _ -> Route("reset") { sender, _ ->
if (App.instance.enabled != State.ACTIVE Database.reset()
) { sender.sendMessage("database reset.")
sender.sendMessage( },
"simplymcdb is already disabled." ),
) Route("migrate") { sender, _ ->
return@Route if (sender !is Player) {
} sender.sendMM("<red>[SMCDB] This command can only be run by players.")
App.instance.disable() return@Route
sender.sendMessage("simplymcdb disabled.") }
} when (App.instance.enabled) {
.apply { State.DISABLED -> {
permission = sender.sendMM("<red>[SMCDB] simplymcdb is disabled.")
"simplydb.command.config.off" return@Route
}, }
) { sender, _ -> State.DISCONNECTED -> {
(sender as ).performCommand("smcdb config help") sender.sendMM("<yellow>[SMCDB] Database disconnected. Try again later.")
} return@Route
.apply { permission = "simplydb.command.config" }, }
"help" to Route { sender, _ -> else -> {}
var help = }
MiniMessage.miniMessage() if (!isRegistered(sender.uniqueId)) {
.deserialize( sender.sendMM("<red>[SMCDB] You are not registered in the database.")
"<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" return@Route
) }
sender.sendMessage(help) try {
} sender.sendMM("<gray>[SMCDB] Applying legacy data...")
.apply { permission = "simplydb.command" }, fetch(sender)
"activate" to Route { sender, _ -> update(sender)
if (App.instance.enabled == State.ACTIVE sender.sendMM("<green>[SMCDB] Migration complete. Data updated to the latest format.")
) { } catch (e: Exception) {
sender.sendMessage( App.instance.logger.warning("Failed to migrate data for ${sender.uniqueId}: ${e.message}")
"simplymcdb is already enabled." sender.sendMM("<red>[SMCDB] Migration failed. Check server logs.")
) }
return@Route }.addArgs(
} Route("all") { sender, _ ->
App.instance.enable() if (sender !is Player) {
sender.sendMessage("simplymcdb enabled.") sender.sendMM("<red>[SMCDB] This command can only be run by players.")
} return@Route
.apply { permission = "simplydb.command.activate" }, }
"initialize" to Route { sender, _ -> when (App.instance.enabled) {
sender.sendMessage("database initializing...") State.DISABLED -> {
Database.initialize() sender.sendMM("<red>[SMCDB] simplymcdb is disabled.")
} return@Route
.apply { permission = "simplydb.command.setup" }, }
"confirm" to Route { sender, _ -> State.DISCONNECTED -> {
sender.sendMM(
} "<yellow>[SMCDB] Database disconnected. Try again later."
.apply { permission = "simplydb.command.confirm" }, )
) { sender, _ -> sender.sendMessage("simplymcdb command") } return@Route
.apply { name = "simplymcdb" } }
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) {
State.ACTIVE -> "<green>●"
State.DISCONNECTED -> "<yellow>■"
State.DISABLED -> "<red>○"
}}<white> simply-minecraft-database"
)
sender.sendMM(
"status: ${when (App.instance.enabled) {
State.ACTIVE -> "<green>active"
State.DISCONNECTED -> "<yellow>disconnected"
State.DISABLED -> "<red>disabled"}}"
)
sender.sendMM(
"<gray>- <white>database test: ${if (Database.ping()) "success" else "failed"}"
)
sender.sendMM(
"<gray>- <white>config test: ${if (Config.config.getBoolean("enabled")) "enabled" else "disabled"}"
)
},
Route("register") { sender, _ ->
if (sender !is Player) {
sender.sendMM("This command is only available for players.")
return@Route
} else
when (App.instance.enabled) {
State.DISABLED ->
sender.sendMM(
"<red>[SMCDB] simplymcdb is disabled.<br>Run /smcdb check to check the status."
)
State.DISCONNECTED ->
sender.sendMM(
"<red>[SMCDB] simplymcdb is enabled but disconnected.<br>Run /smcdb check to check the status."
)
else -> {
if (!isRegistered(sender.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."
)
} else {
sender.sendMM("<gray>[SMCDB] You are already registered.")
}
}
}
},
Route("confirm") { sender, _ ->
if (sender !is Player) return@Route
when (command_buffer[sender.uniqueId]) {
"register" -> {
if (App.instance.enabled == State.ACTIVE) {
register(sender)
sender.sendMM("<gray>[SMCDB] Successfully registered.")
} else {
sender.sendMM("<red>[SMCDB] simplymcdb is disabled.")
}
}
else -> {
sender.sendMM("<red>[SMCDB] Invalid command.")
}
}
command_buffer.remove(sender.uniqueId)
}
// Route("update") { sender, _ ->
// if (sender !is Player) {
// sender.sendMM("This command is only available for players.")
// return@Route
// } else
// when (App.instance.enabled) {
// State.DISABLED ->
// sender.sendMM(
// "<red>[SMCDB] simplymcdb is disabled.<br>Run
// /smcdb check to check the status."
// )
// State.DISCONNECTED ->
// sender.sendMM(
// "<red>[SMCDB] simplymcdb is enabled but
// disconnected.<br>Run /smcdb check to check the status."
// )
// else -> {
// if (isRegistered(sender.uniqueId)) {
// update(sender)
// sender.sendMM("<gray>[SMCDB] Successfully updated.")
// } else {
// sender.sendMM("<red>[SMCDB] You are not registered.")
// }
// }
// }
// },
// Route("fetch") { sender, _ ->
// if (sender !is Player) {
// sender.sendMM("This command is only available for players.")
// return@Route
// } else
// when (App.instance.enabled) {
// State.DISABLED ->
// sender.sendMM(
// "<red>[SMCDB] simplymcdb is disabled.<br>Run
// /smcdb check to check the status."
// )
// State.DISCONNECTED ->
// sender.sendMM(
// "<red>[SMCDB] simplymcdb is enabled but
// disconnected.<br>Run /smcdb check to check the status."
// )
// else -> {
// if (isRegistered(sender.uniqueId)) {
// sender.sendMM(
// "<gray>[SMCDB] Welcome back,
// ${sender.name}.<newline>Fetching your data..."
// )
// fetch(sender)
// } else {
// sender.sendMM(
// "<gray>[SMCDB] Welcome, ${sender.name}.<newline>"
// +
// "SMCDB is active but you have already
// played before.<newline>" +
// "Run <green>/smcdb register<gray> to
// register yourself."
// )
// }
// }
// }
// },
)
@@ -3,7 +3,6 @@ package net.hareworks.simplymcdb
import org.bukkit.configuration.file.YamlConfiguration import org.bukkit.configuration.file.YamlConfiguration
import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.Table
object ConfigTable : Table() { object ConfigTable : Table() {
val uuid = varchar("uuid", 36) val uuid = varchar("uuid", 36)
val name = varchar("name", 16) val name = varchar("name", 16)
@@ -34,4 +33,8 @@ public object Config {
config.save(App.instance.dataFolder.resolve("config.yml")) config.save(App.instance.dataFolder.resolve("config.yml"))
App.instance.logger.info("config saved.") App.instance.logger.info("config saved.")
} }
public fun check(): Boolean {
return config.getBoolean("enabled")
}
} }
@@ -1,14 +1,11 @@
package net.hareworks.simplymcdb.event package net.hareworks.simplymcdb
import net.hareworks.simplymcdb.App
import net.hareworks.simplymcdb.State
import net.hareworks.simplymcdb.database.Database
import net.kyori.adventure.text.minimessage.MiniMessage import net.kyori.adventure.text.minimessage.MiniMessage
import org.bukkit.event.EventHandler import org.bukkit.event.EventHandler
import org.bukkit.event.Listener import org.bukkit.event.Listener
import org.bukkit.event.player.PlayerJoinEvent import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.event.player.PlayerQuitEvent
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
public object EventListener : Listener { public object EventListener : Listener {
@EventHandler @EventHandler
@@ -16,27 +13,53 @@ public object EventListener : Listener {
if (event.player.hasPermission("simplymcdb.admin")) { if (event.player.hasPermission("simplymcdb.admin")) {
when (App.instance.enabled) { when (App.instance.enabled) {
State.DISABLED -> { State.DISABLED -> {
val mm = MiniMessage.miniMessage().deserialize("<red>simplymcdb is disabled.") val mm =
MiniMessage.miniMessage()
.deserialize(
"<red>[SMCDB:admin] simplymcdb is disabled.<br>Run /smcdb check to check the status."
)
event.player.sendMessage(mm) event.player.sendMessage(mm)
} }
State.DISCONNECTED -> { State.DISCONNECTED -> {
val mm = MiniMessage.miniMessage().deserialize("<red>simplymcdb is enabled but disconnected.<br><gray>Check the database connection and try /smcdb on.") val mm =
MiniMessage.miniMessage()
.deserialize(
"<red>[SMCDB:admin] simplymcdb is enabled but disconnected.<br>Run /smcdb check to check the status."
)
event.player.sendMessage(mm) event.player.sendMessage(mm)
} }
else -> { else -> {
val mm = MiniMessage.miniMessage().deserialize("<green>simplymcdb is enabled.") val mm =
MiniMessage.miniMessage()
.deserialize("<green>[SMCDB:admin] simplymcdb is enabled.")
event.player.sendMessage(mm) event.player.sendMessage(mm)
} }
} }
} }
if (App.instance.enabled == State.ACTIVE) {
val player = event.player if (App.instance.enabled !== State.ACTIVE) return
// if player has already registered in the database, load the data
Database.instance?.let { /*-----------+-------------------+
transaction(it) { | | played not |
+------------+-------------------+
} | registered | fetch fetch |
} | 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."
)
)
}
@EventHandler
fun onQuit(event: PlayerQuitEvent) {
if (App.instance.enabled !== State.ACTIVE) return
if (isRegistered(event.player.uniqueId)) update(event.player)
} }
} }
@@ -1,49 +0,0 @@
package net.hareworks.simplymcdb.playerdata
import net.hareworks.simplymcdb.App
import net.hareworks.simplymcdb.database.Database
import org.bukkit.entity.Player as BukkitPlayer
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
public object Player : Table() {
val uuid = varchar("uuid", 36)
val name = varchar("name", 16)
val lastLogin = long("last_login")
val lastLogout = long("last_logout")
val playTime = long("play_time")
val firstLogin = long("first_login")
val lastIp = varchar("last_ip", 15)
val data = text("data").nullable()
override val primaryKey = PrimaryKey(uuid)
}
public fun register(player: BukkitPlayer) {
App.instance.logger.info("Registering player data for ${player.name}")
Database.instance?.let {
transaction {
Player.insert {
it[uuid] = player.uniqueId.toString()
it[name] = player.name
it[lastLogin] = System.currentTimeMillis()
it[firstLogin] = System.currentTimeMillis()
it[lastIp] = player.address?.address?.hostAddress ?: "unknown"
}
}
}
}
public fun push(player: BukkitPlayer) {
App.instance.logger.info("Creating player data for ${player.name}")
Database.instance?.let {
transaction {
Player.update({ Player.uuid eq player.uniqueId.toString() }) {
it[lastLogin] = System.currentTimeMillis()
it[lastIp] = player.address?.address?.hostAddress ?: "unknown"
}
}
}
}
@@ -1,31 +1,179 @@
package net.hareworks.simplymcdb package net.hareworks.simplymcdb
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.entity.Player as BukkitPlayer
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
import org.bukkit.potion.PotionEffect
import org.bukkit.potion.PotionEffectType
class PlayerInventory( const val PLAYER_DATA_CURRENT_VERSION = 1
val contents: Array<ItemStack>,
val armor: Array<ItemStack>, @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 { object PlayerSerializer {
fun serialize(player: BukkitPlayer): String { fun serialize(player: BukkitPlayer): String {
val inv = player.inventory.storageContents val snapshot =
val armor = player.inventory.armorContents PlayerSnapshot(
val enderChest = player.enderChest health = player.health,
foodLevel = player.foodLevel,
return "" // base64 encoded string 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 json.encodeToString(PlayerSnapshot.serializer(), snapshot)
} }
fun deserialize(data: String): Map<String, Any> { fun deserialize(player: BukkitPlayer, data: String) {
return mapOf() 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))
}
} }
} }
fun ItemStacksToBase64(item: Array<ItemStack>): String { private fun serializePotionEffect(effect: PotionEffect): PotionEffectSnapshot {
return "" 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()
)
} }
fun fromBase64(data: String): ByteArray { private fun serializeItemStack(item: ItemStack): ItemStackSnapshot {
return byteArrayOf() 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
}
}
}
@@ -0,0 +1,95 @@
package net.hareworks.simplymcdb
import java.util.UUID
import net.hareworks.simplymcdb.database.Database
import org.bukkit.entity.Player as BukkitPlayer
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
public object Players : Table() {
val id = integer("id").autoIncrement()
val uuid = varchar("uuid", 36)
val name = varchar("name", 16)
val firstLogin = long("first_login")
val playTime = long("play_time").default(0)
val lastOnline = long("last_online").default(0)
val lastIp = varchar("last_ip", 15)
val data = text("data").default("")
val dataVersion = integer("data_version").default(0)
override val primaryKey = PrimaryKey(uuid)
}
public fun isRegistered(player: UUID): Boolean {
return transaction(Database.instance) {
val data =
Players.select(Players.uuid).where { Players.uuid eq player.toString() }.map {
it[Players.uuid]
}
data.isNotEmpty()
}
}
public fun register(player: BukkitPlayer) {
transaction(Database.instance) {
Players.insert {
it[uuid] = player.uniqueId.toString()
it[name] = player.name
it[firstLogin] = System.currentTimeMillis()
it[lastOnline] = System.currentTimeMillis()
it[lastIp] = player.address?.address?.hostAddress ?: "unknown"
it[dataVersion] = 0
}
}
}
public fun update(player: BukkitPlayer) {
val dat = PlayerSerializer.serialize(player)
transaction(Database.instance) {
Players.update({ Players.uuid eq player.uniqueId.toString() }) {
it[lastOnline] = System.currentTimeMillis()
it[lastIp] = player.address?.address?.hostAddress ?: "unknown"
// player.sendMessage(dat)
it[data] = dat
it[dataVersion] = PLAYER_DATA_CURRENT_VERSION
}
}
}
public fun fetch(player: BukkitPlayer) {
val dat =
transaction(Database.instance) {
Players.select(Players.uuid, Players.data)
.where { Players.uuid eq player.uniqueId.toString() }
.withDistinct()
.map { it[Players.data] }
.first()
}
// 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
}
}
}
@@ -1,8 +1,8 @@
package net.hareworks.simplymcdb.database package net.hareworks.simplymcdb.database
import net.hareworks.simplymcdb.App import net.hareworks.simplymcdb.App
import net.hareworks.simplymcdb.config.config import net.hareworks.simplymcdb.Config
import net.hareworks.simplymcdb.playerdata.Player import net.hareworks.simplymcdb.Players
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
@@ -10,7 +10,9 @@ import org.jetbrains.exposed.sql.transactions.transaction
public object Database { public object Database {
public var instance: Database? = null public var instance: Database? = null
private set private set
public fun connect() { public fun connect() {
var config = Config.config
val type = config.getString("database.type") val type = config.getString("database.type")
val host = config.getString("database.host") val host = config.getString("database.host")
val port = config.getInt("database.port") val port = config.getInt("database.port")
@@ -49,11 +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")
if (!ping()) { transaction(instance) { SchemaUtils.createMissingTablesAndColumns(Players) }
App.instance.logger.warning("Database ping failed")
} else {
App.instance.logger.info("Database ping successful")
}
} }
public fun disconnect() { public fun disconnect() {
instance?.let { instance?.let {
@@ -66,20 +64,23 @@ public object Database {
} }
public fun ping(): Boolean { public fun ping(): Boolean {
val flag = (instance == null)
if (flag) connect()
return try { return try {
transaction { transaction(instance) { exec("SELECT 1") }
addLogger(StdOutSqlLogger)
exec("SELECT 1")
}
true true
} catch (e: Exception) { } catch (e: Exception) {
false false
} finally {
if (flag) disconnect()
} }
} }
public fun initialize() { public fun initialize() {
transaction { transaction(instance) { SchemaUtils.create(Players) }
SchemaUtils.create(Player) }
}
public fun reset() {
transaction(instance) { SchemaUtils.drop(Players) }
} }
} }
+12 -6
View File
@@ -1,18 +1,24 @@
enable: false
database: database:
type: postgresql type: postgresql # Supported: mysql, postgresql
host: localhost host: localhost
port: 5432 port: 5432
# You must have created a database and user.
database: smcdb database: smcdb
user: smcdb user: smcdb
password: SaN1m_wk2eh9 password: password
playerdata:
playerdata: # README for more details.
health: true health: true
hunger: true hunger: true
experience: true experience: true
status: true effects: true
inventory: true inventory: true
enderchest: true enderchest: true
# achievements: true # achievements: true
# recipebook: true # recipebook: true
gamemode: true gamemode: true
# Do not change this value manually,
# If the plugin settings are OK, enable it with the command.
enabled: false