Heimdall: Player Names Table, Gjallarhorn: Block State Image Rendering

This commit is contained in:
Kenneth Endfinger 2021-12-27 23:56:09 -05:00
parent 9386dc7c56
commit e681df1e65
No known key found for this signature in database
GPG Key ID: C4E68E5647420E10
9 changed files with 177 additions and 25 deletions

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -0,0 +1,6 @@
package cloud.kubelet.foundation.gjallarhorn
import kotlinx.serialization.Serializable
@Serializable
data class BlockState(val type: String)

View File

@ -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<Long, TreeMap<Long, TreeMap<Long, BlockState>>>()
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
}
}

View File

@ -1,7 +1,7 @@
package cloud.kubelet.foundation.gjallarhorn package cloud.kubelet.foundation.gjallarhorn
import java.util.*
import kotlin.collections.HashMap import kotlin.collections.HashMap
import kotlin.math.absoluteValue
class BlockStateTracker { class BlockStateTracker {
val blocks = HashMap<BlockPosition, BlockState>() val blocks = HashMap<BlockPosition, BlockState>()
@ -14,21 +14,22 @@ class BlockStateTracker {
blocks.remove(position) 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 xOffset = if (x < 0) x.absoluteValue else 0
val x: Long, val yOffset = if (y < 0) y.absoluteValue else 0
val y: Long, val zOffset = if (z < 0) z.absoluteValue else 0
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 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)
} }
} }

View File

@ -1,21 +1,24 @@
package cloud.kubelet.foundation.gjallarhorn.commands package cloud.kubelet.foundation.gjallarhorn.commands
import cloud.kubelet.foundation.gjallarhorn.BlockStateTracker import cloud.kubelet.foundation.gjallarhorn.*
import cloud.kubelet.foundation.gjallarhorn.compose
import cloud.kubelet.foundation.heimdall.view.BlockChangeView import cloud.kubelet.foundation.heimdall.view.BlockChangeView
import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.requireObject import com.github.ajalt.clikt.core.requireObject
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.option
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import java.io.File
import java.time.Instant import java.time.Instant
import javax.imageio.ImageIO
class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-log") { class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-log") {
private val db by requireObject<Database>() private val db by requireObject<Database>()
private val timeAsString by option("--time", help = "Replay Time") private val timeAsString by option("--time", help = "Replay Time")
private val render by option("--render", help = "Enable Render Mode").flag()
override fun run() { override fun run() {
val filter = compose( val filter = compose(
@ -32,18 +35,25 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo
val z = row[BlockChangeView.z] val z = row[BlockChangeView.z]
val block = row[BlockChangeView.block] 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) { if (changeIsBreak) {
tracker.delete(location) tracker.delete(location)
} else { } else {
tracker.place(location, BlockStateTracker.BlockState(block)) tracker.place(location, BlockState(block))
} }
} }
} }
println("x,y,z,block") if (render) {
for ((position, block) in tracker.blocks) { val image = BlockStateImage()
println("${position.x},${position.y},${position.z},${block.type}") 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}")
}
} }
} }
} }

View File

@ -0,0 +1,19 @@
package cloud.kubelet.foundation.gjallarhorn.util
import java.awt.Color
class RandomColorKey {
private val colors = mutableMapOf<String, Color>()
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())
}

View File

@ -137,7 +137,14 @@ class FoundationHeimdallPlugin : JavaPlugin(), Listener {
@EventHandler @EventHandler
fun onEntityDeath(event: EntityDeathEvent) { fun onEntityDeath(event: EntityDeathEvent) {
val killer = event.entity.killer ?: return 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() { override fun onDisable() {
@ -145,7 +152,12 @@ class FoundationHeimdallPlugin : JavaPlugin(), Listener {
val endTime = Instant.now() val endTime = Instant.now()
for (playerId in playerJoinTimes.keys().toList()) { for (playerId in playerJoinTimes.keys().toList()) {
val startTime = playerJoinTimes.remove(playerId) ?: continue 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() bufferFlushThread.flush()
} }

View File

@ -1,4 +1,3 @@
--
create extension if not exists "uuid-ossp"; create extension if not exists "uuid-ossp";
-- --
create extension if not exists timescaledb; 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); 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;