diff --git a/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/BlockOffset.kt b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/BlockOffset.kt new file mode 100644 index 0000000..79c41e3 --- /dev/null +++ b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/BlockOffset.kt @@ -0,0 +1,17 @@ +package cloud.kubelet.foundation.gjallarhorn + +data class BlockOffset( + val x: Long, + val y: Long, + val z: Long +) { + fun apply(position: BlockPosition) = position.copy( + x = position.x + x, + y = position.y + y, + z = position.z + z + ) + + companion object { + val none = BlockOffset(0, 0, 0) + } +} diff --git a/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/BlockPosition.kt b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/BlockPosition.kt new file mode 100644 index 0000000..105fc9c --- /dev/null +++ b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/BlockPosition.kt @@ -0,0 +1,19 @@ +package cloud.kubelet.foundation.gjallarhorn + +import java.util.* + +data class BlockPosition( + val x: Long, + val y: Long, + val z: Long +) { + override fun equals(other: Any?): Boolean { + if (other !is BlockPosition) { + return false + } + + return other.x == x && other.y == y && other.z == z + } + + override fun hashCode(): Int = Objects.hash(x, y, z) +} \ No newline at end of file diff --git a/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/BlockState.kt b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/BlockState.kt new file mode 100644 index 0000000..405852c --- /dev/null +++ b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/BlockState.kt @@ -0,0 +1,6 @@ +package cloud.kubelet.foundation.gjallarhorn + +import kotlinx.serialization.Serializable + +@Serializable +data class BlockState(val type: String) diff --git a/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/BlockStateImage.kt b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/BlockStateImage.kt new file mode 100644 index 0000000..c71a1cf --- /dev/null +++ b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/BlockStateImage.kt @@ -0,0 +1,51 @@ +package cloud.kubelet.foundation.gjallarhorn + +import cloud.kubelet.foundation.gjallarhorn.util.RandomColorKey +import java.awt.Color +import java.awt.image.BufferedImage +import java.util.* + +class BlockStateImage { + val blocks = TreeMap>>() + + fun put(position: BlockPosition, state: BlockState) { + blocks.getOrPut(position.x) { + TreeMap() + }.getOrPut(position.z) { + TreeMap() + }[position.y] = state + } + + fun buildBufferedImage(): BufferedImage { + val colorKey = RandomColorKey() + val xMax = blocks.keys.maxOf { it } + val zMax = blocks.maxOf { it.value.maxOf { it.key } } + + val bufferedImage = BufferedImage(xMax.toInt() * 2, zMax.toInt() * 2, BufferedImage.TYPE_4BYTE_ABGR) + for (x in 0 until xMax) { + for (z in 0 until zMax) { + fun set(rgb: Int) { + bufferedImage.setRGB(x.toInt() * 2, z.toInt() * 2, rgb) + bufferedImage.setRGB((x.toInt() * 2) + 1, z.toInt() * 2, rgb) + bufferedImage.setRGB(x.toInt() * 2, (z.toInt() * 2) + 1, rgb) + bufferedImage.setRGB((x.toInt() * 2) + 1, (z.toInt() * 2) + 1, rgb) + } + + val maybeYBlocks = blocks[x]?.get(z) + if (maybeYBlocks == null) { + set(Color.white.rgb) + continue + } + val maxBlockState = maybeYBlocks.maxByOrNull { it.key }?.value + if (maxBlockState == null) { + set(Color.white.rgb) + continue + } + + val color = colorKey.map(maxBlockState.type) + set(color.rgb) + } + } + return bufferedImage + } +} diff --git a/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/BlockStateTracker.kt b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/BlockStateTracker.kt index 9b36748..61b1094 100644 --- a/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/BlockStateTracker.kt +++ b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/BlockStateTracker.kt @@ -1,7 +1,7 @@ package cloud.kubelet.foundation.gjallarhorn -import java.util.* import kotlin.collections.HashMap +import kotlin.math.absoluteValue class BlockStateTracker { val blocks = HashMap() @@ -14,21 +14,22 @@ class BlockStateTracker { blocks.remove(position) } - data class BlockState(val type: String) + fun calculateZeroBlockOffset(): BlockOffset { + val x = blocks.keys.minOf { it.x } + val y = blocks.keys.minOf { it.y } + val z = blocks.keys.minOf { it.z } - data class BlockPosition( - val x: Long, - val y: Long, - val z: Long - ) { - override fun equals(other: Any?): Boolean { - if (other !is BlockPosition) { - return false - } + 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 other.x == x && other.y == y && other.z == z + return BlockOffset(xOffset, yOffset, zOffset) + } + + fun populate(image: BlockStateImage, offset: BlockOffset = BlockOffset.none) { + blocks.forEach { (position, state) -> + val realPosition = offset.apply(position) + image.put(realPosition, state) } - - override fun hashCode(): Int = Objects.hash(x, y, z) } } diff --git a/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/BlockLogReplay.kt b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/BlockLogReplay.kt index 866e51a..c5f90d2 100644 --- a/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/BlockLogReplay.kt +++ b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/BlockLogReplay.kt @@ -1,21 +1,24 @@ package cloud.kubelet.foundation.gjallarhorn.commands -import cloud.kubelet.foundation.gjallarhorn.BlockStateTracker -import cloud.kubelet.foundation.gjallarhorn.compose +import cloud.kubelet.foundation.gjallarhorn.* import cloud.kubelet.foundation.heimdall.view.BlockChangeView import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.requireObject +import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.option import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction +import java.io.File import java.time.Instant +import javax.imageio.ImageIO class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-log") { private val db by requireObject() private val timeAsString by option("--time", help = "Replay Time") + private val render by option("--render", help = "Enable Render Mode").flag() override fun run() { val filter = compose( @@ -32,18 +35,25 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo val z = row[BlockChangeView.z] val block = row[BlockChangeView.block] - val location = BlockStateTracker.BlockPosition(x.toLong(), y.toLong(), z.toLong()) + val location = BlockPosition(x.toLong(), y.toLong(), z.toLong()) if (changeIsBreak) { tracker.delete(location) } else { - tracker.place(location, BlockStateTracker.BlockState(block)) + tracker.place(location, BlockState(block)) } } } - println("x,y,z,block") - for ((position, block) in tracker.blocks) { - println("${position.x},${position.y},${position.z},${block.type}") + if (render) { + val image = BlockStateImage() + tracker.populate(image, offset = tracker.calculateZeroBlockOffset()) + val bufferedImage = image.buildBufferedImage() + ImageIO.write(bufferedImage, "png", File("top-down.png")) + } else { + println("x,y,z,block") + for ((position, block) in tracker.blocks) { + println("${position.x},${position.y},${position.z},${block.type}") + } } } } \ No newline at end of file diff --git a/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/util/RandomColorKey.kt b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/util/RandomColorKey.kt new file mode 100644 index 0000000..f1b086a --- /dev/null +++ b/foundation-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/util/RandomColorKey.kt @@ -0,0 +1,19 @@ +package cloud.kubelet.foundation.gjallarhorn.util + +import java.awt.Color + +class RandomColorKey { + private val colors = mutableMapOf() + + fun map(key: String) = colors.getOrPut(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()) +} \ No newline at end of file diff --git a/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/FoundationHeimdallPlugin.kt b/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/FoundationHeimdallPlugin.kt index b2c9411..b222df9 100644 --- a/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/FoundationHeimdallPlugin.kt +++ b/foundation-heimdall/src/main/kotlin/cloud/kubelet/foundation/heimdall/FoundationHeimdallPlugin.kt @@ -137,7 +137,14 @@ class FoundationHeimdallPlugin : JavaPlugin(), Listener { @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())) + buffer.push( + EntityKill( + killer.uniqueId, + killer.location, + event.entity.uniqueId, + event.entityType.key.toString() + ) + ) } override fun onDisable() { @@ -145,7 +152,12 @@ class FoundationHeimdallPlugin : JavaPlugin(), Listener { 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)) + buffer.push(PlayerSession( + playerId, + server.getPlayer(playerId)?.name ?: "__unknown__", + startTime, + endTime + )) } bufferFlushThread.flush() } diff --git a/foundation-heimdall/src/main/resources/init.sql b/foundation-heimdall/src/main/resources/init.sql index f008b65..f4addda 100644 --- a/foundation-heimdall/src/main/resources/init.sql +++ b/foundation-heimdall/src/main/resources/init.sql @@ -1,4 +1,3 @@ --- create extension if not exists "uuid-ossp"; -- create extension if not exists timescaledb; @@ -119,4 +118,22 @@ create table if not exists heimdall.entity_kills ( -- select create_hypertable('heimdall.entity_kills', 'time', 'player', 4, if_not_exists => TRUE); -- -create or replace view heimdall.block_changes as select true as break, * from heimdall.block_breaks union all select false as break, * from heimdall.block_places; +create or replace view heimdall.block_changes as + select true as break, * + from heimdall.block_breaks + union all + select false as break, * from heimdall.block_places; +-- +create or replace view heimdall.player_names as + with unique_player_ids as ( + select distinct player + from heimdall.player_sessions + ) + select player, ( + select name + from heimdall.player_sessions + where player = unique_player_ids.player + order by "end" desc + limit 1 + ) as name + from unique_player_ids;