mirror of
				https://github.com/GayPizzaSpecifications/foundation.git
				synced 2025-11-04 11:39:39 +00:00 
			
		
		
		
	Gjallarhorn: Heat Map Support
This commit is contained in:
		@ -1,12 +1,14 @@
 | 
			
		||||
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 java.awt.Color
 | 
			
		||||
import java.awt.image.BufferedImage
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
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) {
 | 
			
		||||
    blocks.getOrPut(position.x) {
 | 
			
		||||
@ -16,34 +18,61 @@ class BlockStateImage {
 | 
			
		||||
    }[position.y] = state
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fun buildBufferedImage(): BufferedImage {
 | 
			
		||||
  fun buildTopDownImage(): BufferedImage {
 | 
			
		||||
    val colorKey = RandomColorKey()
 | 
			
		||||
    val xMax = blocks.keys.maxOf { it }
 | 
			
		||||
    val zMax = blocks.maxOf { it.value.maxOf { it.key } }
 | 
			
		||||
    return buildPixelQuadImage { x, z ->
 | 
			
		||||
      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)
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
        callback(bufferedImage, x, z)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return bufferedImage
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,8 @@
 | 
			
		||||
package cloud.kubelet.foundation.gjallarhorn
 | 
			
		||||
 | 
			
		||||
import kotlin.collections.HashMap
 | 
			
		||||
import kotlin.math.absoluteValue
 | 
			
		||||
 | 
			
		||||
class BlockStateTracker {
 | 
			
		||||
class BlockStateTracker(private val mode: BlockTrackMode = BlockTrackMode.RemoveOnDelete) {
 | 
			
		||||
  val blocks = HashMap<BlockPosition, BlockState>()
 | 
			
		||||
 | 
			
		||||
  fun place(position: BlockPosition, state: BlockState) {
 | 
			
		||||
@ -11,7 +10,11 @@ class BlockStateTracker {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fun delete(position: BlockPosition) {
 | 
			
		||||
    blocks.remove(position)
 | 
			
		||||
    if (mode == BlockTrackMode.AirOnDelete) {
 | 
			
		||||
      blocks[position] = BlockState("minecraft:air")
 | 
			
		||||
    } else {
 | 
			
		||||
      blocks.remove(position)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
 | 
			
		||||
import cloud.kubelet.foundation.gjallarhorn.*
 | 
			
		||||
import cloud.kubelet.foundation.gjallarhorn.util.savePngFile
 | 
			
		||||
import cloud.kubelet.foundation.heimdall.view.BlockChangeView
 | 
			
		||||
import com.github.ajalt.clikt.core.CliktCommand
 | 
			
		||||
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.select
 | 
			
		||||
import org.jetbrains.exposed.sql.transactions.transaction
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.time.Instant
 | 
			
		||||
import javax.imageio.ImageIO
 | 
			
		||||
import java.util.concurrent.atomic.AtomicLong
 | 
			
		||||
 | 
			
		||||
class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-log") {
 | 
			
		||||
  private val db by requireObject<Database>()
 | 
			
		||||
  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() {
 | 
			
		||||
    val filter = compose(
 | 
			
		||||
      combine = { a, b -> a and b },
 | 
			
		||||
      { 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) {
 | 
			
		||||
      BlockChangeView.select(filter).orderBy(BlockChangeView.time).forEach { row ->
 | 
			
		||||
        val changeIsBreak = row[BlockChangeView.isBreak]
 | 
			
		||||
@ -41,14 +46,31 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo
 | 
			
		||||
        } else {
 | 
			
		||||
          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()
 | 
			
		||||
      tracker.populate(image, offset = tracker.calculateZeroBlockOffset())
 | 
			
		||||
      val bufferedImage = image.buildBufferedImage()
 | 
			
		||||
      ImageIO.write(bufferedImage, "png", File("top-down.png"))
 | 
			
		||||
      tracker.populate(image, offset = blockZeroOffset)
 | 
			
		||||
      val bufferedImage = image.buildTopDownImage()
 | 
			
		||||
      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 {
 | 
			
		||||
      println("x,y,z,block")
 | 
			
		||||
      for ((position, block) in tracker.blocks) {
 | 
			
		||||
@ -56,4 +78,4 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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))
 | 
			
		||||
@ -16,4 +16,4 @@ class RandomColorKey {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun randomColor() = Color((Math.random() * 0x1000000).toInt())
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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