mirror of
https://github.com/GayPizzaSpecifications/foundation.git
synced 2025-08-03 13:31:32 +00:00
Gjallarhorn: Heat Map Support
This commit is contained in:
@ -1,12 +1,14 @@
|
|||||||
package cloud.kubelet.foundation.gjallarhorn
|
package cloud.kubelet.foundation.gjallarhorn
|
||||||
|
|
||||||
|
import cloud.kubelet.foundation.gjallarhorn.util.ColorGradient
|
||||||
|
import cloud.kubelet.foundation.gjallarhorn.util.FloatClamp
|
||||||
import cloud.kubelet.foundation.gjallarhorn.util.RandomColorKey
|
import cloud.kubelet.foundation.gjallarhorn.util.RandomColorKey
|
||||||
import java.awt.Color
|
import java.awt.Color
|
||||||
import java.awt.image.BufferedImage
|
import java.awt.image.BufferedImage
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class BlockStateImage {
|
class BlockStateImage {
|
||||||
val blocks = TreeMap<Long, TreeMap<Long, TreeMap<Long, BlockState>>>()
|
private val blocks = TreeMap<Long, TreeMap<Long, TreeMap<Long, BlockState>>>()
|
||||||
|
|
||||||
fun put(position: BlockPosition, state: BlockState) {
|
fun put(position: BlockPosition, state: BlockState) {
|
||||||
blocks.getOrPut(position.x) {
|
blocks.getOrPut(position.x) {
|
||||||
@ -16,34 +18,61 @@ class BlockStateImage {
|
|||||||
}[position.y] = state
|
}[position.y] = state
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildBufferedImage(): BufferedImage {
|
fun buildTopDownImage(): BufferedImage {
|
||||||
val colorKey = RandomColorKey()
|
val colorKey = RandomColorKey()
|
||||||
val xMax = blocks.keys.maxOf { it }
|
return buildPixelQuadImage { x, z ->
|
||||||
val zMax = blocks.maxOf { it.value.maxOf { it.key } }
|
val maybeYBlocks = blocks[x]?.get(z)
|
||||||
|
if (maybeYBlocks == null) {
|
||||||
|
setPixelQuad(x, z, Color.white.rgb)
|
||||||
|
return@buildPixelQuadImage
|
||||||
|
}
|
||||||
|
val maxBlockState = maybeYBlocks.maxByOrNull { it.key }?.value
|
||||||
|
if (maxBlockState == null) {
|
||||||
|
setPixelQuad(x, z, Color.white.rgb)
|
||||||
|
return@buildPixelQuadImage
|
||||||
|
}
|
||||||
|
|
||||||
|
val color = colorKey.map(maxBlockState.type)
|
||||||
|
setPixelQuad(x, z, color.rgb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildHeightMapImage(): BufferedImage {
|
||||||
|
val yMin = blocks.minOf { xSection -> xSection.value.minOf { zSection -> zSection.value.minOf { it.key } } }
|
||||||
|
val yMax = blocks.maxOf { xSection -> xSection.value.maxOf { zSection -> zSection.value.maxOf { it.key } } }
|
||||||
|
val clamp = FloatClamp(yMin, yMax)
|
||||||
|
|
||||||
|
return buildHeatMapImage(clamp) { x, z -> blocks[x]?.get(z)?.maxOf { it.key } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildHeatMapImage(clamp: FloatClamp, calculate: (Long, Long) -> Long?): BufferedImage =
|
||||||
|
buildPixelQuadImage { 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(x, z, color.rgb)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BufferedImage.setPixelQuad(x: Long, z: Long, rgb: Int) {
|
||||||
|
setRGB(x.toInt() * 2, z.toInt() * 2, rgb)
|
||||||
|
setRGB((x.toInt() * 2) + 1, z.toInt() * 2, rgb)
|
||||||
|
setRGB(x.toInt() * 2, (z.toInt() * 2) + 1, rgb)
|
||||||
|
setRGB((x.toInt() * 2) + 1, (z.toInt() * 2) + 1, rgb)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildPixelQuadImage(callback: BufferedImage.(Long, Long) -> Unit): BufferedImage {
|
||||||
|
val xMax = blocks.keys.maxOf { it }
|
||||||
|
val zMax = blocks.maxOf { xSection -> xSection.value.maxOf { zSection -> zSection.key } }
|
||||||
val bufferedImage = BufferedImage(xMax.toInt() * 2, zMax.toInt() * 2, BufferedImage.TYPE_4BYTE_ABGR)
|
val bufferedImage = BufferedImage(xMax.toInt() * 2, zMax.toInt() * 2, BufferedImage.TYPE_4BYTE_ABGR)
|
||||||
|
|
||||||
for (x in 0 until xMax) {
|
for (x in 0 until xMax) {
|
||||||
for (z in 0 until zMax) {
|
for (z in 0 until zMax) {
|
||||||
fun set(rgb: Int) {
|
callback(bufferedImage, x, z)
|
||||||
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
|
return bufferedImage
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
package cloud.kubelet.foundation.gjallarhorn
|
package cloud.kubelet.foundation.gjallarhorn
|
||||||
|
|
||||||
import kotlin.collections.HashMap
|
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
class BlockStateTracker {
|
class BlockStateTracker(private val mode: BlockTrackMode = BlockTrackMode.RemoveOnDelete) {
|
||||||
val blocks = HashMap<BlockPosition, BlockState>()
|
val blocks = HashMap<BlockPosition, BlockState>()
|
||||||
|
|
||||||
fun place(position: BlockPosition, state: BlockState) {
|
fun place(position: BlockPosition, state: BlockState) {
|
||||||
@ -11,7 +10,11 @@ class BlockStateTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun delete(position: BlockPosition) {
|
fun delete(position: BlockPosition) {
|
||||||
blocks.remove(position)
|
if (mode == BlockTrackMode.AirOnDelete) {
|
||||||
|
blocks[position] = BlockState("minecraft:air")
|
||||||
|
} else {
|
||||||
|
blocks.remove(position)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun calculateZeroBlockOffset(): BlockOffset {
|
fun calculateZeroBlockOffset(): BlockOffset {
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
package cloud.kubelet.foundation.gjallarhorn
|
||||||
|
|
||||||
|
enum class BlockTrackMode {
|
||||||
|
RemoveOnDelete,
|
||||||
|
AirOnDelete
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package cloud.kubelet.foundation.gjallarhorn.commands
|
package cloud.kubelet.foundation.gjallarhorn.commands
|
||||||
|
|
||||||
import cloud.kubelet.foundation.gjallarhorn.*
|
import cloud.kubelet.foundation.gjallarhorn.*
|
||||||
|
import cloud.kubelet.foundation.gjallarhorn.util.savePngFile
|
||||||
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
|
||||||
@ -11,22 +12,26 @@ 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
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
|
|
||||||
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()
|
private val renderTopDown by option("--render-top-down", help = "Render TOp Down Image").flag()
|
||||||
|
private val renderHeightMap by option("--render-height-map", help = "Render Height Map Image").flag()
|
||||||
|
|
||||||
|
private val considerAirBlocks by option("--consider-air-blocks", help = "Enable Air Block Consideration").flag()
|
||||||
|
|
||||||
override fun run() {
|
override fun run() {
|
||||||
val filter = compose(
|
val filter = compose(
|
||||||
combine = { a, b -> a and b },
|
combine = { a, b -> a and b },
|
||||||
{ timeAsString != null } to { BlockChangeView.time lessEq Instant.parse(timeAsString) }
|
{ timeAsString != null } to { BlockChangeView.time lessEq Instant.parse(timeAsString) }
|
||||||
)
|
)
|
||||||
val tracker = BlockStateTracker()
|
val tracker =
|
||||||
|
BlockStateTracker(if (considerAirBlocks) BlockTrackMode.AirOnDelete else BlockTrackMode.RemoveOnDelete)
|
||||||
|
|
||||||
|
val blockChangeCounter = AtomicLong()
|
||||||
transaction(db) {
|
transaction(db) {
|
||||||
BlockChangeView.select(filter).orderBy(BlockChangeView.time).forEach { row ->
|
BlockChangeView.select(filter).orderBy(BlockChangeView.time).forEach { row ->
|
||||||
val changeIsBreak = row[BlockChangeView.isBreak]
|
val changeIsBreak = row[BlockChangeView.isBreak]
|
||||||
@ -41,14 +46,31 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo
|
|||||||
} else {
|
} else {
|
||||||
tracker.place(location, BlockState(block))
|
tracker.place(location, BlockState(block))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val count = blockChangeCounter.addAndGet(1)
|
||||||
|
if (count % 1000L == 0L) {
|
||||||
|
System.err.println("Calculating Block Changes... $count")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
System.err.println("Total Block Changes... ${blockChangeCounter.get()}")
|
||||||
|
|
||||||
if (render) {
|
val uniqueBlockPositions = tracker.blocks.size
|
||||||
|
System.err.println("Unique Block Positions... $uniqueBlockPositions")
|
||||||
|
|
||||||
|
val blockZeroOffset = tracker.calculateZeroBlockOffset()
|
||||||
|
System.err.println("Zero Block Offset... $blockZeroOffset")
|
||||||
|
|
||||||
|
if (renderTopDown) {
|
||||||
val image = BlockStateImage()
|
val image = BlockStateImage()
|
||||||
tracker.populate(image, offset = tracker.calculateZeroBlockOffset())
|
tracker.populate(image, offset = blockZeroOffset)
|
||||||
val bufferedImage = image.buildBufferedImage()
|
val bufferedImage = image.buildTopDownImage()
|
||||||
ImageIO.write(bufferedImage, "png", File("top-down.png"))
|
bufferedImage.savePngFile("top-down.png")
|
||||||
|
} else if (renderHeightMap) {
|
||||||
|
val image = BlockStateImage()
|
||||||
|
tracker.populate(image, offset = blockZeroOffset)
|
||||||
|
val bufferedImage = image.buildHeightMapImage()
|
||||||
|
bufferedImage.savePngFile("height-map.png")
|
||||||
} else {
|
} else {
|
||||||
println("x,y,z,block")
|
println("x,y,z,block")
|
||||||
for ((position, block) in tracker.blocks) {
|
for ((position, block) in tracker.blocks) {
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
package cloud.kubelet.foundation.gjallarhorn.util
|
||||||
|
|
||||||
|
import java.awt.Color
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
class ColorGradient {
|
||||||
|
data class ColorPoint(
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val points = mutableListOf<ColorPoint>()
|
||||||
|
|
||||||
|
fun addColorPoint(red: Float, green: Float, blue: Float, value: Float) {
|
||||||
|
val point = ColorPoint(red, green, blue, value)
|
||||||
|
for (x in 0 until points.size) {
|
||||||
|
if (value < points[x].value) {
|
||||||
|
points.add(x, point)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
points.add(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getColorAtValue(value: Float): Color {
|
||||||
|
if (points.isEmpty()) {
|
||||||
|
return ColorPoint(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 ColorPoint(
|
||||||
|
(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().apply {
|
||||||
|
addColorPoint(0f, 0f, 1f, 0.0f)
|
||||||
|
addColorPoint(0f, 1f, 1f, 0.25f)
|
||||||
|
addColorPoint(0f, 1f, 0f, 0.5f)
|
||||||
|
addColorPoint(1f, 1f, 0f, 0.75f)
|
||||||
|
addColorPoint(1f, 0f, 0f, 1.0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package cloud.kubelet.foundation.gjallarhorn.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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package cloud.kubelet.foundation.gjallarhorn.util
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
import java.io.File
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
|
||||||
|
fun BufferedImage.savePngFile(path: String) = ImageIO.write(this, "png", File(path))
|
@ -0,0 +1,64 @@
|
|||||||
|
WITH
|
||||||
|
unique_player_ids AS (
|
||||||
|
SELECT
|
||||||
|
DISTINCT player
|
||||||
|
FROM heimdall.player_sessions
|
||||||
|
),
|
||||||
|
player_names AS (
|
||||||
|
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
|
||||||
|
),
|
||||||
|
unique_world_ids AS (
|
||||||
|
SELECT
|
||||||
|
DISTINCT to_world AS world
|
||||||
|
FROM heimdall.world_changes
|
||||||
|
),
|
||||||
|
world_names AS (
|
||||||
|
SELECT
|
||||||
|
world,
|
||||||
|
(
|
||||||
|
SELECT to_world_name
|
||||||
|
FROM heimdall.world_changes
|
||||||
|
WHERE world = heimdall.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 heimdall.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
|
Reference in New Issue
Block a user