From 7289e5cb9fb5ffce0184d35a0ad57a75c33631e0 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Sat, 28 Jan 2023 19:35:10 -0800 Subject: [PATCH] Heimdall: It's back! --- build.gradle.kts | 3 +- common-heimdall/build.gradle.kts | 10 + .../heimdall/export/ExportedBlock.kt | 8 + .../heimdall/export/ExportedChunk.kt | 10 + .../heimdall/export/ExportedChunkSection.kt | 10 + .../heimdall/table/BlockBreakTable.kt | 16 ++ .../heimdall/table/BlockPlaceTable.kt | 16 ++ .../heimdall/table/EntityKillTable.kt | 17 ++ .../heimdall/table/PlayerAdvancementTable.kt | 16 ++ .../heimdall/table/PlayerDeathTable.kt | 17 ++ .../heimdall/table/PlayerPositionTable.kt | 15 ++ .../heimdall/table/PlayerSessionTable.kt | 12 ++ .../heimdall/table/WorldChangeTable.kt | 13 ++ .../heimdall/view/BlockChangeView.kt | 17 ++ foundation-heimdall/build.gradle.kts | 7 + .../foundation/heimdall/plugin/Extensions.kt | 21 ++ .../heimdall/plugin/HeimdallPlugin.kt | 201 ++++++++++++++++++ .../plugin/buffer/BufferFlushThread.kt | 49 +++++ .../heimdall/plugin/buffer/EventBuffer.kt | 28 +++ .../heimdall/plugin/event/BlockBreak.kt | 35 +++ .../heimdall/plugin/event/BlockPlace.kt | 35 +++ .../heimdall/plugin/event/EntityKill.kt | 33 +++ .../heimdall/plugin/event/HeimdallEvent.kt | 7 + .../plugin/event/PlayerAdvancement.kt | 35 +++ .../heimdall/plugin/event/PlayerDeath.kt | 41 ++++ .../heimdall/plugin/event/PlayerPosition.kt | 32 +++ .../heimdall/plugin/event/PlayerSession.kt | 26 +++ .../heimdall/plugin/event/WorldChange.kt | 29 +++ .../heimdall/plugin/export/ChunkExporter.kt | 70 ++++++ .../plugin/export/ExportChunksCommand.kt | 17 ++ .../heimdall/plugin/model/HeimdallConfig.kt | 16 ++ .../src/main/resources/heimdall.yaml | 11 + .../src/main/resources/init.sql | 147 +++++++++++++ .../src/main/resources/plugin.yml | 13 ++ .../queries/player_positions_aggregates.sql | 64 ++++++ settings.gradle.kts | 3 + tool-gjallarhorn/build.gradle.kts | 21 ++ .../heimdall/tool/GjallarhornCommand.kt | 37 ++++ .../commands/BlockChangeTimelapseCommand.kt | 156 ++++++++++++++ .../tool/commands/ChunkExportLoaderCommand.kt | 59 +++++ .../heimdall/tool/commands/ImageFormatType.kt | 10 + .../heimdall/tool/commands/ImageRenderType.kt | 16 ++ .../tool/commands/PlayerPositionExport.kt | 42 ++++ .../tool/commands/PlayerSessionExport.kt | 41 ++++ .../heimdall/tool/export/ChunkExportLoader.kt | 68 ++++++ .../tool/export/CombinedChunkFormat.kt | 11 + .../pizza/foundation/heimdall/tool/main.kt | 14 ++ .../tool/render/BlockDiversityRenderer.kt | 30 +++ .../heimdall/tool/render/BlockGridRenderer.kt | 47 ++++ .../tool/render/BlockHeatMapRenderer.kt | 26 +++ .../tool/render/BlockHeightMapRenderer.kt | 20 ++ .../tool/render/BlockImageRenderer.kt | 5 + .../heimdall/tool/render/BlockMapRenderer.kt | 8 + .../render/BlockVerticalFillMapRenderer.kt | 20 ++ .../render/LaunchGraphicalRenderSession.kt | 20 ++ .../render/PlayerLocationShareRenderer.kt | 56 +++++ .../tool/render/ui/GraphicalRenderSession.kt | 22 ++ .../tool/render/ui/LazyImageRenderer.kt | 21 ++ .../heimdall/tool/state/BlockChange.kt | 11 + .../heimdall/tool/state/BlockChangeType.kt | 9 + .../heimdall/tool/state/BlockChangelog.kt | 94 ++++++++ .../heimdall/tool/state/BlockCoordinate.kt | 47 ++++ .../tool/state/BlockCoordinateSparseMap.kt | 65 ++++++ .../tool/state/BlockCoordinateStore.kt | 9 + .../heimdall/tool/state/BlockExpanse.kt | 16 ++ .../heimdall/tool/state/BlockLogTracker.kt | 62 ++++++ .../heimdall/tool/state/BlockMapRenderPool.kt | 81 +++++++ .../tool/state/BlockMapRenderPoolDelegate.kt | 6 + .../heimdall/tool/state/BlockMapTimelapse.kt | 30 +++ .../heimdall/tool/state/BlockState.kt | 15 ++ .../heimdall/tool/state/BlockStateMap.kt | 3 + .../tool/state/BlockStateSerializer.kt | 20 ++ .../heimdall/tool/state/BlockTrackMode.kt | 6 + .../heimdall/tool/state/ChangelogSlice.kt | 29 +++ .../tool/state/PlayerPositionChange.kt | 15 ++ .../tool/state/PlayerPositionChangelog.kt | 28 +++ .../tool/state/SparseBlockStateMap.kt | 7 + .../state/SparseBlockStateMapSerializer.kt | 23 ++ .../heimdall/tool/util/BlockColorKey.kt | 20 ++ .../heimdall/tool/util/BlockColorKeys.kt | 18 ++ .../heimdall/tool/util/ColorGradient.kt | 57 +++++ .../heimdall/tool/util/ColorGradientPoint.kt | 16 ++ .../heimdall/tool/util/FloatClamp.kt | 12 ++ .../heimdall/tool/util/ImageTools.kt | 17 ++ .../foundation/heimdall/tool/util/compose.kt | 12 ++ .../foundation/heimdall/tool/util/numerics.kt | 39 ++++ tools/organize-artifacts.sh | 2 +- 87 files changed, 2617 insertions(+), 2 deletions(-) create mode 100644 common-heimdall/build.gradle.kts create mode 100644 common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/export/ExportedBlock.kt create mode 100644 common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/export/ExportedChunk.kt create mode 100644 common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/export/ExportedChunkSection.kt create mode 100644 common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/BlockBreakTable.kt create mode 100644 common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/BlockPlaceTable.kt create mode 100644 common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/EntityKillTable.kt create mode 100644 common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/PlayerAdvancementTable.kt create mode 100644 common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/PlayerDeathTable.kt create mode 100644 common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/PlayerPositionTable.kt create mode 100644 common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/PlayerSessionTable.kt create mode 100644 common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/WorldChangeTable.kt create mode 100644 common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/view/BlockChangeView.kt create mode 100644 foundation-heimdall/build.gradle.kts create mode 100644 foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/Extensions.kt create mode 100644 foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/HeimdallPlugin.kt create mode 100644 foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/buffer/BufferFlushThread.kt create mode 100644 foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/buffer/EventBuffer.kt create mode 100644 foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/BlockBreak.kt create mode 100644 foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/BlockPlace.kt create mode 100644 foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/EntityKill.kt create mode 100644 foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/HeimdallEvent.kt create mode 100644 foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/PlayerAdvancement.kt create mode 100644 foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/PlayerDeath.kt create mode 100644 foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/PlayerPosition.kt create mode 100644 foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/PlayerSession.kt create mode 100644 foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/WorldChange.kt create mode 100644 foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/export/ChunkExporter.kt create mode 100644 foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/export/ExportChunksCommand.kt create mode 100644 foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/model/HeimdallConfig.kt create mode 100644 foundation-heimdall/src/main/resources/heimdall.yaml create mode 100644 foundation-heimdall/src/main/resources/init.sql create mode 100644 foundation-heimdall/src/main/resources/plugin.yml create mode 100644 foundation-heimdall/src/main/resources/queries/player_positions_aggregates.sql create mode 100644 tool-gjallarhorn/build.gradle.kts create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/GjallarhornCommand.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/BlockChangeTimelapseCommand.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/ChunkExportLoaderCommand.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/ImageFormatType.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/ImageRenderType.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/PlayerPositionExport.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/PlayerSessionExport.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/export/ChunkExportLoader.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/export/CombinedChunkFormat.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/main.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockDiversityRenderer.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockGridRenderer.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockHeatMapRenderer.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockHeightMapRenderer.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockImageRenderer.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockMapRenderer.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockVerticalFillMapRenderer.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/LaunchGraphicalRenderSession.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/PlayerLocationShareRenderer.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/ui/GraphicalRenderSession.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/ui/LazyImageRenderer.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockChange.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockChangeType.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockChangelog.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockCoordinate.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockCoordinateSparseMap.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockCoordinateStore.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockExpanse.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockLogTracker.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockMapRenderPool.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockMapRenderPoolDelegate.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockMapTimelapse.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockState.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockStateMap.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockStateSerializer.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockTrackMode.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/ChangelogSlice.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/PlayerPositionChange.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/PlayerPositionChangelog.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/SparseBlockStateMap.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/SparseBlockStateMapSerializer.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/BlockColorKey.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/BlockColorKeys.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/ColorGradient.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/ColorGradientPoint.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/FloatClamp.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/ImageTools.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/compose.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/numerics.kt diff --git a/build.gradle.kts b/build.gradle.kts index e517396..4681a48 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { java id("gay.pizza.foundation.concrete-root") version "0.7.0" + id("gay.pizza.foundation.concrete-library") version "0.7.0" apply false id("gay.pizza.foundation.concrete-plugin") version "0.7.0" apply false } @@ -53,7 +54,7 @@ subprojects { tasks.withType { kotlinOptions { - freeCompilerArgs += "-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi" + freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi" } } } diff --git a/common-heimdall/build.gradle.kts b/common-heimdall/build.gradle.kts new file mode 100644 index 0000000..d8ff8cf --- /dev/null +++ b/common-heimdall/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("gay.pizza.foundation.concrete-library") +} + +dependencies { + api("org.postgresql:postgresql:42.5.1") + api("org.jetbrains.exposed:exposed-jdbc:0.41.1") + api("org.jetbrains.exposed:exposed-java-time:0.41.1") + api("com.zaxxer:HikariCP:5.0.1") +} diff --git a/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/export/ExportedBlock.kt b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/export/ExportedBlock.kt new file mode 100644 index 0000000..f668cb9 --- /dev/null +++ b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/export/ExportedBlock.kt @@ -0,0 +1,8 @@ +package gay.pizza.foundation.heimdall.export + +import kotlinx.serialization.Serializable + +@Serializable +data class ExportedBlock( + val type: String +) diff --git a/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/export/ExportedChunk.kt b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/export/ExportedChunk.kt new file mode 100644 index 0000000..92e71d9 --- /dev/null +++ b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/export/ExportedChunk.kt @@ -0,0 +1,10 @@ +package gay.pizza.foundation.heimdall.export + +import kotlinx.serialization.Serializable + +@Serializable +data class ExportedChunk( + val x: Int, + val z: Int, + val sections: List +) diff --git a/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/export/ExportedChunkSection.kt b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/export/ExportedChunkSection.kt new file mode 100644 index 0000000..0215c1f --- /dev/null +++ b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/export/ExportedChunkSection.kt @@ -0,0 +1,10 @@ +package gay.pizza.foundation.heimdall.export + +import kotlinx.serialization.Serializable + +@Serializable +data class ExportedChunkSection( + val x: Int, + val z: Int, + val blocks: List +) diff --git a/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/BlockBreakTable.kt b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/BlockBreakTable.kt new file mode 100644 index 0000000..d6e2df8 --- /dev/null +++ b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/BlockBreakTable.kt @@ -0,0 +1,16 @@ +package gay.pizza.foundation.heimdall.table + +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.javatime.timestamp + +object BlockBreakTable : Table("block_breaks") { + 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/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/BlockPlaceTable.kt b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/BlockPlaceTable.kt new file mode 100644 index 0000000..9a89da6 --- /dev/null +++ b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/BlockPlaceTable.kt @@ -0,0 +1,16 @@ +package gay.pizza.foundation.heimdall.table + +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.javatime.timestamp + +object BlockPlaceTable : Table("block_places") { + 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/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/EntityKillTable.kt b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/EntityKillTable.kt new file mode 100644 index 0000000..730ad71 --- /dev/null +++ b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/EntityKillTable.kt @@ -0,0 +1,17 @@ +package gay.pizza.foundation.heimdall.table + +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.javatime.timestamp + +object EntityKillTable : Table("entity_kills") { + val time = timestamp("time") + val player = uuid("player") + val entity = uuid("entity") + 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 entityType = text("entity_type") +} diff --git a/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/PlayerAdvancementTable.kt b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/PlayerAdvancementTable.kt new file mode 100644 index 0000000..53ddf9f --- /dev/null +++ b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/PlayerAdvancementTable.kt @@ -0,0 +1,16 @@ +package gay.pizza.foundation.heimdall.table + +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.javatime.timestamp + +object PlayerAdvancementTable : Table("player_advancements") { + 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 advancement = text("advancement") +} diff --git a/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/PlayerDeathTable.kt b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/PlayerDeathTable.kt new file mode 100644 index 0000000..17ec1c2 --- /dev/null +++ b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/PlayerDeathTable.kt @@ -0,0 +1,17 @@ +package gay.pizza.foundation.heimdall.table + +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.javatime.timestamp + +object PlayerDeathTable : Table("player_deaths") { + 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") + val experience = double("experience") + val message = text("message").nullable() +} diff --git a/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/PlayerPositionTable.kt b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/PlayerPositionTable.kt new file mode 100644 index 0000000..4e19ba6 --- /dev/null +++ b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/PlayerPositionTable.kt @@ -0,0 +1,15 @@ +package gay.pizza.foundation.heimdall.table + +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.javatime.timestamp + +object PlayerPositionTable : Table("player_positions") { + 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") +} diff --git a/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/PlayerSessionTable.kt b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/PlayerSessionTable.kt new file mode 100644 index 0000000..e5f3610 --- /dev/null +++ b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/PlayerSessionTable.kt @@ -0,0 +1,12 @@ +package gay.pizza.foundation.heimdall.table + +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.javatime.timestamp + +object PlayerSessionTable : Table("player_sessions") { + val id = uuid("id") + val player = uuid("player") + val name = text("name") + val startTime = timestamp("start") + val endTime = timestamp("end") +} diff --git a/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/WorldChangeTable.kt b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/WorldChangeTable.kt new file mode 100644 index 0000000..b24fd4a --- /dev/null +++ b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/table/WorldChangeTable.kt @@ -0,0 +1,13 @@ +package gay.pizza.foundation.heimdall.table + +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.javatime.timestamp + +object WorldChangeTable : Table("world_changes") { + val time = timestamp("time") + val player = uuid("player") + val fromWorld = uuid("from_world") + val toWorld = uuid("to_world") + val fromWorldName = text("from_world_name") + val toWorldName = text("to_world_name") +} diff --git a/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/view/BlockChangeView.kt b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/view/BlockChangeView.kt new file mode 100644 index 0000000..5fe0b78 --- /dev/null +++ b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/view/BlockChangeView.kt @@ -0,0 +1,17 @@ +package gay.pizza.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/build.gradle.kts b/foundation-heimdall/build.gradle.kts new file mode 100644 index 0000000..0d25b36 --- /dev/null +++ b/foundation-heimdall/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("gay.pizza.foundation.concrete-plugin") +} + +dependencies { + api(project(":common-heimdall")) +} diff --git a/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/Extensions.kt b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/Extensions.kt new file mode 100644 index 0000000..000cbec --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/Extensions.kt @@ -0,0 +1,21 @@ +package gay.pizza.foundation.heimdall.plugin + +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/gay/pizza/foundation/heimdall/plugin/HeimdallPlugin.kt b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/HeimdallPlugin.kt new file mode 100644 index 0000000..8438992 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/HeimdallPlugin.kt @@ -0,0 +1,201 @@ +package gay.pizza.foundation.heimdall.plugin + +import gay.pizza.foundation.heimdall.plugin.buffer.BufferFlushThread +import gay.pizza.foundation.heimdall.plugin.buffer.EventBuffer +import gay.pizza.foundation.heimdall.plugin.event.* +import gay.pizza.foundation.heimdall.plugin.model.HeimdallConfig +import gay.pizza.foundation.heimdall.plugin.export.ExportChunksCommand +import com.charleskorn.kaml.Yaml +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.block.BlockBreakEvent +import org.bukkit.event.block.BlockPlaceEvent +import org.bukkit.event.entity.EntityDeathEvent +import org.bukkit.event.entity.PlayerDeathEvent +import org.bukkit.event.player.* +import org.bukkit.plugin.java.JavaPlugin +import org.jetbrains.exposed.sql.Database +import org.postgresql.Driver +import org.slf4j.Logger +import java.nio.file.Path +import java.time.Duration +import java.time.Instant +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import kotlin.io.path.inputStream + +class HeimdallPlugin : JavaPlugin(), Listener { + private lateinit var config: HeimdallConfig + private lateinit var pool: HikariDataSource + internal var db: Database? = null + + private val buffer = EventBuffer() + private val bufferFlushThread = BufferFlushThread(this, buffer) + + private val playerJoinTimes = ConcurrentHashMap() + + private val legacyComponentSerializer = LegacyComponentSerializer.builder().build() + + override fun onEnable() { + val exportChunksCommand = getCommand("export_all_chunks") ?: throw Exception("Failed to get export_all_chunks command") + exportChunksCommand.setExecutor(ExportChunksCommand(this)) + + val pluginDataPath = dataFolder.toPath() + pluginDataPath.toFile().mkdir() + + val configPath = copyDefaultConfig( + slF4JLogger, + 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 + maximumPoolSize = 10 + idleTimeout = Duration.ofMinutes(5).toMillis() + maxLifetime = Duration.ofMinutes(10).toMillis() + }) + val initMigrationContent = HeimdallPlugin::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(PlayerPosition(event)) + + @EventHandler + fun onBlockBroken(event: BlockPlaceEvent) = buffer.push(BlockPlace(event)) + + @EventHandler + fun onBlockBroken(event: BlockBreakEvent) = buffer.push(BlockBreak(event)) + + @EventHandler + fun onPlayerJoin(event: PlayerJoinEvent) { + playerJoinTimes[event.player.uniqueId] = Instant.now() + } + + @EventHandler + fun onPlayerQuit(event: PlayerQuitEvent) { + val startTime = playerJoinTimes.remove(event.player.uniqueId) ?: return + val endTime = Instant.now() + buffer.push(PlayerSession(event.player.uniqueId, event.player.name, startTime, endTime)) + } + + @EventHandler + fun onPlayerDeath(event: PlayerDeathEvent) { + val deathMessage = event.deathMessage() + val deathMessageString = if (deathMessage != null) { + legacyComponentSerializer.serialize(deathMessage) + } else { + null + } + buffer.push(PlayerDeath(event, deathMessageString)) + } + + @EventHandler + fun onPlayerAdvancementDone(event: PlayerAdvancementDoneEvent) = buffer.push(PlayerAdvancement(event)) + + @EventHandler + fun onWorldLoad(event: PlayerChangedWorldEvent) = buffer.push( + WorldChange( + event.player.uniqueId, + event.from.uid, + event.from.name, + event.player.world.uid, + event.player.world.name + ) + ) + + @EventHandler + fun onEntityDeath(event: EntityDeathEvent) { + val killer = event.entity.killer ?: return + buffer.push( + EntityKill( + killer.uniqueId, + killer.location, + event.entity.uniqueId, + event.entityType.key.toString() + ) + ) + } + + override fun onDisable() { + bufferFlushThread.stop() + val endTime = Instant.now() + for (playerId in playerJoinTimes.keys().toList()) { + val startTime = playerJoinTimes.remove(playerId) ?: continue + buffer.push(PlayerSession( + playerId, + server.getPlayer(playerId)?.name ?: "__unknown__", + startTime, + endTime + )) + } + bufferFlushThread.flush() + } + + private inline fun copyDefaultConfig(log: Logger, targetPath: Path, resourceName: String): Path { + if (resourceName.startsWith("/")) { + throw IllegalArgumentException("resourceName starts with slash") + } + + if (!targetPath.toFile().exists()) { + throw Exception("Configuration output path does not exist!") + } + val outPath = targetPath.resolve(resourceName) + val outFile = outPath.toFile() + if (outFile.exists()) { + log.debug("Configuration file already exists.") + return outPath + } + + val resourceStream = T::class.java.getResourceAsStream("/$resourceName") + ?: throw Exception("Configuration resource does not exist!") + val outputStream = outFile.outputStream() + + resourceStream.use { + outputStream.use { + log.info("Copied default configuration to $outPath") + resourceStream.copyTo(outputStream) + } + } + + return outPath + } +} diff --git a/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/buffer/BufferFlushThread.kt b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/buffer/BufferFlushThread.kt new file mode 100644 index 0000000..6daa7af --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/buffer/BufferFlushThread.kt @@ -0,0 +1,49 @@ +package gay.pizza.foundation.heimdall.plugin.buffer + +import gay.pizza.foundation.heimdall.plugin.HeimdallPlugin +import org.jetbrains.exposed.sql.transactions.transaction +import java.util.concurrent.atomic.AtomicBoolean + +class BufferFlushThread(val plugin: HeimdallPlugin, val buffer: EventBuffer) { + private val running = AtomicBoolean(false) + private var thread: Thread? = null + + fun start() { + running.set(true) + val thread = Thread { + plugin.slF4JLogger.info("Buffer Flusher Started") + while (running.get()) { + flush() + Thread.sleep(5000) + } + plugin.slF4JLogger.info("Buffer Flusher Stopped") + } + thread.name = "Heimdall Buffer Flush" + thread.isDaemon = false + thread.start() + this.thread = thread + } + + fun stop() { + running.set(false) + thread?.join() + } + + fun flush() { + try { + val db = plugin.db + if (db == null) { + buffer.clear() + return + } + transaction(plugin.db) { + val count = buffer.flush(this) + if (count > 0) { + plugin.slF4JLogger.debug("Flushed $count Events") + } + } + } catch (e: Exception) { + plugin.slF4JLogger.warn("Failed to flush buffer.", e) + } + } +} diff --git a/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/buffer/EventBuffer.kt b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/buffer/EventBuffer.kt new file mode 100644 index 0000000..e83be87 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/buffer/EventBuffer.kt @@ -0,0 +1,28 @@ +package gay.pizza.foundation.heimdall.plugin.buffer + +import gay.pizza.foundation.heimdall.plugin.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) + } + + fun clear() { + events = mutableListOf() + } +} diff --git a/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/BlockBreak.kt b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/BlockBreak.kt new file mode 100644 index 0000000..74ba363 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/BlockBreak.kt @@ -0,0 +1,35 @@ +package gay.pizza.foundation.heimdall.plugin.event + +import gay.pizza.foundation.heimdall.table.BlockBreakTable +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.event.block.BlockBreakEvent +import org.jetbrains.exposed.sql.Transaction +import org.jetbrains.exposed.sql.insert +import java.time.Instant +import java.util.* + +class BlockBreak( + val playerUniqueIdentity: UUID, + val location: Location, + val material: Material, + val timestamp: Instant = Instant.now() +) : HeimdallEvent() { + constructor(event: BlockBreakEvent) : this(event.player.uniqueId, event.block.location, event.block.type) + + override fun store(transaction: Transaction) { + transaction.apply { + BlockBreakTable.insert { + it[time] = timestamp + 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() + it[block] = material.key.toString() + } + } + } +} diff --git a/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/BlockPlace.kt b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/BlockPlace.kt new file mode 100644 index 0000000..215b9dc --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/BlockPlace.kt @@ -0,0 +1,35 @@ +package gay.pizza.foundation.heimdall.plugin.event + +import gay.pizza.foundation.heimdall.table.BlockPlaceTable +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.event.block.BlockPlaceEvent +import org.jetbrains.exposed.sql.Transaction +import org.jetbrains.exposed.sql.insert +import java.time.Instant +import java.util.* + +class BlockPlace( + val playerUniqueIdentity: UUID, + val location: Location, + val material: Material, + val timestamp: Instant = Instant.now() +) : HeimdallEvent() { + constructor(event: BlockPlaceEvent) : this(event.player.uniqueId, event.block.location, event.block.type) + + override fun store(transaction: Transaction) { + transaction.apply { + BlockPlaceTable.insert { + it[time] = timestamp + 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() + it[block] = material.key.toString() + } + } + } +} diff --git a/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/EntityKill.kt b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/EntityKill.kt new file mode 100644 index 0000000..222e1e1 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/EntityKill.kt @@ -0,0 +1,33 @@ +package gay.pizza.foundation.heimdall.plugin.event + +import gay.pizza.foundation.heimdall.table.EntityKillTable +import org.bukkit.Location +import org.jetbrains.exposed.sql.Transaction +import org.jetbrains.exposed.sql.insert +import java.time.Instant +import java.util.* + +class EntityKill( + val playerUniqueIdentity: UUID, + val location: Location, + val entityUniqueIdentity: UUID, + val entityTypeName: String, + val timestamp: Instant = Instant.now() +) : HeimdallEvent() { + override fun store(transaction: Transaction) { + transaction.apply { + EntityKillTable.insert { + it[time] = timestamp + 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() + it[entity] = entityUniqueIdentity + it[entityType] = entityTypeName + } + } + } +} diff --git a/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/HeimdallEvent.kt b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/HeimdallEvent.kt new file mode 100644 index 0000000..0bdf203 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/HeimdallEvent.kt @@ -0,0 +1,7 @@ +package gay.pizza.foundation.heimdall.plugin.event + +import org.jetbrains.exposed.sql.Transaction + +abstract class HeimdallEvent { + abstract fun store(transaction: Transaction) +} diff --git a/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/PlayerAdvancement.kt b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/PlayerAdvancement.kt new file mode 100644 index 0000000..414bbbc --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/PlayerAdvancement.kt @@ -0,0 +1,35 @@ +package gay.pizza.foundation.heimdall.plugin.event + +import gay.pizza.foundation.heimdall.table.PlayerAdvancementTable +import org.bukkit.Location +import org.bukkit.advancement.Advancement +import org.bukkit.event.player.PlayerAdvancementDoneEvent +import org.jetbrains.exposed.sql.Transaction +import org.jetbrains.exposed.sql.insert +import java.time.Instant +import java.util.* + +class PlayerAdvancement( + val playerUniqueIdentity: UUID, + val location: Location, + val advancement: Advancement, + val timestamp: Instant = Instant.now() +) : HeimdallEvent() { + constructor(event: PlayerAdvancementDoneEvent) : this(event.player.uniqueId, event.player.location, event.advancement) + + override fun store(transaction: Transaction) { + transaction.apply { + PlayerAdvancementTable.insert { + it[time] = timestamp + 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() + it[advancement] = this@PlayerAdvancement.advancement.key.toString() + } + } + } +} diff --git a/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/PlayerDeath.kt b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/PlayerDeath.kt new file mode 100644 index 0000000..812b162 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/PlayerDeath.kt @@ -0,0 +1,41 @@ +package gay.pizza.foundation.heimdall.plugin.event + +import gay.pizza.foundation.heimdall.table.PlayerDeathTable +import org.bukkit.Location +import org.bukkit.event.entity.PlayerDeathEvent +import org.jetbrains.exposed.sql.Transaction +import org.jetbrains.exposed.sql.insert +import java.time.Instant +import java.util.* + +class PlayerDeath( + val playerUniqueIdentity: UUID, + val location: Location, + val experienceLevel: Float, + val deathMessage: String?, + val timestamp: Instant = Instant.now() +) : HeimdallEvent() { + constructor(event: PlayerDeathEvent, deathMessage: String? = null) : this( + event.player.uniqueId, + event.player.location, + event.player.exp, + deathMessage + ) + + override fun store(transaction: Transaction) { + transaction.apply { + PlayerDeathTable.insert { + it[time] = timestamp + 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() + it[experience] = experienceLevel.toDouble() + it[message] = deathMessage + } + } + } +} diff --git a/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/PlayerPosition.kt b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/PlayerPosition.kt new file mode 100644 index 0000000..3123c47 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/PlayerPosition.kt @@ -0,0 +1,32 @@ +package gay.pizza.foundation.heimdall.plugin.event + +import gay.pizza.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 PlayerPosition( + val playerUniqueIdentity: UUID, + val location: Location, + val timestamp: Instant = Instant.now() +) : HeimdallEvent() { + constructor(event: PlayerMoveEvent) : this(event.player.uniqueId, event.to) + + override fun store(transaction: Transaction) { + transaction.apply { + PlayerPositionTable.insert { + it[time] = timestamp + 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/gay/pizza/foundation/heimdall/plugin/event/PlayerSession.kt b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/PlayerSession.kt new file mode 100644 index 0000000..dd93549 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/PlayerSession.kt @@ -0,0 +1,26 @@ +package gay.pizza.foundation.heimdall.plugin.event + +import gay.pizza.foundation.heimdall.table.PlayerSessionTable +import org.jetbrains.exposed.sql.Transaction +import org.jetbrains.exposed.sql.insert +import java.time.Instant +import java.util.* + +class PlayerSession( + val playerUniqueIdentity: UUID, + val playerName: String, + val startTimeInstant: Instant, + val endTimeInstant: Instant +) : HeimdallEvent() { + override fun store(transaction: Transaction) { + transaction.apply { + PlayerSessionTable.insert { + it[id] = UUID.randomUUID() + it[player] = playerUniqueIdentity + it[name] = playerName + it[startTime] = startTimeInstant + it[endTime] = endTimeInstant + } + } + } +} diff --git a/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/WorldChange.kt b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/WorldChange.kt new file mode 100644 index 0000000..7974955 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/event/WorldChange.kt @@ -0,0 +1,29 @@ +package gay.pizza.foundation.heimdall.plugin.event + +import gay.pizza.foundation.heimdall.table.WorldChangeTable +import org.jetbrains.exposed.sql.Transaction +import org.jetbrains.exposed.sql.insert +import java.time.Instant +import java.util.* + +class WorldChange( + val playerUniqueIdentity: UUID, + val fromWorldId: UUID, + val fromWorldActualName: String, + val toWorldId: UUID, + val toWorldActualName: String, + val timestamp: Instant = Instant.now() +) : HeimdallEvent() { + override fun store(transaction: Transaction) { + transaction.apply { + WorldChangeTable.insert { + it[time] = timestamp + it[player] = playerUniqueIdentity + it[fromWorld] = fromWorldId + it[fromWorldName] = fromWorldActualName + it[toWorld] = toWorldId + it[toWorldName] = toWorldActualName + } + } + } +} diff --git a/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/export/ChunkExporter.kt b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/export/ChunkExporter.kt new file mode 100644 index 0000000..c571926 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/export/ChunkExporter.kt @@ -0,0 +1,70 @@ +package gay.pizza.foundation.heimdall.plugin.export + +import gay.pizza.foundation.heimdall.export.ExportedBlock +import gay.pizza.foundation.heimdall.export.ExportedChunk +import gay.pizza.foundation.heimdall.export.ExportedChunkSection +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToStream +import org.bukkit.Chunk +import org.bukkit.ChunkSnapshot +import org.bukkit.World +import org.bukkit.plugin.Plugin +import java.io.File +import java.util.zip.GZIPOutputStream + +class ChunkExporter(private val plugin: Plugin, val world: World) { + private val json = Json { + ignoreUnknownKeys = true + } + + fun exportLoadedChunksAsync() { + exportChunkListAsync(world.loadedChunks.toList()) + } + + private fun exportChunkListAsync(chunks: List) { + plugin.slF4JLogger.info("Exporting ${chunks.size} Chunks") + val snapshots = chunks.map { it.chunkSnapshot } + Thread { + for (snapshot in snapshots) { + exportChunkSnapshot(snapshot) + } + plugin.slF4JLogger.info("Exported ${chunks.size} Chunks") + }.start() + } + + private fun exportChunkSnapshot(snapshot: ChunkSnapshot) { + val sections = mutableListOf() + val yRange = world.minHeight until world.maxHeight + val chunkRange = 0..15 + for (x in chunkRange) { + for (z in chunkRange) { + sections.add(exportChunkSection(snapshot, yRange, x, z)) + } + } + + val exported = ExportedChunk(snapshot.x, snapshot.z, sections) + saveChunkSnapshot(snapshot, exported) + } + + private fun saveChunkSnapshot(snapshot: ChunkSnapshot, chunk: ExportedChunk) { + val file = File("exported_chunks/${snapshot.worldName}_chunk_${snapshot.x}_${snapshot.z}.json.gz") + if (!file.parentFile.exists()) { + file.parentFile.mkdirs() + } + + val fileOutputStream = file.outputStream() + val gzipOutputStream = GZIPOutputStream(fileOutputStream) + json.encodeToStream(ExportedChunk.serializer(), chunk, gzipOutputStream) + gzipOutputStream.close() + } + + private fun exportChunkSection(snapshot: ChunkSnapshot, yRange: IntRange, x: Int, z: Int): ExportedChunkSection { + val blocks = mutableListOf() + for (y in yRange) { + val blockData = snapshot.getBlockData(x, y, z) + val block = ExportedBlock(blockData.material.key.toString()) + blocks.add(block) + } + return ExportedChunkSection(x, z, blocks) + } +} diff --git a/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/export/ExportChunksCommand.kt b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/export/ExportChunksCommand.kt new file mode 100644 index 0000000..4729883 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/export/ExportChunksCommand.kt @@ -0,0 +1,17 @@ +package gay.pizza.foundation.heimdall.plugin.export + +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandSender +import org.bukkit.plugin.Plugin + +class ExportChunksCommand(private val plugin: Plugin) : CommandExecutor { + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + plugin.slF4JLogger.info("Exporting All Chunks") + for (world in sender.server.worlds) { + val export = ChunkExporter(plugin, world) + export.exportLoadedChunksAsync() + } + return true + } +} diff --git a/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/model/HeimdallConfig.kt b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/model/HeimdallConfig.kt new file mode 100644 index 0000000..74ccb62 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/model/HeimdallConfig.kt @@ -0,0 +1,16 @@ +package gay.pizza.foundation.heimdall.plugin.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/resources/heimdall.yaml b/foundation-heimdall/src/main/resources/heimdall.yaml new file mode 100644 index 0000000..6e76f0b --- /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/heimdall" + # JDBC Username + username: "heimdall" + # JDBC Password + password: "heimdall" diff --git a/foundation-heimdall/src/main/resources/init.sql b/foundation-heimdall/src/main/resources/init.sql new file mode 100644 index 0000000..aee8855 --- /dev/null +++ b/foundation-heimdall/src/main/resources/init.sql @@ -0,0 +1,147 @@ +create extension if not exists "uuid-ossp"; +-- +create extension if not exists timescaledb; +-- +create schema if not exists heimdall; +-- +create table if not exists 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('player_positions', 'time', 'player', 4, if_not_exists => TRUE); +-- +alter table player_positions set ( + timescaledb.compress, + timescaledb.compress_segmentby = 'player,world', + timescaledb.compress_orderby = 'time' +); +-- +select add_compression_policy('player_positions', interval '3 days', if_not_exists => true); +-- +create table if not exists block_breaks ( + 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, + block text not null, + PRIMARY KEY (time, player, world) +); +-- +select create_hypertable('block_breaks', 'time', 'player', 4, if_not_exists => TRUE); +-- +create table if not exists block_places ( + 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, + block text not null, + PRIMARY KEY (time, player, world) +); +-- +select create_hypertable('block_places', 'time', 'player', 4, if_not_exists => TRUE); +-- +create table if not exists player_sessions ( + id uuid not null, + player uuid not null, + name text not null, + "start" timestamp not null, + "end" timestamp not null, + primary key (id, player, start) +); +-- +select create_hypertable('player_sessions', 'start', 'player', 4, if_not_exists => TRUE); +-- +create table if not exists world_changes ( + time timestamp not null, + player uuid not null, + from_world uuid not null, + from_world_name text not null, + to_world uuid not null, + to_world_name text not null, + primary key (time, player) +); +-- +select create_hypertable('world_changes', 'time', 'player', 4, if_not_exists => TRUE); +-- +create table if not exists player_deaths ( + 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, + experience double precision not null, + message text null, + primary key (time, player) +); +-- +select create_hypertable('player_deaths', 'time', 'player', 4, if_not_exists => TRUE); +-- +create table if not exists player_advancements ( + 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, + advancement text not null, + primary key (time, player, advancement) +); +-- +select create_hypertable('player_advancements', 'time', 'player', 4, if_not_exists => TRUE); +-- +create table if not exists entity_kills ( + time timestamp not null, + player uuid not null, + entity 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, + entity_type text not null, + primary key (time, entity, player) +); +-- +select create_hypertable('entity_kills', 'time', 'player', 4, if_not_exists => TRUE); +-- +create or replace view block_changes as + select true as break, * + from block_breaks + union all + select false as break, * from block_places; +-- +create or replace view player_names as + with unique_player_ids as ( + select distinct player + from player_sessions + ) + select player, ( + select name + from player_sessions + where player = unique_player_ids.player + order by "end" desc + limit 1 + ) as name + from unique_player_ids; diff --git a/foundation-heimdall/src/main/resources/plugin.yml b/foundation-heimdall/src/main/resources/plugin.yml new file mode 100644 index 0000000..a298d0c --- /dev/null +++ b/foundation-heimdall/src/main/resources/plugin.yml @@ -0,0 +1,13 @@ +name: Heimdall +version: '${version}' +main: io.kexec.heimdall.plugin.HeimdallPlugin +api-version: 1.18 +prefix: Heimdall +load: STARTUP +authors: + - kendfinger +commands: + export_all_chunks: + description: Export All Chunks + usage: /export_all_chunks + permission: heimdall.command.export_all_chunks diff --git a/foundation-heimdall/src/main/resources/queries/player_positions_aggregates.sql b/foundation-heimdall/src/main/resources/queries/player_positions_aggregates.sql new file mode 100644 index 0000000..f06b531 --- /dev/null +++ b/foundation-heimdall/src/main/resources/queries/player_positions_aggregates.sql @@ -0,0 +1,64 @@ +WITH + unique_player_ids AS ( + SELECT + DISTINCT player + FROM player_sessions + ), + player_names AS ( + SELECT + player, + ( + SELECT name + FROM player_sessions + WHERE player = unique_player_ids.player + ORDER BY "end" DESC + LIMIT 1 + ) AS name + FROM unique_player_ids + ), + unique_world_ids AS ( + SELECT + DISTINCT to_world AS world + FROM world_changes + ), + world_names AS ( + SELECT + world, + ( + SELECT to_world_name + FROM world_changes + WHERE world = world_changes.to_world + ORDER BY time DESC + LIMIT 1 + ) AS name + FROM unique_world_ids + ), + player_calculated_positions AS ( + SELECT + player, + world, + AVG(x) AS avg_x, + AVG(y) AS avg_y, + AVG(z) AS avg_z, + MAX(x) AS max_x, + MAX(y) AS max_y, + MAX(z) AS max_z, + MIN(x) AS min_x, + MIN(y) AS min_y, + MIN(z) AS min_z, + COUNT(*) AS count, + MODE() WITHIN GROUP (ORDER BY x) AS mode_x, + MODE() WITHIN GROUP (ORDER BY y) AS mode_y, + MODE() WITHIN GROUP (ORDER BY z) AS mode_z + FROM player_positions + GROUP BY player, world + ) +SELECT + player_names.name AS player_name, + world_names.name AS world_name, + player_calculated_positions.* +FROM player_calculated_positions +JOIN player_names + ON player_names.player = player_calculated_positions.player +JOIN world_names + ON world_names.world = player_calculated_positions.world diff --git a/settings.gradle.kts b/settings.gradle.kts index 62799c7..df90541 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,7 +9,10 @@ pluginManagement { } include( + ":common-heimdall", ":foundation-core", ":foundation-bifrost", ":foundation-chaos", + ":foundation-heimdall", + ":tool-gjallarhorn", ) diff --git a/tool-gjallarhorn/build.gradle.kts b/tool-gjallarhorn/build.gradle.kts new file mode 100644 index 0000000..46ba1d9 --- /dev/null +++ b/tool-gjallarhorn/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + id("gay.pizza.foundation.concrete-library") + id("com.github.johnrengelman.shadow") +} + +dependencies { + api(project(":common-heimdall")) + + implementation("com.github.ajalt.clikt:clikt:3.5.0") + implementation("org.slf4j:slf4j-simple:1.7.36") +} + +tasks.jar { + manifest.attributes( + "Main-Class" to "io.kexec.heimdall.tool.MainKt" + ) +} + +tasks.assemble { + dependsOn("shadowJar") +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/GjallarhornCommand.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/GjallarhornCommand.kt new file mode 100644 index 0000000..37d1f6f --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/GjallarhornCommand.kt @@ -0,0 +1,37 @@ +package gay.pizza.foundation.heimdall.tool + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.int +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import org.jetbrains.exposed.sql.Database +import java.time.Duration + +class GjallarhornCommand : CliktCommand(invokeWithoutSubcommand = true) { + private val jdbcConnectionUrl by option("-c", "--connection-url", help = "JDBC Connection URL") + .default("jdbc:postgresql://localhost/heimdall") + + private val jdbcConnectionUsername by option("-u", "--connection-username", help = "JDBC Connection Username") + .default("heimdall") + + private val jdbcConnectionPassword by option("-p", "--connection-password", help = "JDBC Connection Password") + .default("heimdall") + + private val dbPoolSize by option("--db-pool-size", help = "JDBC Pool Size").int().default(8) + + override fun run() { + val pool = HikariDataSource(HikariConfig().apply { + jdbcUrl = jdbcConnectionUrl + username = jdbcConnectionUsername + password = jdbcConnectionPassword + minimumIdle = dbPoolSize / 2 + maximumPoolSize = dbPoolSize + idleTimeout = Duration.ofMinutes(5).toMillis() + maxLifetime = Duration.ofMinutes(10).toMillis() + }) + val db = Database.connect(pool) + currentContext.findOrSetObject { db } + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/BlockChangeTimelapseCommand.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/BlockChangeTimelapseCommand.kt new file mode 100644 index 0000000..fe34aed --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/BlockChangeTimelapseCommand.kt @@ -0,0 +1,156 @@ +package gay.pizza.foundation.heimdall.tool.commands + +import gay.pizza.foundation.heimdall.tool.render.* +import gay.pizza.foundation.heimdall.tool.state.* +import gay.pizza.foundation.heimdall.tool.util.compose +import gay.pizza.foundation.heimdall.tool.util.savePngFile +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.requireObject +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import com.github.ajalt.clikt.parameters.types.enum +import com.github.ajalt.clikt.parameters.types.int +import gay.pizza.foundation.heimdall.view.BlockChangeView +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SqlExpressionBuilder.greaterEq +import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq +import org.jetbrains.exposed.sql.and +import org.slf4j.LoggerFactory +import java.awt.Color +import java.awt.Font +import java.awt.font.TextLayout +import java.awt.image.BufferedImage +import java.time.Duration +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ScheduledThreadPoolExecutor + +class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name = "block-change-timelapse") { + private val db by requireObject() + private val timelapseIntervalLimit by option("--timelapse-limit", help = "Timelapse Limit Intervals").int() + private val timelapseMode by option("--timelapse", help = "Timelapse Mode").enum { it.id }.required() + private val timelapseSpeedChangeThreshold by option( + "--timelapse-change-speed-threshold", + help = "Timelapse Change Speed Threshold" + ).int() + private val timelapseSpeedChangeMinimumIntervalSeconds by option( + "--timelapse-change-speed-minimum-interval-seconds", + help = "Timelapse Change Speed Minimum Interval Seconds" + ).int() + + private val render by option("--render", help = "Render Top Down Image").enum { it.id }.required() + private val renderImageFormat by option("--render-image-format", help = "Render Image Format") + .enum { it.id } + .default(ImageFormatType.Png) + + private val considerAirBlocks by option("--consider-air-blocks", help = "Enable Air Block Consideration").flag() + + private val fromCoordinate by option("--trim-from", help = "Trim From Coordinate") + private val toCoordinate by option("--trim-to", help = "Trim To Coordinate") + + private val parallelPoolSize by option("--pool-size", help = "Task Pool Size").int().default(8) + private val inMemoryRender by option("--in-memory-render", help = "Render Images to Memory").flag() + private val shouldRenderLoop by option("--loop-render", help = "Loop Render").flag() + private val quadPixelNoop by option("--quad-pixel-noop", help = "Disable Quad Pixel Render").flag() + + private val logger = LoggerFactory.getLogger(BlockChangeTimelapseCommand::class.java) + + override fun run() { + if (quadPixelNoop) { + BlockGridRenderer.globalQuadPixelNoop = true + } + val threadPoolExecutor = ScheduledThreadPoolExecutor(parallelPoolSize) + if (shouldRenderLoop) { + while (true) { + perform(threadPoolExecutor) + } + } else { + perform(threadPoolExecutor) + } + threadPoolExecutor.shutdown() + } + + private fun perform(threadPoolExecutor: ScheduledThreadPoolExecutor) { + val trim = maybeBuildTrim() + val filter = compose( + combine = { a, b -> a and b }, + { trim?.first?.x != null } to { gay.pizza.foundation.heimdall.view.BlockChangeView.x greaterEq trim!!.first.x }, + { trim?.first?.z != null } to { gay.pizza.foundation.heimdall.view.BlockChangeView.z greaterEq trim!!.first.z }, + { trim?.second?.x != null } to { gay.pizza.foundation.heimdall.view.BlockChangeView.x lessEq trim!!.second.x }, + { trim?.second?.z != null } to { gay.pizza.foundation.heimdall.view.BlockChangeView.z lessEq trim!!.second.z } + ) + + val changelog = BlockChangelog.query(db, filter) + logger.info("Block Changelog: ${changelog.changes.size} changes") + val timelapse = BlockMapTimelapse() + var slices = changelog.calculateChangelogSlices(timelapseMode.interval, timelapseIntervalLimit) + + if (timelapseSpeedChangeThreshold != null && timelapseSpeedChangeMinimumIntervalSeconds != null) { + val minimumInterval = Duration.ofSeconds(timelapseSpeedChangeMinimumIntervalSeconds!!.toLong()) + val blockChangeThreshold = timelapseSpeedChangeThreshold!! + + slices = changelog.splitChangelogSlicesWithThreshold(blockChangeThreshold, minimumInterval, slices) + } + + logger.info("Timelapse Slices: ${slices.size} slices") + + val imagePadCount = slices.size.toString().length + + val inMemoryPool = if (inMemoryRender) { + ConcurrentHashMap() + } else { + null + } + + val pool = BlockMapRenderPool( + changelog = changelog, + blockTrackMode = if (considerAirBlocks) BlockTrackMode.AirOnDelete else BlockTrackMode.RemoveOnDelete, + delegate = timelapse, + createRendererFunction = { expanse -> render.createNewRenderer(expanse, db) }, + threadPoolExecutor = threadPoolExecutor + ) { slice, result -> + val speed = slice.sliceRelativeDuration.toSeconds().toDouble() / timelapseMode.interval.toSeconds().toDouble() + val graphics = result.createGraphics() + val font = Font.decode("Arial Black").deriveFont(24.0f) + graphics.color = Color.black + graphics.font = font + val context = graphics.fontRenderContext + val text = String.format("%s @ %.4f speed (1 frame = %s sec)", slice.sliceEndTime, speed, slice.sliceRelativeDuration.toSeconds()) + val layout = + TextLayout(text, font, context) + layout.draw(graphics, 60f, 60f) + graphics.dispose() + val index = slices.indexOf(slice) + 1 + if (inMemoryRender) { + inMemoryPool?.put(slice, result) + } else { + val suffix = "-${index.toString().padStart(imagePadCount, '0')}" + renderImageFormat.save(result, "${render.id}${suffix}.${renderImageFormat.extension}") + } + logger.info("Rendered Timelapse Slice $index") + } + + pool.render(slices) + } + + private fun maybeBuildTrim(): Pair? { + if (fromCoordinate == null || toCoordinate == null) { + return null + } + + val from = fromCoordinate!!.split(",").map { it.toLong() } + val to = toCoordinate!!.split(",").map { it.toLong() } + + val fromBlock = BlockCoordinate(from[0], 0, from[1]) + val toBlock = BlockCoordinate(to[0], 0, to[1]) + return fromBlock to toBlock + } + + @Suppress("unused") + enum class TimelapseMode(val id: String, val interval: Duration) { + ByHour("hours", Duration.ofHours(1)), + ByDay("days", Duration.ofDays(1)), + ByFifteenMinutes("fifteen-minutes", Duration.ofMinutes(15)) + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/ChunkExportLoaderCommand.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/ChunkExportLoaderCommand.kt new file mode 100644 index 0000000..3c109ca --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/ChunkExportLoaderCommand.kt @@ -0,0 +1,59 @@ +package gay.pizza.foundation.heimdall.tool.commands + +import gay.pizza.foundation.heimdall.tool.export.ChunkExportLoader +import gay.pizza.foundation.heimdall.tool.export.CombinedChunkFormat +import gay.pizza.foundation.heimdall.tool.state.BlockExpanse +import gay.pizza.foundation.heimdall.tool.state.BlockLogTracker +import gay.pizza.foundation.heimdall.tool.state.ChangelogSlice +import gay.pizza.foundation.heimdall.tool.util.savePngFile +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.requireObject +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.enum +import com.github.ajalt.clikt.parameters.types.int +import com.github.ajalt.clikt.parameters.types.path +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import kotlinx.serialization.json.encodeToStream +import org.jetbrains.exposed.sql.Database + +class ChunkExportLoaderCommand : CliktCommand("Chunk Export Loader", name = "chunk-export-loader") { + private val db by requireObject() + + private val exportDirectoryPath by argument("export-directory-path").path() + private val world by argument("world") + private val chunkLoadLimit by option("--chunk-limit", help = "Chunk Limit").int() + private val render by option("--render", help = "Render Top Down Image").enum { it.id } + private val loadCombinedFormat by option("--load-combined-format").flag() + private val saveCombinedFormat by option("--save-combined-format").flag() + + override fun run() { + val combinedFormatFile = exportDirectoryPath.resolve("combined.json").toFile() + val format = if (loadCombinedFormat) { + Json.decodeFromStream(CombinedChunkFormat.serializer(), combinedFormatFile.inputStream()) + } else { + val tracker = BlockLogTracker(isConcurrent = true) + val loader = ChunkExportLoader(tracker = tracker) + loader.loadAllChunksForWorld(exportDirectoryPath, world, fast = true, limit = chunkLoadLimit) + val expanse = BlockExpanse.zeroOffsetAndMax(tracker.calculateZeroBlockOffset(), tracker.calculateMaxBlock()) + val map = tracker.buildBlockMap(expanse.offset) + CombinedChunkFormat(expanse, map) + } + + if (render != null) { + val renderer = render!!.createNewRenderer(format.expanse, db) + val image = renderer.render(ChangelogSlice.none, format.map) + image.savePngFile("full.png") + } + + if (saveCombinedFormat) { + if (combinedFormatFile.exists()) { + combinedFormatFile.delete() + } + Json.encodeToStream(CombinedChunkFormat.serializer(), format, combinedFormatFile.outputStream()) + } + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/ImageFormatType.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/ImageFormatType.kt new file mode 100644 index 0000000..d7034db --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/ImageFormatType.kt @@ -0,0 +1,10 @@ +package gay.pizza.foundation.heimdall.tool.commands + +import gay.pizza.foundation.heimdall.tool.util.saveJpegFile +import gay.pizza.foundation.heimdall.tool.util.savePngFile +import java.awt.image.BufferedImage + +enum class ImageFormatType(val id: String, val extension: String, val save: (BufferedImage, String) -> Unit) { + Png("png", "png", { image, path -> image.savePngFile(path) }), + Jpeg("jpeg", "jpg", { image, path -> image.saveJpegFile(path) }) +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/ImageRenderType.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/ImageRenderType.kt new file mode 100644 index 0000000..a3ed0ab --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/ImageRenderType.kt @@ -0,0 +1,16 @@ +package gay.pizza.foundation.heimdall.tool.commands + +import gay.pizza.foundation.heimdall.tool.render.* +import gay.pizza.foundation.heimdall.tool.state.BlockExpanse +import org.jetbrains.exposed.sql.Database + +@Suppress("unused") +enum class ImageRenderType( + val id: String, + val createNewRenderer: (BlockExpanse, Database) -> BlockImageRenderer +) { + BlockDiversity("block-diversity", { expanse, _ -> BlockDiversityRenderer(expanse) }), + HeightMap("height-map", { expanse, _ -> BlockHeightMapRenderer(expanse) }), + PlayerPosition("player-position", { expanse, db -> PlayerLocationShareRenderer(expanse, db) }), + GraphicalSession("graphical", { expanse, _ -> LaunchGraphicalRenderSession(expanse) }) +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/PlayerPositionExport.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/PlayerPositionExport.kt new file mode 100644 index 0000000..dfcd84d --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/PlayerPositionExport.kt @@ -0,0 +1,42 @@ +package gay.pizza.foundation.heimdall.tool.commands + +import gay.pizza.foundation.heimdall.tool.state.PlayerPositionChangelog +import gay.pizza.foundation.heimdall.tool.util.compose +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.requireObject +import com.github.ajalt.clikt.parameters.options.option +import gay.pizza.foundation.heimdall.table.PlayerPositionTable +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.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 { gay.pizza.foundation.heimdall.table.PlayerPositionTable.time greaterEq Instant.parse(startTimeString) }, + { endTimeString != null } to { gay.pizza.foundation.heimdall.table.PlayerPositionTable.time lessEq Instant.parse(endTimeString) }, + { playerIdString != null } to { gay.pizza.foundation.heimdall.table.PlayerPositionTable.player eq UUID.fromString(playerIdString) } + ) + + println("time,player,world,x,y,z,pitch,yaw") + transaction(db) { + PlayerPositionChangelog.query(db, filter).changes.forEach { change -> + change.apply { + println("${time},${player},${world},${x},${y},${z},${pitch},${yaw}") + } + } + } + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/PlayerSessionExport.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/PlayerSessionExport.kt new file mode 100644 index 0000000..c9fd923 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/PlayerSessionExport.kt @@ -0,0 +1,41 @@ +package gay.pizza.foundation.heimdall.tool.commands + +import gay.pizza.foundation.heimdall.tool.util.compose +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.requireObject +import com.github.ajalt.clikt.parameters.options.option +import gay.pizza.foundation.heimdall.table.PlayerSessionTable +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 { gay.pizza.foundation.heimdall.table.PlayerSessionTable.player eq UUID.fromString(playerIdString) }, + { playerNameString != null } to { gay.pizza.foundation.heimdall.table.PlayerSessionTable.name eq playerNameString!! } + ) + + println("id,player,name,start,end") + transaction(db) { + gay.pizza.foundation.heimdall.table.PlayerSessionTable.select(filter).orderBy(gay.pizza.foundation.heimdall.table.PlayerSessionTable.endTime).forEach { row -> + val id = row[gay.pizza.foundation.heimdall.table.PlayerSessionTable.id] + val player = row[gay.pizza.foundation.heimdall.table.PlayerSessionTable.player] + val name = row[gay.pizza.foundation.heimdall.table.PlayerSessionTable.name] + val start = row[gay.pizza.foundation.heimdall.table.PlayerSessionTable.startTime] + val end = row[gay.pizza.foundation.heimdall.table.PlayerSessionTable.endTime] + + println("${id},${player},${name},${start},${end}") + } + } + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/export/ChunkExportLoader.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/export/ChunkExportLoader.kt new file mode 100644 index 0000000..b30b630 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/export/ChunkExportLoader.kt @@ -0,0 +1,68 @@ +package gay.pizza.foundation.heimdall.tool.export + +import gay.pizza.foundation.heimdall.export.ExportedChunk +import gay.pizza.foundation.heimdall.tool.state.BlockCoordinate +import gay.pizza.foundation.heimdall.tool.state.BlockLogTracker +import gay.pizza.foundation.heimdall.tool.state.BlockState +import gay.pizza.foundation.heimdall.tool.state.SparseBlockStateMap +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import org.slf4j.LoggerFactory +import java.nio.file.Path +import java.util.zip.GZIPInputStream +import kotlin.io.path.inputStream +import kotlin.io.path.listDirectoryEntries + +class ChunkExportLoader( + val map: SparseBlockStateMap? = null, + val tracker: BlockLogTracker? = null) { + fun loadAllChunksForWorld(path: Path, world: String, fast: Boolean = false, limit: Int? = null) { + var chunkFiles = path.listDirectoryEntries("${world}_chunk_*.json.gz") + if (limit != null) { + chunkFiles = chunkFiles.take(limit) + } + if (fast) { + chunkFiles.withIndex().toList().parallelStream().forEach { loadChunkFile(it.value, id = it.index) } + } else { + for (filePath in chunkFiles) { + loadChunkFile(filePath, id = chunkFiles.indexOf(filePath)) + } + } + } + + fun loadChunkFile(path: Path, id: Int = 0) { + val fileInputStream = path.inputStream() + val gzipInputStream = GZIPInputStream(fileInputStream) + val chunk = Json.decodeFromStream(gay.pizza.foundation.heimdall.export.ExportedChunk.serializer(), gzipInputStream) + + var blockCount = 0L + val allBlocks = if (tracker != null) mutableMapOf() else null + for (section in chunk.sections) { + val x = (chunk.x * 16) + section.x + val z = (chunk.z * 16) + section.z + for ((y, block) in section.blocks.withIndex()) { + if (block.type == "minecraft:air") { + continue + } + + val coordinate = BlockCoordinate(x.toLong(), y.toLong(), z.toLong()) + val state = BlockState.cached(block.type) + map?.put(coordinate, state) + if (allBlocks != null) { + allBlocks[coordinate] = state + } + blockCount++ + } + } + + if (allBlocks != null) { + tracker?.placeAll(allBlocks) + } + + logger.info("($id) Chunk X=${chunk.x} Z=${chunk.z} had $blockCount blocks") + } + + companion object { + private val logger = LoggerFactory.getLogger(ChunkExportLoader::class.java) + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/export/CombinedChunkFormat.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/export/CombinedChunkFormat.kt new file mode 100644 index 0000000..1d5d95f --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/export/CombinedChunkFormat.kt @@ -0,0 +1,11 @@ +package gay.pizza.foundation.heimdall.tool.export + +import gay.pizza.foundation.heimdall.tool.state.BlockExpanse +import gay.pizza.foundation.heimdall.tool.state.SparseBlockStateMap +import kotlinx.serialization.Serializable + +@Serializable +class CombinedChunkFormat( + val expanse: BlockExpanse, + val map: SparseBlockStateMap +) diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/main.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/main.kt new file mode 100644 index 0000000..7e33350 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/main.kt @@ -0,0 +1,14 @@ +package gay.pizza.foundation.heimdall.tool + +import gay.pizza.foundation.heimdall.tool.commands.BlockChangeTimelapseCommand +import gay.pizza.foundation.heimdall.tool.commands.ChunkExportLoaderCommand +import gay.pizza.foundation.heimdall.tool.commands.PlayerPositionExport +import gay.pizza.foundation.heimdall.tool.commands.PlayerSessionExport +import com.github.ajalt.clikt.core.subcommands + +fun main(args: Array) = GjallarhornCommand().subcommands( + BlockChangeTimelapseCommand(), + PlayerSessionExport(), + PlayerPositionExport(), + ChunkExportLoaderCommand() +).main(args) diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockDiversityRenderer.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockDiversityRenderer.kt new file mode 100644 index 0000000..417cf8e --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockDiversityRenderer.kt @@ -0,0 +1,30 @@ +package gay.pizza.foundation.heimdall.tool.render + +import gay.pizza.foundation.heimdall.tool.state.BlockExpanse +import gay.pizza.foundation.heimdall.tool.state.BlockStateMap +import gay.pizza.foundation.heimdall.tool.state.ChangelogSlice +import gay.pizza.foundation.heimdall.tool.util.BlockColorKey +import gay.pizza.foundation.heimdall.tool.util.defaultBlockColorMap +import java.awt.Color +import java.awt.image.BufferedImage + +class BlockDiversityRenderer(val expanse: BlockExpanse, quadPixelSize: Int = defaultQuadPixelSize) : + BlockGridRenderer(quadPixelSize) { + private val blockColorKey = BlockColorKey(defaultBlockColorMap) + + override fun render(slice: ChangelogSlice, map: BlockStateMap): BufferedImage = buildPixelQuadImage(expanse) { graphics, x, z -> + val maybeYBlocks = map.getVerticalSection(x, z) + if (maybeYBlocks == null) { + setPixelQuad(graphics, x, z, Color.white) + return@buildPixelQuadImage + } + val maxBlockState = maybeYBlocks.maxByOrNull { it.key }?.value + if (maxBlockState == null) { + setPixelQuad(graphics, x, z, Color.white) + return@buildPixelQuadImage + } + + val color = blockColorKey.map(maxBlockState.type) + setPixelQuad(graphics, x, z, color) + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockGridRenderer.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockGridRenderer.kt new file mode 100644 index 0000000..656aac0 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockGridRenderer.kt @@ -0,0 +1,47 @@ +package gay.pizza.foundation.heimdall.tool.render + +import gay.pizza.foundation.heimdall.tool.state.BlockExpanse +import java.awt.Color +import java.awt.Graphics2D +import java.awt.Rectangle +import java.awt.image.BufferedImage + +abstract class BlockGridRenderer(val quadPixelSize: Int = defaultQuadPixelSize) : BlockImageRenderer { + protected fun setPixelQuad(graphics: Graphics2D, x: Long, z: Long, color: Color) { + if (globalQuadPixelNoop) { + return + } + drawSquare(graphics, x * quadPixelSize, z * quadPixelSize, quadPixelSize.toLong(), color) + } + + protected fun drawSquare(graphics: Graphics2D, x: Long, y: Long, side: Long, color: Color) { + graphics.color = color + graphics.fill(Rectangle(x.toInt(), y.toInt(), side.toInt(), side.toInt())) + } + + protected fun buildPixelQuadImage( + expanse: BlockExpanse, + callback: BufferedImage.(Graphics2D, Long, Long) -> Unit + ): BufferedImage { + val widthInBlocks = expanse.size.x + val heightInBlocks = expanse.size.z + val widthInPixels = widthInBlocks.toInt() * quadPixelSize + val heightInPixels = heightInBlocks.toInt() * quadPixelSize + val bufferedImage = + BufferedImage(widthInPixels, heightInPixels, BufferedImage.TYPE_3BYTE_BGR) + + val graphics = bufferedImage.createGraphics() + for (x in 0 until widthInBlocks) { + for (z in 0 until heightInBlocks) { + callback(bufferedImage, graphics, x, z) + } + } + graphics.dispose() + return bufferedImage + } + + companion object { + const val defaultQuadPixelSize = 4 + var globalQuadPixelNoop = false + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockHeatMapRenderer.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockHeatMapRenderer.kt new file mode 100644 index 0000000..b235a46 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockHeatMapRenderer.kt @@ -0,0 +1,26 @@ +package gay.pizza.foundation.heimdall.tool.render + +import gay.pizza.foundation.heimdall.tool.state.BlockExpanse +import gay.pizza.foundation.heimdall.tool.util.ColorGradient +import gay.pizza.foundation.heimdall.tool.util.FloatClamp +import java.awt.Color +import java.awt.image.BufferedImage + +abstract class BlockHeatMapRenderer(quadPixelSize: Int = defaultQuadPixelSize) : BlockGridRenderer(quadPixelSize) { + protected fun buildHeatMapImage( + expanse: BlockExpanse, + clamp: FloatClamp, + calculate: (Long, Long) -> Long? + ): BufferedImage = + buildPixelQuadImage(expanse) { graphics, x, z -> + val value = calculate(x, z) + val color = if (value != null) { + val floatValue = clamp.convert(value) + ColorGradient.HeatMap.getColorAtValue(floatValue) + } else { + Color.white + } + + setPixelQuad(graphics, x, z, color) + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockHeightMapRenderer.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockHeightMapRenderer.kt new file mode 100644 index 0000000..3cb3f2b --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockHeightMapRenderer.kt @@ -0,0 +1,20 @@ +package gay.pizza.foundation.heimdall.tool.render + +import gay.pizza.foundation.heimdall.tool.state.BlockExpanse +import gay.pizza.foundation.heimdall.tool.state.BlockStateMap +import gay.pizza.foundation.heimdall.tool.state.SparseBlockStateMap +import gay.pizza.foundation.heimdall.tool.state.ChangelogSlice +import gay.pizza.foundation.heimdall.tool.util.FloatClamp +import java.awt.image.BufferedImage + +class BlockHeightMapRenderer(val expanse: BlockExpanse, quadPixelSize: Int = defaultQuadPixelSize) : + BlockHeatMapRenderer(quadPixelSize) { + override fun render(slice: ChangelogSlice, map: BlockStateMap): BufferedImage { + val blockMap = map as SparseBlockStateMap + val yMin = blockMap.blocks.minOf { xSection -> xSection.value.minOf { zSection -> zSection.value.minOf { it.key } } } + val yMax = blockMap.blocks.maxOf { xSection -> xSection.value.maxOf { zSection -> zSection.value.maxOf { it.key } } } + val clamp = FloatClamp(yMin, yMax) + + return buildHeatMapImage(expanse, clamp) { x, z -> blockMap.blocks[x]?.get(z)?.maxOf { it.key } } + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockImageRenderer.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockImageRenderer.kt new file mode 100644 index 0000000..37f10f6 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockImageRenderer.kt @@ -0,0 +1,5 @@ +package gay.pizza.foundation.heimdall.tool.render + +import java.awt.image.BufferedImage + +interface BlockImageRenderer : BlockMapRenderer diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockMapRenderer.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockMapRenderer.kt new file mode 100644 index 0000000..6e03c9d --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockMapRenderer.kt @@ -0,0 +1,8 @@ +package gay.pizza.foundation.heimdall.tool.render + +import gay.pizza.foundation.heimdall.tool.state.BlockStateMap +import gay.pizza.foundation.heimdall.tool.state.ChangelogSlice + +interface BlockMapRenderer { + fun render(slice: ChangelogSlice, map: BlockStateMap): T +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockVerticalFillMapRenderer.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockVerticalFillMapRenderer.kt new file mode 100644 index 0000000..87033eb --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/BlockVerticalFillMapRenderer.kt @@ -0,0 +1,20 @@ +package gay.pizza.foundation.heimdall.tool.render + +import gay.pizza.foundation.heimdall.tool.state.BlockExpanse +import gay.pizza.foundation.heimdall.tool.state.BlockStateMap +import gay.pizza.foundation.heimdall.tool.state.SparseBlockStateMap +import gay.pizza.foundation.heimdall.tool.state.ChangelogSlice +import gay.pizza.foundation.heimdall.tool.util.FloatClamp +import java.awt.image.BufferedImage + +class BlockVerticalFillMapRenderer(val expanse: BlockExpanse, quadPixelSize: Int = defaultQuadPixelSize) : + BlockHeatMapRenderer(quadPixelSize) { + override fun render(slice: ChangelogSlice, map: BlockStateMap): BufferedImage { + val blockMap = map as SparseBlockStateMap + val yMin = blockMap.blocks.minOf { xSection -> xSection.value.minOf { zSection -> zSection.value.size } } + val yMax = blockMap.blocks.maxOf { xSection -> xSection.value.maxOf { zSection -> zSection.value.size } } + val clamp = FloatClamp(yMin.toLong(), yMax.toLong()) + + return buildHeatMapImage(expanse, clamp) { x, z -> blockMap.blocks[x]?.get(z)?.maxOf { it.key } } + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/LaunchGraphicalRenderSession.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/LaunchGraphicalRenderSession.kt new file mode 100644 index 0000000..d60c822 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/LaunchGraphicalRenderSession.kt @@ -0,0 +1,20 @@ +package gay.pizza.foundation.heimdall.tool.render + +import gay.pizza.foundation.heimdall.tool.render.ui.GraphicalRenderSession +import gay.pizza.foundation.heimdall.tool.state.BlockExpanse +import gay.pizza.foundation.heimdall.tool.state.BlockStateMap +import gay.pizza.foundation.heimdall.tool.state.ChangelogSlice +import java.awt.image.BufferedImage +import javax.swing.WindowConstants + +class LaunchGraphicalRenderSession(val expanse: BlockExpanse) : BlockImageRenderer { + override fun render(slice: ChangelogSlice, map: BlockStateMap): BufferedImage { + val session = GraphicalRenderSession(expanse, map) + session.isVisible = true + session.defaultCloseOperation = WindowConstants.HIDE_ON_CLOSE + while (session.isVisible) { + Thread.sleep(1000) + } + return BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR) + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/PlayerLocationShareRenderer.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/PlayerLocationShareRenderer.kt new file mode 100644 index 0000000..5e74639 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/PlayerLocationShareRenderer.kt @@ -0,0 +1,56 @@ +package gay.pizza.foundation.heimdall.tool.render + +import gay.pizza.foundation.heimdall.table.PlayerPositionTable +import gay.pizza.foundation.heimdall.tool.state.* +import gay.pizza.foundation.heimdall.tool.util.BlockColorKey +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import java.awt.Color +import java.awt.image.BufferedImage +import java.util.* + +class PlayerLocationShareRenderer( + val expanse: BlockExpanse, + val db: Database, + quadPixelSize: Int = defaultQuadPixelSize +) : BlockGridRenderer(quadPixelSize) { + private val colorKey = BlockColorKey(mapOf()) + + override fun render(slice: ChangelogSlice, map: BlockStateMap): BufferedImage { + val start = slice.sliceChangeRange.start + val end = slice.sliceChangeRange.endInclusive + + val playerSparseMap = BlockCoordinateSparseMap>() + val allPlayerIds = HashSet() + transaction(db) { + gay.pizza.foundation.heimdall.table.PlayerPositionTable.select { + (gay.pizza.foundation.heimdall.table.PlayerPositionTable.time greater start) and + (gay.pizza.foundation.heimdall.table.PlayerPositionTable.time lessEq end) + }.forEach { + val x = it[gay.pizza.foundation.heimdall.table.PlayerPositionTable.x].toLong() + val y = it[gay.pizza.foundation.heimdall.table.PlayerPositionTable.y].toLong() + val z = it[gay.pizza.foundation.heimdall.table.PlayerPositionTable.z].toLong() + val coordinate = expanse.offset.applyAsOffset(BlockCoordinate(x, y, z)) + val player = it[gay.pizza.foundation.heimdall.table.PlayerPositionTable.player] + playerSparseMap.createOrModify( + coordinate, + create = { mutableListOf(player) }, + modify = { players -> players.add(player) }) + allPlayerIds.add(player) + } + } + + val colorOfPlayers = allPlayerIds.associateWith { colorKey.map(it.toString()) } + + return buildPixelQuadImage(expanse) { g, x, z -> + val players = playerSparseMap.getVerticalSection(x, z)?.flatMap { it.value }?.distinct() + if (players != null) { + setPixelQuad(g, x, z, colorOfPlayers[players.first()]!!) + } else { + setPixelQuad(g, x, z, Color.white) + } + } + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/ui/GraphicalRenderSession.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/ui/GraphicalRenderSession.kt new file mode 100644 index 0000000..d65ce40 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/ui/GraphicalRenderSession.kt @@ -0,0 +1,22 @@ +package gay.pizza.foundation.heimdall.tool.render.ui + +import gay.pizza.foundation.heimdall.tool.render.BlockDiversityRenderer +import gay.pizza.foundation.heimdall.tool.render.BlockHeightMapRenderer +import gay.pizza.foundation.heimdall.tool.render.BlockVerticalFillMapRenderer +import gay.pizza.foundation.heimdall.tool.state.BlockExpanse +import gay.pizza.foundation.heimdall.tool.state.BlockStateMap +import java.awt.Dimension +import javax.swing.JFrame +import javax.swing.JTabbedPane + +class GraphicalRenderSession(val expanse: BlockExpanse, val map: BlockStateMap) : JFrame() { + init { + name = "Gjallarhorn Renderer" + size = Dimension(1024, 1024) + val pane = JTabbedPane() + pane.addTab("Block Diversity", LazyImageRenderer(map, BlockDiversityRenderer(expanse))) + pane.addTab("Height Map", LazyImageRenderer(map, BlockHeightMapRenderer(expanse))) + pane.addTab("Vertical Fill Map", LazyImageRenderer(map, BlockVerticalFillMapRenderer(expanse))) + add(pane) + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/ui/LazyImageRenderer.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/ui/LazyImageRenderer.kt new file mode 100644 index 0000000..e70d334 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/render/ui/LazyImageRenderer.kt @@ -0,0 +1,21 @@ +package gay.pizza.foundation.heimdall.tool.render.ui + +import gay.pizza.foundation.heimdall.tool.render.BlockImageRenderer +import gay.pizza.foundation.heimdall.tool.state.BlockStateMap +import gay.pizza.foundation.heimdall.tool.state.ChangelogSlice +import java.awt.Graphics +import javax.swing.JComponent + +class LazyImageRenderer(val map: BlockStateMap, private val renderer: BlockImageRenderer) : JComponent() { + private val image by lazy { + renderer.render(ChangelogSlice.none, map) + } + + override fun paint(g: Graphics?) { + g?.drawImage(image, 0, 0, this) + } + + override fun paintComponent(g: Graphics?) { + g?.drawImage(image, 0, 0, this) + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockChange.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockChange.kt new file mode 100644 index 0000000..413ce60 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockChange.kt @@ -0,0 +1,11 @@ +package gay.pizza.foundation.heimdall.tool.state + +import java.time.Instant + +data class BlockChange( + val time: Instant, + val type: BlockChangeType, + val location: BlockCoordinate, + val from: BlockState, + val to: BlockState +) diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockChangeType.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockChangeType.kt new file mode 100644 index 0000000..b362328 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockChangeType.kt @@ -0,0 +1,9 @@ +package gay.pizza.foundation.heimdall.tool.state + +import kotlinx.serialization.Serializable + +@Serializable +enum class BlockChangeType { + Place, + Break +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockChangelog.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockChangelog.kt new file mode 100644 index 0000000..b176468 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockChangelog.kt @@ -0,0 +1,94 @@ +package gay.pizza.foundation.heimdall.tool.state + +import gay.pizza.foundation.heimdall.view.BlockChangeView +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.Op +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import java.time.Duration +import java.time.Instant +import java.util.stream.Stream + +class BlockChangelog( + val changes: List +) { + fun slice(slice: ChangelogSlice): BlockChangelog = BlockChangelog(changes.filter { + slice.isTimeWithinFullRange(it.time) + }) + + fun countRelativeChangesInSlice(slice: ChangelogSlice): Int = changes.count { + slice.isTimeWithinSliceRange(it.time) + } + + val fullTimeSlice: ChangelogSlice + get() = ChangelogSlice(changes.minOf { it.time }, changes.maxOf { it.time }) + + fun calculateChangelogSlices(interval: Duration, limit: Int? = null): List { + val timeSlice = fullTimeSlice + val start = timeSlice.rootStartTime + val end = timeSlice.sliceEndTime + var intervals = mutableListOf() + var current = start + while (!current.isAfter(end)) { + intervals.add(current) + current = current.plus(interval) + } + + if (limit != null) { + intervals = intervals.takeLast(limit).toMutableList() + } + return intervals.map { ChangelogSlice(start, it, interval) } + } + + fun splitChangelogSlicesWithThreshold( + targetChangeThreshold: Int, + minimumTimeInterval: Duration, + slices: List + ): List { + return slices.parallelStream().flatMap { slice -> + val count = countRelativeChangesInSlice(slice) + if (count < targetChangeThreshold || + slice.sliceRelativeDuration < minimumTimeInterval + ) { + return@flatMap Stream.of(slice) + } + + val split = slice.split() + return@flatMap splitChangelogSlicesWithThreshold(targetChangeThreshold, minimumTimeInterval, split).parallelStream() + }.toList() + } + + companion object { + fun query(db: Database, filter: Op = Op.TRUE): BlockChangelog = transaction(db) { + BlockChangelog(gay.pizza.foundation.heimdall.view.BlockChangeView.select(filter).orderBy(gay.pizza.foundation.heimdall.view.BlockChangeView.time).map { row -> + val time = row[gay.pizza.foundation.heimdall.view.BlockChangeView.time] + val changeIsBreak = row[gay.pizza.foundation.heimdall.view.BlockChangeView.isBreak] + val x = row[gay.pizza.foundation.heimdall.view.BlockChangeView.x] + val y = row[gay.pizza.foundation.heimdall.view.BlockChangeView.y] + val z = row[gay.pizza.foundation.heimdall.view.BlockChangeView.z] + val block = row[gay.pizza.foundation.heimdall.view.BlockChangeView.block] + val location = BlockCoordinate(x.toLong(), y.toLong(), z.toLong()) + + val fromBlock = if (changeIsBreak) { + BlockState.cached(block) + } else { + BlockState.AirBlock + } + + val toBlock = if (changeIsBreak) { + BlockState.AirBlock + } else { + BlockState.cached(block) + } + + BlockChange( + time, + if (changeIsBreak) BlockChangeType.Break else BlockChangeType.Place, + location, + fromBlock, + toBlock + ) + }) + } + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockCoordinate.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockCoordinate.kt new file mode 100644 index 0000000..979c32e --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockCoordinate.kt @@ -0,0 +1,47 @@ +package gay.pizza.foundation.heimdall.tool.state + +import kotlinx.serialization.Serializable +import java.util.* + +@Serializable +data class BlockCoordinate( + val x: Long, + val y: Long, + val z: Long +) { + override fun equals(other: Any?): Boolean { + if (other !is BlockCoordinate) { + return false + } + + return other.x == x && other.y == y && other.z == z + } + + override fun hashCode(): Int = Objects.hash(x, y, z) + + fun applyAsOffset(coordinate: BlockCoordinate) = coordinate.copy( + x = coordinate.x + x, + y = coordinate.y + y, + z = coordinate.z + z + ) + + companion object { + val zero = BlockCoordinate(0, 0, 0) + + fun maxOf(coordinates: List): BlockCoordinate { + val x = coordinates.maxOf { it.x } + val y = coordinates.maxOf { it.y } + val z = coordinates.maxOf { it.z } + + return BlockCoordinate(x, y, z) + } + + fun minOf(coordinates: List): BlockCoordinate { + val x = coordinates.minOf { it.x } + val y = coordinates.minOf { it.y } + val z = coordinates.minOf { it.z } + + return BlockCoordinate(x, y, z) + } + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockCoordinateSparseMap.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockCoordinateSparseMap.kt new file mode 100644 index 0000000..9c78ce6 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockCoordinateSparseMap.kt @@ -0,0 +1,65 @@ +package gay.pizza.foundation.heimdall.tool.state + +import gay.pizza.foundation.heimdall.tool.util.maxOfAll +import gay.pizza.foundation.heimdall.tool.util.minOfAll +import kotlin.math.absoluteValue + +open class BlockCoordinateSparseMap(blocks: Map>> = mutableMapOf()) : BlockCoordinateStore { + private var internalBlocks = blocks + + val blocks: Map>> + get() = internalBlocks + + override fun get(position: BlockCoordinate): T? = internalBlocks[position.x]?.get(position.z)?.get(position.z) + override fun getVerticalSection(x: Long, z: Long): Map? = internalBlocks[x]?.get(z) + override fun getXSection(x: Long): Map>? = internalBlocks[x] + + override fun put(position: BlockCoordinate, value: T) { + (((internalBlocks as MutableMap).getOrPut(position.x) { + mutableMapOf() + } as MutableMap).getOrPut(position.z) { + mutableMapOf() + } as MutableMap)[position.y] = value + } + + override fun createOrModify(position: BlockCoordinate, create: () -> T, modify: (T) -> Unit) { + val existing = get(position) + if (existing == null) { + put(position, create()) + } else { + modify(existing) + } + } + + fun coordinateSequence(): Sequence = internalBlocks.asSequence().flatMap { x -> + x.value.asSequence().flatMap { z -> + z.value.asSequence().map { y -> BlockCoordinate(x.key, z.key, y.key) } + } + } + + fun calculateZeroBlockOffset(): BlockCoordinate { + val (x, y, z) = coordinateSequence().minOfAll(3) { listOf(it.x, it.y, it.z) } + val xOffset = if (x < 0) x.absoluteValue else 0 + val yOffset = if (y < 0) y.absoluteValue else 0 + val zOffset = if (z < 0) z.absoluteValue else 0 + return BlockCoordinate(xOffset, yOffset, zOffset) + } + + fun calculateMaxBlock(): BlockCoordinate { + val (x, y, z) = coordinateSequence().maxOfAll(3) { listOf(it.x, it.y, it.z) } + return BlockCoordinate(x, y, z) + } + + fun applyCoordinateOffset(offset: BlockCoordinate) { + val root = mutableMapOf>>() + internalBlocks = internalBlocks.map { xSection -> + val zSectionMap = mutableMapOf>() + (xSection.key + offset.x) to xSection.value.map { zSection -> + val ySectionMap = mutableMapOf() + (zSection.key + offset.z) to zSection.value.mapKeys { + (it.key + offset.y) + }.toMap(ySectionMap) + }.toMap(zSectionMap) + }.toMap(root) + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockCoordinateStore.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockCoordinateStore.kt new file mode 100644 index 0000000..0b52cd5 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockCoordinateStore.kt @@ -0,0 +1,9 @@ +package gay.pizza.foundation.heimdall.tool.state + +interface BlockCoordinateStore { + fun get(position: BlockCoordinate): T? + fun getVerticalSection(x: Long, z: Long): Map? + fun getXSection(x: Long): Map>? + fun put(position: BlockCoordinate, value: T) + fun createOrModify(position: BlockCoordinate, create: () -> T, modify: (T) -> Unit) +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockExpanse.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockExpanse.kt new file mode 100644 index 0000000..ea8b4cc --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockExpanse.kt @@ -0,0 +1,16 @@ +package gay.pizza.foundation.heimdall.tool.state + +import kotlinx.serialization.Serializable + +@Serializable +data class BlockExpanse( + val offset: BlockCoordinate, + val size: BlockCoordinate +) { + companion object { + fun zeroOffsetAndMax(offset: BlockCoordinate, max: BlockCoordinate) = BlockExpanse( + offset, + offset.applyAsOffset(max) + ) + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockLogTracker.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockLogTracker.kt new file mode 100644 index 0000000..4b1b2cf --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockLogTracker.kt @@ -0,0 +1,62 @@ +package gay.pizza.foundation.heimdall.tool.state + +import gay.pizza.foundation.heimdall.tool.util.maxOfAll +import gay.pizza.foundation.heimdall.tool.util.minOfAll +import java.util.concurrent.ConcurrentHashMap +import kotlin.math.absoluteValue + +class BlockLogTracker(private val mode: BlockTrackMode = BlockTrackMode.RemoveOnDelete, isConcurrent: Boolean = false) { + internal val blocks: MutableMap = if (isConcurrent) ConcurrentHashMap() else mutableMapOf() + + fun place(position: BlockCoordinate, state: BlockState) { + blocks[position] = state + } + + fun placeAll(map: Map) { + blocks.putAll(map) + } + + fun delete(position: BlockCoordinate) { + if (mode == BlockTrackMode.AirOnDelete) { + blocks[position] = BlockState.AirBlock + } else { + blocks.remove(position) + } + } + + fun calculateZeroBlockOffset(): BlockCoordinate { + val (x, y, z) = blocks.keys.minOfAll(3) { listOf(it.x, it.y, it.z) } + val xOffset = if (x < 0) x.absoluteValue else 0 + val yOffset = if (y < 0) y.absoluteValue else 0 + val zOffset = if (z < 0) z.absoluteValue else 0 + + return BlockCoordinate(xOffset, yOffset, zOffset) + } + + fun calculateMaxBlock(): BlockCoordinate { + val (x, y, z) = blocks.keys.maxOfAll(3) { listOf(it.x, it.y, it.z) } + return BlockCoordinate(x, y, z) + } + + fun isEmpty() = blocks.isEmpty() + fun isNotEmpty() = !isEmpty() + + fun buildBlockMap(offset: BlockCoordinate = BlockCoordinate.zero): SparseBlockStateMap { + val map = SparseBlockStateMap() + blocks.forEach { (position, state) -> + val realPosition = offset.applyAsOffset(position) + map.put(realPosition, state) + } + return map + } + + fun replay(changelog: BlockChangelog) = changelog.changes.forEach { change -> + if (change.type == BlockChangeType.Break) { + delete(change.location) + } else { + place(change.location, change.to) + } + } + + fun get(position: BlockCoordinate): BlockState? = blocks[position] +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockMapRenderPool.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockMapRenderPool.kt new file mode 100644 index 0000000..2e7cf6e --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockMapRenderPool.kt @@ -0,0 +1,81 @@ +package gay.pizza.foundation.heimdall.tool.state + +import gay.pizza.foundation.heimdall.tool.render.BlockMapRenderer +import org.slf4j.LoggerFactory +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Future +import java.util.concurrent.ThreadPoolExecutor + +class BlockMapRenderPool( + val changelog: BlockChangelog, + val blockTrackMode: BlockTrackMode, + val createRendererFunction: (BlockExpanse) -> BlockMapRenderer, + val delegate: BlockMapRenderPoolDelegate, + val threadPoolExecutor: ThreadPoolExecutor, + val renderResultCallback: (ChangelogSlice, T) -> Unit +) { + private val trackers = ConcurrentHashMap() + private val playbackJobFutures = ConcurrentHashMap>() + private val renderJobFutures = ConcurrentHashMap>() + + fun submitPlaybackJob(id: String, slice: ChangelogSlice) { + val future = threadPoolExecutor.submit { + try { + runPlaybackSlice(id, slice) + } catch (e: Exception) { + logger.error("Failed to run playback job for slice $id", e) + } + } + playbackJobFutures[slice] = future + } + + fun submitRenderJob(slice: ChangelogSlice, callback: () -> T) { + val future = threadPoolExecutor.submit { + try { + val result = callback() + renderResultCallback(slice, result) + } catch (e: Exception) { + logger.error("Failed to run render job for slice $slice", e) + } + } + renderJobFutures[slice] = future + } + + fun render(slices: List) { + for (slice in slices) { + submitPlaybackJob((slices.indexOf(slice) + 1).toString(), slice) + } + + for (future in playbackJobFutures.values) { + future.get() + } + + delegate.onAllPlaybackComplete(this, trackers) + + for (future in renderJobFutures.values) { + try { + future.get() + } catch (e: Exception) { + logger.error("Failed to render slice.", e) + } + } + } + + private fun runPlaybackSlice(id: String, slice: ChangelogSlice) { + val start = System.currentTimeMillis() + val sliced = changelog.slice(slice) + val tracker = BlockLogTracker(blockTrackMode) + tracker.replay(sliced) + if (tracker.isNotEmpty()) { + trackers[slice] = tracker + delegate.onSinglePlaybackComplete(this, slice, tracker) + } + val end = System.currentTimeMillis() + val timeInMilliseconds = end - start + logger.debug("Playback Completed for Slice $id in ${timeInMilliseconds}ms") + } + + companion object { + private val logger = LoggerFactory.getLogger(BlockMapRenderPool::class.java) + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockMapRenderPoolDelegate.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockMapRenderPoolDelegate.kt new file mode 100644 index 0000000..61d886c --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockMapRenderPoolDelegate.kt @@ -0,0 +1,6 @@ +package gay.pizza.foundation.heimdall.tool.state + +interface BlockMapRenderPoolDelegate { + fun onSinglePlaybackComplete(pool: BlockMapRenderPool, slice: ChangelogSlice, tracker: BlockLogTracker) + fun onAllPlaybackComplete(pool: BlockMapRenderPool, trackers: Map) +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockMapTimelapse.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockMapTimelapse.kt new file mode 100644 index 0000000..eac048b --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockMapTimelapse.kt @@ -0,0 +1,30 @@ +package gay.pizza.foundation.heimdall.tool.state + +class BlockMapTimelapse : + BlockMapRenderPoolDelegate { + override fun onSinglePlaybackComplete(pool: BlockMapRenderPool, slice: ChangelogSlice, tracker: BlockLogTracker) { + } + + override fun onAllPlaybackComplete( + pool: BlockMapRenderPool, + trackers: Map + ) { + if (trackers.isEmpty()) { + return + } + + val allBlockOffsets = trackers.map { it.value.calculateZeroBlockOffset() } + val globalBlockOffset = BlockCoordinate.maxOf(allBlockOffsets) + val allBlockMaxes = trackers.map { it.value.calculateMaxBlock() } + val globalBlockMax = BlockCoordinate.maxOf(allBlockMaxes) + val globalBlockExpanse = BlockExpanse.zeroOffsetAndMax(globalBlockOffset, globalBlockMax) + + val renderer = pool.createRendererFunction(globalBlockExpanse) + for ((slice, tracker) in trackers) { + pool.submitRenderJob(slice) { + val map = tracker.buildBlockMap(globalBlockExpanse.offset) + renderer.render(slice, map) + } + } + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockState.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockState.kt new file mode 100644 index 0000000..2e70146 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockState.kt @@ -0,0 +1,15 @@ +package gay.pizza.foundation.heimdall.tool.state + +import java.util.concurrent.ConcurrentHashMap +import kotlinx.serialization.Serializable + +@Serializable(BlockStateSerializer::class) +data class BlockState(val type: String) { + companion object { + private val cache = ConcurrentHashMap() + + val AirBlock: BlockState = cached("minecraft:air") + + fun cached(type: String): BlockState = cache.computeIfAbsent(type) { BlockState(type) } + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockStateMap.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockStateMap.kt new file mode 100644 index 0000000..4b34419 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockStateMap.kt @@ -0,0 +1,3 @@ +package gay.pizza.foundation.heimdall.tool.state + +typealias BlockStateMap = BlockCoordinateStore diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockStateSerializer.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockStateSerializer.kt new file mode 100644 index 0000000..255174e --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockStateSerializer.kt @@ -0,0 +1,20 @@ +package gay.pizza.foundation.heimdall.tool.state + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +class BlockStateSerializer : KSerializer { + override val descriptor: SerialDescriptor + get() = String.serializer().descriptor + + override fun deserialize(decoder: Decoder): BlockState { + return BlockState.cached(decoder.decodeString()) + } + + override fun serialize(encoder: Encoder, value: BlockState) { + encoder.encodeString(value.type) + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockTrackMode.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockTrackMode.kt new file mode 100644 index 0000000..e29e769 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockTrackMode.kt @@ -0,0 +1,6 @@ +package gay.pizza.foundation.heimdall.tool.state + +enum class BlockTrackMode { + RemoveOnDelete, + AirOnDelete +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/ChangelogSlice.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/ChangelogSlice.kt new file mode 100644 index 0000000..23b6b55 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/ChangelogSlice.kt @@ -0,0 +1,29 @@ +package gay.pizza.foundation.heimdall.tool.state + +import java.time.Duration +import java.time.Instant + +data class ChangelogSlice(val rootStartTime: Instant, val sliceEndTime: Instant, val sliceRelativeDuration: Duration) { + constructor(from: Instant, to: Instant) : this(from, to, Duration.ofMillis(to.toEpochMilli() - from.toEpochMilli())) + + val sliceStartTime: Instant = sliceEndTime.minus(sliceRelativeDuration) + val fullTimeRange: ClosedRange = rootStartTime..sliceEndTime + val sliceChangeRange: ClosedRange = sliceStartTime..sliceEndTime + + fun isTimeWithinFullRange(time: Instant) = time in fullTimeRange + fun isTimeWithinSliceRange(time: Instant) = time in sliceChangeRange + + fun split(): List { + val half = sliceRelativeDuration.dividedBy(2) + val initial = sliceEndTime.minus(sliceRelativeDuration) + val first = initial.plus(half) + return listOf( + ChangelogSlice(rootStartTime, first, half), + ChangelogSlice(rootStartTime, sliceEndTime, half) + ) + } + + companion object { + val none = ChangelogSlice(Instant.MIN, Instant.MIN, Duration.ZERO) + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/PlayerPositionChange.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/PlayerPositionChange.kt new file mode 100644 index 0000000..26150e1 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/PlayerPositionChange.kt @@ -0,0 +1,15 @@ +package gay.pizza.foundation.heimdall.tool.state + +import java.time.Instant +import java.util.* + +data class PlayerPositionChange( + val time: Instant, + val player: UUID, + val world: UUID, + val x: Double, + val y: Double, + val z: Double, + val pitch: Double, + val yaw: Double +) diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/PlayerPositionChangelog.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/PlayerPositionChangelog.kt new file mode 100644 index 0000000..c7d7204 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/PlayerPositionChangelog.kt @@ -0,0 +1,28 @@ +package gay.pizza.foundation.heimdall.tool.state + +import gay.pizza.foundation.heimdall.table.PlayerPositionTable +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.Op +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction + +class PlayerPositionChangelog( + val changes: List +) { + companion object { + fun query(db: Database, filter: Op = Op.TRUE): PlayerPositionChangelog = transaction(db) { + PlayerPositionChangelog(gay.pizza.foundation.heimdall.table.PlayerPositionTable.select(filter).orderBy(gay.pizza.foundation.heimdall.table.PlayerPositionTable.time).map { row -> + val time = row[gay.pizza.foundation.heimdall.table.PlayerPositionTable.time] + val player = row[gay.pizza.foundation.heimdall.table.PlayerPositionTable.player] + val world = row[gay.pizza.foundation.heimdall.table.PlayerPositionTable.world] + val x = row[gay.pizza.foundation.heimdall.table.PlayerPositionTable.x] + val y = row[gay.pizza.foundation.heimdall.table.PlayerPositionTable.y] + val z = row[gay.pizza.foundation.heimdall.table.PlayerPositionTable.z] + val pitch = row[gay.pizza.foundation.heimdall.table.PlayerPositionTable.z] + val yaw = row[gay.pizza.foundation.heimdall.table.PlayerPositionTable.z] + + PlayerPositionChange(time, player, world, x, y, z, pitch, yaw) + }) + } + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/SparseBlockStateMap.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/SparseBlockStateMap.kt new file mode 100644 index 0000000..2cfd6b0 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/SparseBlockStateMap.kt @@ -0,0 +1,7 @@ +package gay.pizza.foundation.heimdall.tool.state + +import kotlinx.serialization.Serializable + +@Serializable(SparseBlockStateMapSerializer::class) +class SparseBlockStateMap(blocks: Map>> = mutableMapOf()) : + BlockCoordinateSparseMap(blocks) diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/SparseBlockStateMapSerializer.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/SparseBlockStateMapSerializer.kt new file mode 100644 index 0000000..fd067d5 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/SparseBlockStateMapSerializer.kt @@ -0,0 +1,23 @@ +package gay.pizza.foundation.heimdall.tool.state + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +class SparseBlockStateMapSerializer : KSerializer { + private val internal = MapSerializer(Long.serializer(), MapSerializer(Long.serializer(), MapSerializer(Long.serializer(), BlockState.serializer()))) + override val descriptor: SerialDescriptor + get() = internal.descriptor + + override fun deserialize(decoder: Decoder): SparseBlockStateMap { + val data = internal.deserialize(decoder) + return SparseBlockStateMap(data) + } + + override fun serialize(encoder: Encoder, value: SparseBlockStateMap) { + internal.serialize(encoder, value.blocks) + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/BlockColorKey.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/BlockColorKey.kt new file mode 100644 index 0000000..db75fd2 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/BlockColorKey.kt @@ -0,0 +1,20 @@ +package gay.pizza.foundation.heimdall.tool.util + +import java.awt.Color +import java.util.concurrent.ConcurrentHashMap + +class BlockColorKey(assigned: Map) { + private val colors = ConcurrentHashMap(assigned) + + fun map(key: String): Color = colors.computeIfAbsent(key) { findUniqueColor() } + + private fun findUniqueColor(): Color { + var random = randomColor() + while (colors.values.any { it.rgb == random.rgb }) { + random = randomColor() + } + return random + } + + private fun randomColor() = Color((Math.random() * 0x1000000).toInt()) +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/BlockColorKeys.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/BlockColorKeys.kt new file mode 100644 index 0000000..f48180f --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/BlockColorKeys.kt @@ -0,0 +1,18 @@ +package gay.pizza.foundation.heimdall.tool.util + +import java.awt.Color + +val defaultBlockColorMap = mapOf( + "minecraft:air" to Color.black, + "minecraft:dirt" to Color.decode("#9b7653"), + "minecraft:farmland" to Color.decode("#5d3f2a"), + "minecraft:stone" to Color.decode("#787366"), + "minecraft:cobblestone" to Color.decode("#c4bca7"), + "minecraft:wheat" to Color.decode("#9e884c"), + "minecraft:carrots" to Color.decode("#f89d40"), + "minecraft:stone_brick_stairs" to Color.decode("#b8a18c"), + "minecraft:dirt_path" to Color.decode("#8f743d"), + "minecraft:deepslate_tiles" to Color.decode("#49494b"), + "minecraft:spruce_planks" to Color.decode("#60492d"), + "minecraft:water" to Color.decode("#1f54ff") +) diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/ColorGradient.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/ColorGradient.kt new file mode 100644 index 0000000..7935c48 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/ColorGradient.kt @@ -0,0 +1,57 @@ +package gay.pizza.foundation.heimdall.tool.util + +import java.awt.Color +import kotlin.math.max + +class ColorGradient constructor() { + constructor(vararg points: ColorGradientPoint) : this() { + for (point in points) { + addColorPoint(point) + } + } + + private val points = mutableListOf() + + fun addColorPoint(point: ColorGradientPoint) { + for (x in 0 until points.size) { + if (point.value < points[x].value) { + points.add(x, point) + return + } + } + points.add(point) + } + + fun getColorAtValue(value: Float): Color { + if (points.isEmpty()) { + return ColorGradientPoint(0f, 0f, 0f, value).toColor() + } + + for (x in 0 until points.size) { + val current = points[x] + if (value < current.value) { + val previous = points[max(0, x - 1)] + val diff = previous.value - current.value + val fractionBetween = if (diff == 0f) 0f else (value - current.value) / diff + return ColorGradientPoint( + (previous.r - current.r) * fractionBetween + current.r, + (previous.g - current.g) * fractionBetween + current.g, + (previous.b - current.b) * fractionBetween + current.b, + value + ).toColor() + } + } + + return points.last().copy(value = value).toColor() + } + + companion object { + val HeatMap = ColorGradient( + ColorGradientPoint(0f, 0f, 1f, 0.0f), + ColorGradientPoint(0f, 1f, 1f, 0.25f), + ColorGradientPoint(0f, 1f, 0f, 0.5f), + ColorGradientPoint(1f, 1f, 0f, 0.75f), + ColorGradientPoint(1f, 0f, 0f, 1.0f) + ) + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/ColorGradientPoint.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/ColorGradientPoint.kt new file mode 100644 index 0000000..1a78222 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/ColorGradientPoint.kt @@ -0,0 +1,16 @@ +package gay.pizza.foundation.heimdall.tool.util + +import java.awt.Color + +data class ColorGradientPoint( + val r: Float, + val g: Float, + val b: Float, + val value: Float +) { + fun toColor() = Color( + FloatClamp.ColorRgbComponent.convert(r).toInt(), + FloatClamp.ColorRgbComponent.convert(g).toInt(), + FloatClamp.ColorRgbComponent.convert(b).toInt() + ) +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/FloatClamp.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/FloatClamp.kt new file mode 100644 index 0000000..3fec03b --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/FloatClamp.kt @@ -0,0 +1,12 @@ +package gay.pizza.foundation.heimdall.tool.util + +import kotlin.math.roundToLong + +class FloatClamp(val min: Long, val max: Long) { + fun convert(value: Float): Long = (value * max.toFloat()).roundToLong() + min + fun convert(value: Long): Float = (value - min.toFloat()) / max + + companion object { + val ColorRgbComponent = FloatClamp(0, 255) + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/ImageTools.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/ImageTools.kt new file mode 100644 index 0000000..7d3fc09 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/ImageTools.kt @@ -0,0 +1,17 @@ +package gay.pizza.foundation.heimdall.tool.util + +import java.awt.image.BufferedImage +import java.io.File +import javax.imageio.ImageIO + +fun BufferedImage.savePngFile(path: String) { + if (!ImageIO.write(this, "png", File(path))) { + throw RuntimeException("Unable to write PNG.") + } +} + +fun BufferedImage.saveJpegFile(path: String) { + if (!ImageIO.write(this, "jpeg", File(path))) { + throw RuntimeException("Unable to write JPEG.") + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/compose.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/compose.kt new file mode 100644 index 0000000..e3a9d78 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/compose.kt @@ -0,0 +1,12 @@ +package gay.pizza.foundation.heimdall.tool.util + +import org.jetbrains.exposed.sql.Op + +fun compose( + combine: (Op, Op) -> Op, + vararg filters: Pair<() -> Boolean, () -> Op> +): Op = filters.toMap().entries + .asSequence() + .filter { it.key() } + .map { it.value() } + .fold(Op.TRUE as Op, combine) diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/numerics.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/numerics.kt new file mode 100644 index 0000000..fb5cba1 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/util/numerics.kt @@ -0,0 +1,39 @@ +package gay.pizza.foundation.heimdall.tool.util + +fun Iterable.minOfAll(fieldCount: Int, block: (value: T) -> List): List { + val fieldRange = 0 until fieldCount + val results = fieldRange.map { Long.MAX_VALUE }.toMutableList() + for (item in this) { + val numerics = block(item) + for (field in fieldRange) { + val current = results[field] + val number = numerics[field] + if (number < current) { + results[field] = number + } + } + } + return results +} + +fun Iterable.maxOfAll(fieldCount: Int, block: (value: T) -> List): List { + val fieldRange = 0 until fieldCount + val results = fieldRange.map { Long.MIN_VALUE }.toMutableList() + for (item in this) { + val numerics = block(item) + for (field in fieldRange) { + val current = results[field] + val number = numerics[field] + if (number > current) { + results[field] = number + } + } + } + return results +} + +fun Sequence.minOfAll(fieldCount: Int, block: (value: T) -> List): List = + asIterable().minOfAll(fieldCount, block) + +fun Sequence.maxOfAll(fieldCount: Int, block: (value: T) -> List): List = + asIterable().maxOfAll(fieldCount, block) diff --git a/tools/organize-artifacts.sh b/tools/organize-artifacts.sh index aade0a0..3d00e3a 100755 --- a/tools/organize-artifacts.sh +++ b/tools/organize-artifacts.sh @@ -7,7 +7,7 @@ mkdir -p artifacts/ mkdir -p artifacts/build/manifests cp build/manifests/update.json artifacts/build/manifests/ -find . -name "*-plugin.jar" | while read -r jar +find . -name "*-plugin.jar" | grep "foundation-" | while read -r jar do DN=`dirname ${jar}` mkdir -p "artifacts/$DN"