From ff665c27f594f178ec659d78129986e1ede33a3a Mon Sep 17 00:00:00 2001 From: Kenneth Endfinger Date: Sun, 26 Dec 2021 03:33:23 -0500 Subject: [PATCH] Initial Commit of Gjallarhorn: A Heimdall Analytics Tool --- build.gradle.kts | 9 +++- foundation-gjallarhorn/build.gradle.kts | 10 ++++ .../gjallarhorn/BlockStateTracker.kt | 34 +++++++++++++ .../gjallarhorn/GjallarhornCommand.kt | 22 +++++++++ .../gjallarhorn/commands/BlockLogReplay.kt | 49 +++++++++++++++++++ .../commands/PlayerPositionExport.kt | 49 +++++++++++++++++++ .../commands/PlayerSessionExport.kt | 40 +++++++++++++++ .../kubelet/foundation/gjallarhorn/compose.kt | 11 +++++ .../kubelet/foundation/gjallarhorn/main.kt | 11 +++++ foundation-heimdall/build.gradle.kts | 8 +-- .../heimdall/view/BlockChangeView.kt | 17 +++++++ .../src/main/resources/init.sql | 2 + settings.gradle.kts | 1 + 13 files changed, 257 insertions(+), 6 deletions(-) create mode 100644 foundation-gjallarhorn/build.gradle.kts create mode 100644 foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/BlockStateTracker.kt create mode 100644 foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/GjallarhornCommand.kt create mode 100644 foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/BlockLogReplay.kt create mode 100644 foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/PlayerPositionExport.kt create mode 100644 foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/PlayerSessionExport.kt create mode 100644 foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/compose.kt create mode 100644 foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/main.kt create mode 100644 foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/view/BlockChangeView.kt diff --git a/build.gradle.kts b/build.gradle.kts index f452d1e..94ecb30 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -39,6 +39,9 @@ tasks.create("updateManifests") { writer.use { val rootPath = rootProject.rootDir.toPath() val updateManifest = subprojects.mapNotNull { project -> + if (project.name == "foundation-gjallarhorn") { + return@mapNotNull null + } val files = project.tasks.getByName("shadowJar").outputs val paths = files.files.map { rootPath.relativize(it.toPath()).toString() } @@ -119,8 +122,10 @@ subprojects { } } - tasks.withType { - archiveClassifier.set("plugin") + if (project.name != "foundation-gjallarhorn") { + tasks.withType { + archiveClassifier.set("plugin") + } } tasks.assemble { diff --git a/foundation-gjallarhorn/build.gradle.kts b/foundation-gjallarhorn/build.gradle.kts new file mode 100644 index 0000000..98e83d1 --- /dev/null +++ b/foundation-gjallarhorn/build.gradle.kts @@ -0,0 +1,10 @@ +dependencies { + implementation(project(":foundation-core")) + implementation(project(":foundation-heimdall")) + implementation("org.slf4j:slf4j-simple:1.7.32") + implementation("com.github.ajalt.clikt:clikt:3.3.0") +} + +listOf(tasks.jar, tasks.shadowJar).map { it.get() }.forEach { task -> + task.manifest.attributes["Main-Class"] = "cloud.kubelet.foundation.gjallarhorn.MainKt" +} diff --git a/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/BlockStateTracker.kt b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/BlockStateTracker.kt new file mode 100644 index 0000000..9b36748 --- /dev/null +++ b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/BlockStateTracker.kt @@ -0,0 +1,34 @@ +package cloud.kubelet.foundation.gjallarhorn + +import java.util.* +import kotlin.collections.HashMap + +class BlockStateTracker { + val blocks = HashMap() + + fun place(position: BlockPosition, state: BlockState) { + blocks[position] = state + } + + fun delete(position: BlockPosition) { + blocks.remove(position) + } + + data class BlockState(val type: String) + + data class BlockPosition( + val x: Long, + val y: Long, + val z: Long + ) { + override fun equals(other: Any?): Boolean { + if (other !is BlockPosition) { + return false + } + + return other.x == x && other.y == y && other.z == z + } + + override fun hashCode(): Int = Objects.hash(x, y, z) + } +} diff --git a/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/GjallarhornCommand.kt b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/GjallarhornCommand.kt new file mode 100644 index 0000000..e1f69db --- /dev/null +++ b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/GjallarhornCommand.kt @@ -0,0 +1,22 @@ +package cloud.kubelet.foundation.gjallarhorn + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import org.jetbrains.exposed.sql.Database + +class GjallarhornCommand : CliktCommand(invokeWithoutSubcommand = true) { + private val jdbcConnectionUrl by option("-c", "--connection-url", help = "JDBC Connection URL") + .default("jdbc:postgresql://localhost/foundation") + + private val jdbcConnectionUsername by option("-u", "--connection-username", help = "JDBC Connection Username") + .default("jdbc:postgresql://localhost/foundation") + + private val jdbcConnectionPassword by option("-p", "--connection-password", help = "JDBC Connection Passowrd") + .default("jdbc:postgresql://localhost/foundation") + + override fun run() { + val db = Database.connect(jdbcConnectionUrl, user = jdbcConnectionUsername, password = jdbcConnectionPassword) + currentContext.findOrSetObject { db } + } +} diff --git a/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/BlockLogReplay.kt b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/BlockLogReplay.kt new file mode 100644 index 0000000..866e51a --- /dev/null +++ b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/BlockLogReplay.kt @@ -0,0 +1,49 @@ +package cloud.kubelet.foundation.gjallarhorn.commands + +import cloud.kubelet.foundation.gjallarhorn.BlockStateTracker +import cloud.kubelet.foundation.gjallarhorn.compose +import cloud.kubelet.foundation.heimdall.view.BlockChangeView +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.requireObject +import com.github.ajalt.clikt.parameters.options.option +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import java.time.Instant + +class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-log") { + private val db by requireObject() + private val timeAsString by option("--time", help = "Replay Time") + + override fun run() { + val filter = compose( + combine = { a, b -> a and b }, + { timeAsString != null } to { BlockChangeView.time lessEq Instant.parse(timeAsString) } + ) + val tracker = BlockStateTracker() + + transaction(db) { + BlockChangeView.select(filter).orderBy(BlockChangeView.time).forEach { row -> + val changeIsBreak = row[BlockChangeView.isBreak] + val x = row[BlockChangeView.x] + val y = row[BlockChangeView.y] + val z = row[BlockChangeView.z] + val block = row[BlockChangeView.block] + + val location = BlockStateTracker.BlockPosition(x.toLong(), y.toLong(), z.toLong()) + if (changeIsBreak) { + tracker.delete(location) + } else { + tracker.place(location, BlockStateTracker.BlockState(block)) + } + } + } + + println("x,y,z,block") + for ((position, block) in tracker.blocks) { + println("${position.x},${position.y},${position.z},${block.type}") + } + } +} \ No newline at end of file diff --git a/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/PlayerPositionExport.kt b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/PlayerPositionExport.kt new file mode 100644 index 0000000..b3ac3f9 --- /dev/null +++ b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/PlayerPositionExport.kt @@ -0,0 +1,49 @@ +package cloud.kubelet.foundation.gjallarhorn.commands + +import cloud.kubelet.foundation.gjallarhorn.compose +import cloud.kubelet.foundation.heimdall.table.PlayerPositionTable +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.requireObject +import com.github.ajalt.clikt.parameters.options.option +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.SqlExpressionBuilder.greaterEq +import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import java.time.Instant +import java.util.* + +class PlayerPositionExport : CliktCommand(name = "export-player-positions", help = "Export Player Positions") { + private val db by requireObject() + + private val playerIdString by option("--player", help = "Player ID") + private val startTimeString by option("--start-time", help = "Start Time") + private val endTimeString by option("--end-time", help = "End Time") + + override fun run() { + val filter = compose( + combine = { a, b -> a and b }, + { startTimeString != null } to { PlayerPositionTable.time greaterEq Instant.parse(startTimeString) }, + { endTimeString != null } to { PlayerPositionTable.time lessEq Instant.parse(endTimeString) }, + { playerIdString != null } to { PlayerPositionTable.player eq UUID.fromString(playerIdString) } + ) + + println("time,player,world,x,y,z,pitch,yaw") + transaction(db) { + PlayerPositionTable.select(filter).orderBy(PlayerPositionTable.time).forEach { row -> + val time = row[PlayerPositionTable.time] + val player = row[PlayerPositionTable.player] + val world = row[PlayerPositionTable.world] + val x = row[PlayerPositionTable.x] + val y = row[PlayerPositionTable.y] + val z = row[PlayerPositionTable.z] + val pitch = row[PlayerPositionTable.pitch] + val yaw = row[PlayerPositionTable.yaw] + + println("${time},${player},${world},${x},${y},${z},${pitch},${yaw}") + } + } + } +} diff --git a/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/PlayerSessionExport.kt b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/PlayerSessionExport.kt new file mode 100644 index 0000000..e9b66c7 --- /dev/null +++ b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/PlayerSessionExport.kt @@ -0,0 +1,40 @@ +package cloud.kubelet.foundation.gjallarhorn + +import cloud.kubelet.foundation.heimdall.table.PlayerSessionTable +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.requireObject +import com.github.ajalt.clikt.parameters.options.option +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import java.util.* + +class PlayerSessionExport : CliktCommand(name = "export-player-sessions", help = "Export Player Sessions") { + private val db by requireObject() + + private val playerIdString by option("--player-id", help = "Player ID") + private val playerNameString by option("--player-name", help = "Player Name") + + override fun run() { + val filter = compose( + combine = { a, b -> a and b }, + { playerIdString != null } to { PlayerSessionTable.player eq UUID.fromString(playerIdString) }, + { playerNameString != null } to { PlayerSessionTable.name eq playerNameString!! } + ) + + println("id,player,name,start,end") + transaction(db) { + PlayerSessionTable.select(filter).orderBy(PlayerSessionTable.endTime).forEach { row -> + val id = row[PlayerSessionTable.id] + val player = row[PlayerSessionTable.player] + val name = row[PlayerSessionTable.name] + val start = row[PlayerSessionTable.startTime] + val end = row[PlayerSessionTable.endTime] + + println("${id},${player},${name},${start},${end}") + } + } + } +} diff --git a/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/compose.kt b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/compose.kt new file mode 100644 index 0000000..0f1a87e --- /dev/null +++ b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/compose.kt @@ -0,0 +1,11 @@ +package cloud.kubelet.foundation.gjallarhorn + +import org.jetbrains.exposed.sql.Op + +fun compose( + combine: (Op, Op) -> Op, + vararg filters: Pair<() -> Boolean, () -> Op> +): Op = filters.toMap().entries + .filter { it.key() } + .map { it.value() } + .fold(Op.TRUE as Op, combine) diff --git a/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/main.kt b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/main.kt new file mode 100644 index 0000000..c9e2b46 --- /dev/null +++ b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/main.kt @@ -0,0 +1,11 @@ +package cloud.kubelet.foundation.gjallarhorn + +import cloud.kubelet.foundation.gjallarhorn.commands.BlockLogReplay +import cloud.kubelet.foundation.gjallarhorn.commands.PlayerPositionExport +import com.github.ajalt.clikt.core.subcommands + +fun main(args: Array) = GjallarhornCommand().subcommands( + BlockLogReplay(), + PlayerSessionExport(), + PlayerPositionExport() +).main(args) diff --git a/foundation-heimdall/build.gradle.kts b/foundation-heimdall/build.gradle.kts index 20b552f..00c9278 100644 --- a/foundation-heimdall/build.gradle.kts +++ b/foundation-heimdall/build.gradle.kts @@ -1,7 +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") + api("org.postgresql:postgresql:42.3.1") + api("org.jetbrains.exposed:exposed-jdbc:0.36.2") + api("org.jetbrains.exposed:exposed-java-time:0.36.2") + api("com.zaxxer:HikariCP:5.0.0") compileOnly(project(":foundation-core")) } diff --git a/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/view/BlockChangeView.kt b/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/view/BlockChangeView.kt new file mode 100644 index 0000000..1ba4165 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/view/BlockChangeView.kt @@ -0,0 +1,17 @@ +package cloud.kubelet.foundation.heimdall.view + +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.javatime.timestamp + +object BlockChangeView : Table("block_changes") { + val isBreak = bool("break") + val time = timestamp("time") + val player = uuid("player") + val world = uuid("world") + val x = double("x") + val y = double("y") + val z = double("z") + val pitch = double("pitch") + val yaw = double("yaw") + val block = text("block") +} diff --git a/foundation-heimdall/src/main/resources/init.sql b/foundation-heimdall/src/main/resources/init.sql index e6def06..f008b65 100644 --- a/foundation-heimdall/src/main/resources/init.sql +++ b/foundation-heimdall/src/main/resources/init.sql @@ -118,3 +118,5 @@ create table if not exists heimdall.entity_kills ( ); -- select create_hypertable('heimdall.entity_kills', 'time', 'player', 4, if_not_exists => TRUE); +-- +create or replace view heimdall.block_changes as select true as break, * from heimdall.block_breaks union all select false as break, * from heimdall.block_places; diff --git a/settings.gradle.kts b/settings.gradle.kts index 3f3b020..ea3e077 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,4 +4,5 @@ include( ":foundation-core", ":foundation-bifrost", ":foundation-heimdall", + ":foundation-gjallarhorn", )