diff --git a/foundation-bifrost/src/main/kotlin/cloud/kubelet/foundation/bifrost/FoundationBifrostPlugin.kt b/foundation-bifrost/src/main/kotlin/cloud/kubelet/foundation/bifrost/FoundationBifrostPlugin.kt index b0f76c7..98a5a67 100644 --- a/foundation-bifrost/src/main/kotlin/cloud/kubelet/foundation/bifrost/FoundationBifrostPlugin.kt +++ b/foundation-bifrost/src/main/kotlin/cloud/kubelet/foundation/bifrost/FoundationBifrostPlugin.kt @@ -13,18 +13,18 @@ import net.dv8tion.jda.api.entities.TextChannel import net.dv8tion.jda.api.events.GenericEvent import net.dv8tion.jda.api.events.ReadyEvent import net.dv8tion.jda.api.events.message.MessageReceivedEvent -import net.dv8tion.jda.api.hooks.EventListener +import net.dv8tion.jda.api.hooks.EventListener as DiscordEventListener import net.kyori.adventure.text.Component import net.kyori.adventure.text.TextComponent import org.bukkit.event.EventHandler -import org.bukkit.event.Listener +import org.bukkit.event.Listener as BukkitEventListener import org.bukkit.event.player.PlayerJoinEvent import org.bukkit.event.player.PlayerQuitEvent import org.bukkit.plugin.java.JavaPlugin import java.awt.Color import kotlin.io.path.inputStream -class FoundationBifrostPlugin : JavaPlugin(), EventListener, Listener { +class FoundationBifrostPlugin : JavaPlugin(), DiscordEventListener, BukkitEventListener { private lateinit var config: BifrostConfig private lateinit var jda: JDA private var isDev = false diff --git a/foundation-heimdall/build.gradle.kts b/foundation-heimdall/build.gradle.kts new file mode 100644 index 0000000..20b552f --- /dev/null +++ b/foundation-heimdall/build.gradle.kts @@ -0,0 +1,7 @@ +dependencies { + implementation("org.postgresql:postgresql:42.3.1") + implementation("org.jetbrains.exposed:exposed-jdbc:0.36.2") + implementation("org.jetbrains.exposed:exposed-java-time:0.36.2") + implementation("com.zaxxer:HikariCP:5.0.0") + compileOnly(project(":foundation-core")) +} diff --git a/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/Extensions.kt b/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/Extensions.kt new file mode 100644 index 0000000..b0bb391 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/Extensions.kt @@ -0,0 +1,21 @@ +package cloud.kubelet.foundation.heimdall + +fun String.sqlSplitStatements(): List { + val statements = mutableListOf() + val buffer = StringBuilder() + fun flush() { + val trimmed = buffer.toString().trim() + if (trimmed.isNotEmpty()) { + statements.add(trimmed) + } + } + for (line in lines()) { + if (line.trim() == "--") { + flush() + } else { + buffer.append(line).append("\n") + } + } + flush() + return statements +} diff --git a/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/FoundationHeimdallPlugin.kt b/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/FoundationHeimdallPlugin.kt new file mode 100644 index 0000000..210ca96 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/FoundationHeimdallPlugin.kt @@ -0,0 +1,90 @@ +package cloud.kubelet.foundation.heimdall + +import cloud.kubelet.foundation.core.FoundationCorePlugin +import cloud.kubelet.foundation.core.Util +import cloud.kubelet.foundation.heimdall.buffer.BufferFlushThread +import cloud.kubelet.foundation.heimdall.buffer.EventBuffer +import cloud.kubelet.foundation.heimdall.event.PlayerPositionEvent +import cloud.kubelet.foundation.heimdall.model.HeimdallConfig +import cloud.kubelet.foundation.heimdall.table.PlayerPositionTable +import com.charleskorn.kaml.Yaml +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.player.PlayerMoveEvent +import org.bukkit.plugin.java.JavaPlugin +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.transactions.transaction +import org.postgresql.Driver +import java.lang.Exception +import java.time.Instant +import kotlin.io.path.inputStream + +class FoundationHeimdallPlugin : JavaPlugin(), Listener { + private lateinit var config: HeimdallConfig + private lateinit var pool: HikariDataSource + internal lateinit var db: Database + + private val buffer = EventBuffer() + private val bufferFlushThread = BufferFlushThread(this, buffer) + + override fun onEnable() { + val foundation = server.pluginManager.getPlugin("Foundation") as FoundationCorePlugin + + val configPath = Util.copyDefaultConfig( + slF4JLogger, + foundation.pluginDataPath, + "heimdall.yaml" + ) + config = Yaml.default.decodeFromStream(HeimdallConfig.serializer(), configPath.inputStream()) + if (!config.enabled) { + slF4JLogger.info("Heimdall is not enabled.") + return + } + slF4JLogger.info("Heimdall is enabled.") + if (!Driver.isRegistered()) { + Driver.register() + } + pool = HikariDataSource(HikariConfig().apply { + jdbcUrl = config.db.url + username = config.db.username + password = config.db.password + schema = "heimdall" + }) + val initMigrationContent = FoundationHeimdallPlugin::class.java.getResourceAsStream( + "/init.sql" + )?.readAllBytes()?.decodeToString() ?: throw RuntimeException("Unable to find Heimdall init.sql") + + val statements = initMigrationContent.sqlSplitStatements() + + pool.connection.use { conn -> + conn.autoCommit = false + try { + for (statementAsString in statements) { + conn.prepareStatement(statementAsString).use { + it.execute() + } + } + conn.commit() + } catch (e: Exception) { + conn.rollback() + throw e + } finally { + conn.autoCommit = true + } + } + + db = Database.connect(pool) + server.pluginManager.registerEvents(this, this) + bufferFlushThread.start() + } + + @EventHandler + fun onPlayerMove(event: PlayerMoveEvent) = buffer.push(PlayerPositionEvent(event)) + + override fun onDisable() { + bufferFlushThread.stop() + } +} diff --git a/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/buffer/BufferFlushThread.kt b/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/buffer/BufferFlushThread.kt new file mode 100644 index 0000000..4f6eb85 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/buffer/BufferFlushThread.kt @@ -0,0 +1,37 @@ +package cloud.kubelet.foundation.heimdall.buffer + +import cloud.kubelet.foundation.heimdall.FoundationHeimdallPlugin +import org.jetbrains.exposed.sql.transactions.transaction +import java.util.concurrent.atomic.AtomicBoolean + +class BufferFlushThread(val plugin: FoundationHeimdallPlugin, val buffer: EventBuffer) { + private val running = AtomicBoolean(false) + + fun start() { + running.set(true) + val thread = Thread { + plugin.slF4JLogger.info("Buffer Flusher Started") + while (running.get()) { + try { + transaction(plugin.db) { + val count = buffer.flush(this) + if (count > 0) { + plugin.slF4JLogger.info("Flushed $count Events") + } + } + } catch (e: Exception) { + plugin.slF4JLogger.warn("Failed to flush buffer.", e) + } + Thread.sleep(5000) + } + plugin.slF4JLogger.info("Buffer Flusher Stopped") + } + thread.name = "Heimdall Buffer Flush" + thread.isDaemon = false + thread.start() + } + + fun stop() { + running.set(false) + } +} diff --git a/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/buffer/EventBuffer.kt b/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/buffer/EventBuffer.kt new file mode 100644 index 0000000..88f53ba --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/buffer/EventBuffer.kt @@ -0,0 +1,24 @@ +package cloud.kubelet.foundation.heimdall.buffer + +import cloud.kubelet.foundation.heimdall.event.HeimdallEvent +import org.jetbrains.exposed.sql.Transaction + +class EventBuffer { + private var events = mutableListOf() + + fun flush(transaction: Transaction): Long { + val referenceOfEvents = events + this.events = mutableListOf() + var count = 0L + while (referenceOfEvents.isNotEmpty()) { + val event = referenceOfEvents.removeAt(0) + event.store(transaction) + count++ + } + return count + } + + fun push(event: HeimdallEvent) { + events.add(event) + } +} diff --git a/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/event/HeimdallEvent.kt b/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/event/HeimdallEvent.kt new file mode 100644 index 0000000..1e847f5 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/event/HeimdallEvent.kt @@ -0,0 +1,7 @@ +package cloud.kubelet.foundation.heimdall.event + +import org.jetbrains.exposed.sql.Transaction + +abstract class HeimdallEvent { + abstract fun store(transaction: Transaction) +} diff --git a/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/event/PlayerPositionEvent.kt b/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/event/PlayerPositionEvent.kt new file mode 100644 index 0000000..885c269 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/event/PlayerPositionEvent.kt @@ -0,0 +1,31 @@ +package cloud.kubelet.foundation.heimdall.event + +import cloud.kubelet.foundation.heimdall.table.PlayerPositionTable +import org.bukkit.Location +import org.bukkit.event.player.PlayerMoveEvent +import org.jetbrains.exposed.sql.Transaction +import org.jetbrains.exposed.sql.insert +import java.time.Instant +import java.util.* + +class PlayerPositionEvent( + val playerUniqueIdentity: UUID, + val location: Location +) : HeimdallEvent() { + constructor(event: PlayerMoveEvent) : this(event.player.uniqueId, event.to) + + override fun store(transaction: Transaction) { + transaction.apply { + PlayerPositionTable.insert { + it[time] = Instant.now() + it[player] = playerUniqueIdentity + it[world] = location.world.uid + it[x] = location.x + it[y] = location.y + it[z] = location.z + it[pitch] = location.pitch.toDouble() + it[yaw] = location.yaw.toDouble() + } + } + } +} diff --git a/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/model/HeimdallConfig.kt b/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/model/HeimdallConfig.kt new file mode 100644 index 0000000..79cb000 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/model/HeimdallConfig.kt @@ -0,0 +1,16 @@ +package cloud.kubelet.foundation.heimdall.model + +import kotlinx.serialization.Serializable + +@Serializable +data class HeimdallConfig( + val enabled: Boolean = false, + val db: DbConfig +) + +@Serializable +data class DbConfig( + val url: String, + val username: String, + val password: String +) diff --git a/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/table/PlayerPositionTable.kt b/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/table/PlayerPositionTable.kt new file mode 100644 index 0000000..cd662af --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/table/PlayerPositionTable.kt @@ -0,0 +1,15 @@ +package cloud.kubelet.foundation.heimdall.table + +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.javatime.timestamp + +object PlayerPositionTable : Table("player_positions") { + val time = timestamp("time") + val world = uuid("world") + val player = uuid("player") + val x = double("x") + val y = double("y") + val z = double("z") + val pitch = double("pitch") + val yaw = double("yaw") +} diff --git a/foundation-heimdall/src/main/resources/heimdall.yaml b/foundation-heimdall/src/main/resources/heimdall.yaml new file mode 100644 index 0000000..4691019 --- /dev/null +++ b/foundation-heimdall/src/main/resources/heimdall.yaml @@ -0,0 +1,11 @@ +# Whether Heimdall should be enabled for tracking events. +enabled: false + +# Database connection information. +db: + # JDBC URL + url: "jdbc:postgresql://localhost/foundation" + # JDBC Username + username: "foundation" + # JDBC Password + password: "foundation" diff --git a/foundation-heimdall/src/main/resources/init.sql b/foundation-heimdall/src/main/resources/init.sql new file mode 100644 index 0000000..4f3cc2e --- /dev/null +++ b/foundation-heimdall/src/main/resources/init.sql @@ -0,0 +1,20 @@ +-- +create extension if not exists "uuid-ossp"; +-- +create extension if not exists timescaledb; +-- +create schema if not exists heimdall; +-- +create table if not exists heimdall.player_positions ( + time timestamp not null, + player uuid not null, + world uuid not null, + x double precision not null, + y double precision not null, + z double precision not null, + pitch double precision not null, + yaw double precision not null, + PRIMARY KEY (time, player, world) +); +-- +select create_hypertable('heimdall.player_positions', 'time', 'player', 4, if_not_exists => TRUE); diff --git a/foundation-heimdall/src/main/resources/plugin.yml b/foundation-heimdall/src/main/resources/plugin.yml new file mode 100644 index 0000000..39d26f5 --- /dev/null +++ b/foundation-heimdall/src/main/resources/plugin.yml @@ -0,0 +1,10 @@ +name: Foundation-Heimdall +version: '${version}' +main: cloud.kubelet.foundation.heimdall.FoundationHeimdallPlugin +api-version: 1.18 +prefix: Foundation-Heimdall +load: STARTUP +depend: + - Foundation +authors: + - kubelet diff --git a/settings.gradle.kts b/settings.gradle.kts index 5c5848e..3f3b020 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,4 +3,5 @@ rootProject.name = "foundation" include( ":foundation-core", ":foundation-bifrost", + ":foundation-heimdall", )