summaryrefslogtreewikicommitdiff
path: root/core/src/db
diff options
context:
space:
mode:
authorFreya Murphy <freya@freyacat.org>2026-03-26 23:15:33 -0400
committerFreya Murphy <freya@freyacat.org>2026-03-27 23:09:23 -0400
commitf8322cd21cde68a72b05efbad3a05b8e67c0bdd0 (patch)
treed7e60bc8fedadc8fa7ae725571cad1f398eaf6dc /core/src/db
downloadkenshinshideandseek2-f8322cd21cde68a72b05efbad3a05b8e67c0bdd0.tar.gz
kenshinshideandseek2-f8322cd21cde68a72b05efbad3a05b8e67c0bdd0.tar.bz2
kenshinshideandseek2-f8322cd21cde68a72b05efbad3a05b8e67c0bdd0.zip
initial
Diffstat (limited to 'core/src/db')
-rw-r--r--core/src/db/Database.kt80
-rw-r--r--core/src/db/Driver.kt79
-rw-r--r--core/src/db/Legacy.kt58
-rw-r--r--core/src/db/Player.kt62
4 files changed, 279 insertions, 0 deletions
diff --git a/core/src/db/Database.kt b/core/src/db/Database.kt
new file mode 100644
index 0000000..27864b3
--- /dev/null
+++ b/core/src/db/Database.kt
@@ -0,0 +1,80 @@
+package cat.freya.khs.db
+
+import cat.freya.khs.Khs
+import java.util.UUID
+import org.jetbrains.exposed.v1.core.*
+import org.jetbrains.exposed.v1.jdbc.*
+import org.jetbrains.exposed.v1.jdbc.Database as Exposed
+import org.jetbrains.exposed.v1.jdbc.transactions.transaction
+
+class Database(plugin: Khs) {
+ val driver = getDriver(plugin)
+ val source = driver.connect()
+ val db = Exposed.connect(source)
+
+ init {
+ transaction(db) { SchemaUtils.create(Players) }
+ migrateLegacy()
+ }
+
+ fun getPlayer(uuid: UUID): Player? =
+ transaction(db) {
+ val id = uuid.toString()
+ Players.selectAll().where { Players.uuid eq id }.map { it.toPlayer() }.singleOrNull()
+ }
+
+ fun getPlayer(name: String): Player? =
+ transaction(db) {
+ Players.selectAll().where { Players.name eq name }.map { it.toPlayer() }.singleOrNull()
+ }
+
+ fun getPlayers(page: UInt, pageSize: UInt): List<Player> =
+ transaction(db) {
+ val offset = page * pageSize
+ val wins = Players.hiderWins + Players.seekerWins
+ Players.selectAll()
+ .orderBy(wins to SortOrder.DESC)
+ .limit(pageSize.toInt())
+ .offset(offset.toLong())
+ .map { it.toPlayer() }
+ }
+
+ fun getPlayerNames(limit: UInt, startsWith: String): List<String> =
+ transaction(db) {
+ Players.select(Players.name)
+ .where { Players.name like "$startsWith%" }
+ .orderBy(Players.name to SortOrder.ASC)
+ .limit(limit.toInt())
+ .map { it[Players.name] }
+ .filterNotNull()
+ }
+
+ fun upsertPlayer(player: Player) = transaction(db) { Players.upsert { it.fromPlayer(player) } }
+
+ fun upsertName(u: UUID, n: String) =
+ transaction(db) {
+ Players.upsert {
+ it[uuid] = u.toString()
+ it[name] = n
+ }
+ }
+
+ fun migrateLegacy() =
+ transaction(db) {
+ if (!LegacyPlayers.exists() || !LegacyNames.exists()) return@transaction
+
+ val legacy =
+ LegacyPlayers.join(
+ LegacyNames,
+ JoinType.LEFT,
+ onColumn = LegacyPlayers.uuid,
+ otherColumn = LegacyNames.uuid,
+ )
+ .selectAll()
+ .map { it.toLegacyPlayer() }
+ Players.insertIgnore { legacy.forEach { player -> it.fromPlayer(player) } }
+
+ SchemaUtils.drop(LegacyPlayers)
+ SchemaUtils.drop(LegacyNames)
+ }
+}
diff --git a/core/src/db/Driver.kt b/core/src/db/Driver.kt
new file mode 100644
index 0000000..c30e5c1
--- /dev/null
+++ b/core/src/db/Driver.kt
@@ -0,0 +1,79 @@
+package cat.freya.khs.db
+
+import cat.freya.khs.Khs
+import cat.freya.khs.config.DatabaseConfig
+import cat.freya.khs.config.DatabaseType
+import com.zaxxer.hikari.HikariConfig
+import com.zaxxer.hikari.HikariDataSource
+import javax.sql.DataSource
+
+abstract class Driver {
+ abstract val driverClass: String
+
+ abstract fun jdbcUrl(): String
+
+ abstract fun configure(hikari: HikariConfig)
+
+ fun connect(): DataSource {
+ // load driver for some reason
+ Class.forName(driverClass)
+
+ val cores = Runtime.getRuntime().availableProcessors()
+ val hikari =
+ HikariConfig().apply {
+ jdbcUrl = jdbcUrl()
+ driverClassName = driverClass
+ maximumPoolSize = minOf(cores, 8)
+ configure(this)
+ }
+ return HikariDataSource(hikari)
+ }
+}
+
+class SqliteDriver(val path: String) : Driver() {
+ override val driverClass = "org.sqlite.JDBC"
+
+ override fun jdbcUrl(): String {
+ return "jdbc:sqlite:$path"
+ }
+
+ override fun configure(hikari: HikariConfig) {
+ // sqlite is single threaded
+ hikari.maximumPoolSize = 1
+ }
+}
+
+class MysqlDriver(val config: DatabaseConfig) : Driver() {
+ override val driverClass = "com.mysql.cj.jdbc.Driver"
+
+ override fun jdbcUrl(): String {
+ val port = config.port ?: 3006u
+ return "jdbc:mysql://${config.host}:${port}/${config.database}"
+ }
+
+ override fun configure(hikari: HikariConfig) {
+ hikari.username = config.username
+ hikari.password = config.password
+ }
+}
+
+class PostgresDriver(val config: DatabaseConfig) : Driver() {
+ override val driverClass = "org.postgresql.Driver"
+
+ override fun jdbcUrl(): String {
+ val port = config.port ?: 5432u
+ return "jdbc:postgresql://${config.host}:${port}/${config.database}"
+ }
+
+ override fun configure(hikari: HikariConfig) {
+ hikari.username = config.username
+ hikari.password = config.password
+ }
+}
+
+fun getDriver(plugin: Khs): Driver =
+ when (plugin.config.database.type) {
+ DatabaseType.SQLITE -> SqliteDriver(plugin.shim.sqliteDatabasePath)
+ DatabaseType.MYSQL -> MysqlDriver(plugin.config.database)
+ DatabaseType.POSTGRES -> PostgresDriver(plugin.config.database)
+ }
diff --git a/core/src/db/Legacy.kt b/core/src/db/Legacy.kt
new file mode 100644
index 0000000..7f64ef2
--- /dev/null
+++ b/core/src/db/Legacy.kt
@@ -0,0 +1,58 @@
+package cat.freya.khs.db
+
+import java.nio.ByteBuffer
+import java.util.UUID
+import org.jetbrains.exposed.v1.core.ResultRow
+import org.jetbrains.exposed.v1.core.Table
+
+// tables introduced in version 1.7.0
+// pre 1.7.x tables are NOT SUPPORTED
+
+object LegacyNames : Table("hs_names") {
+ val uuid = binary("uuid", 16)
+ val name = varchar("name", 48).nullable()
+ override val primaryKey = PrimaryKey(uuid, name)
+}
+
+object LegacyPlayers : Table("hs_data") {
+ val uuid = binary("uuid", 16)
+ val hiderWins = integer("hider_wins").nullable()
+ val seekerWins = integer("seeker_wins").nullable()
+ val hiderGames = integer("hider_games").nullable()
+ val seekerGames = integer("seeker_games").nullable()
+ val hiderKills = integer("hider_kills").nullable()
+ val seekerKills = integer("seeker_kills").nullable()
+ val hiderDeaths = integer("hider_deaths").nullable()
+ val seekerDeaths = integer("seeker_deaths").nullable()
+}
+
+fun ResultRow.toLegacyPlayer(): Player {
+ val uuidBuffer = ByteBuffer.wrap(this[LegacyPlayers.uuid])
+ val uuidHigh = uuidBuffer.long
+ val uuidLow = uuidBuffer.long
+ val uuid = UUID(uuidHigh, uuidLow)
+
+ val hiderGames = this[LegacyPlayers.hiderGames] ?: 0
+ val seekerGames = this[LegacyPlayers.seekerGames] ?: 0
+ val hiderWins = this[LegacyPlayers.hiderWins] ?: 0
+ val seekerWins = this[LegacyPlayers.seekerWins] ?: 0
+ val hiderLosses = hiderGames - hiderWins
+ val seekerLosses = seekerGames - seekerWins
+ val hiderKills = this[LegacyPlayers.hiderKills] ?: 0
+ val seekerKills = this[LegacyPlayers.seekerKills] ?: 0
+ val hiderDeaths = this[LegacyPlayers.hiderDeaths] ?: 0
+ val seekerDeaths = this[LegacyPlayers.seekerDeaths] ?: 0
+
+ return Player(
+ uuid,
+ name = this[LegacyNames.name],
+ seekerWins = maxOf(seekerWins, 0).toUInt(),
+ hiderWins = maxOf(hiderWins, 0).toUInt(),
+ hiderLosses = maxOf(hiderLosses, 0).toUInt(),
+ seekerLosses = maxOf(seekerLosses, 0).toUInt(),
+ seekerKills = maxOf(seekerKills, 0).toUInt(),
+ hiderKills = maxOf(hiderKills, 0).toUInt(),
+ seekerDeaths = maxOf(seekerDeaths, 0).toUInt(),
+ hiderDeaths = maxOf(hiderDeaths, 0).toUInt(),
+ )
+}
diff --git a/core/src/db/Player.kt b/core/src/db/Player.kt
new file mode 100644
index 0000000..ccebcaa
--- /dev/null
+++ b/core/src/db/Player.kt
@@ -0,0 +1,62 @@
+package cat.freya.khs.db
+
+import java.util.UUID
+import org.jetbrains.exposed.v1.core.ResultRow
+import org.jetbrains.exposed.v1.core.Table
+import org.jetbrains.exposed.v1.core.statements.UpdateBuilder
+
+object Players : Table("hs_players") {
+ val uuid = varchar("uuid", 36)
+ val name = text("name").nullable()
+ val seekerWins = integer("seeker_wins").default(0)
+ val hiderWins = integer("hider_wins").default(0)
+ val seekerLosses = integer("seeker_losses").default(0)
+ val hiderLosses = integer("hider_losses").default(0)
+ val seekerKills = integer("seeker_kills").default(0)
+ val hiderKills = integer("hider_kills").default(0)
+ val seekerDeaths = integer("seeker_deaths").default(0)
+ val hiderDeaths = integer("hider_deaths").default(0)
+
+ override val primaryKey = PrimaryKey(uuid)
+}
+
+data class Player(
+ val uuid: UUID,
+ var name: String? = null,
+ var seekerWins: UInt = 0u,
+ var hiderWins: UInt = 0u,
+ var seekerLosses: UInt = 0u,
+ var hiderLosses: UInt = 0u,
+ var seekerKills: UInt = 0u,
+ var hiderKills: UInt = 0u,
+ var seekerDeaths: UInt = 0u,
+ var hiderDeaths: UInt = 0u,
+)
+
+fun ResultRow.toPlayer(): Player {
+ return Player(
+ uuid = UUID.fromString(this[Players.uuid]),
+ name = this[Players.name],
+ seekerWins = this[Players.seekerWins].toUInt(),
+ hiderWins = this[Players.hiderWins].toUInt(),
+ seekerLosses = this[Players.seekerLosses].toUInt(),
+ hiderLosses = this[Players.hiderLosses].toUInt(),
+ seekerKills = this[Players.seekerKills].toUInt(),
+ hiderKills = this[Players.hiderKills].toUInt(),
+ seekerDeaths = this[Players.seekerDeaths].toUInt(),
+ hiderDeaths = this[Players.hiderDeaths].toUInt(),
+ )
+}
+
+fun UpdateBuilder<*>.fromPlayer(player: Player) {
+ this[Players.uuid] = player.uuid.toString()
+ this[Players.name] = player.name
+ this[Players.seekerWins] = player.seekerWins.toInt()
+ this[Players.hiderWins] = player.hiderWins.toInt()
+ this[Players.seekerLosses] = player.seekerLosses.toInt()
+ this[Players.hiderLosses] = player.hiderLosses.toInt()
+ this[Players.seekerKills] = player.seekerKills.toInt()
+ this[Players.hiderKills] = player.hiderKills.toInt()
+ this[Players.seekerDeaths] = player.seekerDeaths.toInt()
+ this[Players.hiderDeaths] = player.hiderDeaths.toInt()
+}