mirror of
				https://github.com/GayPizzaSpecifications/foundation.git
				synced 2025-11-04 11:39:39 +00:00 
			
		
		
		
	Gjallarhorn: Refactor Rendering Code
This commit is contained in:
		@ -1,8 +1,10 @@
 | 
				
			|||||||
package cloud.kubelet.foundation.gjallarhorn.commands
 | 
					package cloud.kubelet.foundation.gjallarhorn.commands
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.gjallarhorn.render.BlockDiversityRenderer
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.gjallarhorn.render.BlockHeightMapRenderer
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.gjallarhorn.render.BlockImageRenderer
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.gjallarhorn.state.*
 | 
				
			||||||
import cloud.kubelet.foundation.gjallarhorn.util.compose
 | 
					import cloud.kubelet.foundation.gjallarhorn.util.compose
 | 
				
			||||||
import cloud.kubelet.foundation.gjallarhorn.render.*
 | 
					 | 
				
			||||||
import cloud.kubelet.foundation.gjallarhorn.util.RandomColorKey
 | 
					 | 
				
			||||||
import cloud.kubelet.foundation.gjallarhorn.util.savePngFile
 | 
					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
 | 
				
			||||||
@ -17,7 +19,6 @@ import org.jetbrains.exposed.sql.*
 | 
				
			|||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq
 | 
					import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq
 | 
				
			||||||
import org.jetbrains.exposed.sql.transactions.transaction
 | 
					import org.jetbrains.exposed.sql.transactions.transaction
 | 
				
			||||||
import org.slf4j.LoggerFactory
 | 
					import org.slf4j.LoggerFactory
 | 
				
			||||||
import java.awt.image.BufferedImage
 | 
					 | 
				
			||||||
import java.time.Duration
 | 
					import java.time.Duration
 | 
				
			||||||
import java.time.Instant
 | 
					import java.time.Instant
 | 
				
			||||||
import java.util.concurrent.ConcurrentHashMap
 | 
					import java.util.concurrent.ConcurrentHashMap
 | 
				
			||||||
@ -60,7 +61,7 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      val trackerPool = ScheduledThreadPoolExecutor(8)
 | 
					      val trackerPool = ScheduledThreadPoolExecutor(8)
 | 
				
			||||||
      val trackers = ConcurrentHashMap<Int, BlockStateTracker>()
 | 
					      val trackers = ConcurrentHashMap<Int, BlockLogTracker>()
 | 
				
			||||||
      for (time in intervals) {
 | 
					      for (time in intervals) {
 | 
				
			||||||
        trackerPool.submit {
 | 
					        trackerPool.submit {
 | 
				
			||||||
          val index = intervals.indexOf(time) + 1
 | 
					          val index = intervals.indexOf(time) + 1
 | 
				
			||||||
@ -77,19 +78,20 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      logger.info("State Tracking Completed")
 | 
					      logger.info("State Tracking Completed")
 | 
				
			||||||
      val allBlockOffsets = trackers.map { it.value.calculateZeroBlockOffset() }
 | 
					      val allBlockOffsets = trackers.map { it.value.calculateZeroBlockOffset() }
 | 
				
			||||||
      val globalBlockOffset = BlockPosition.maxOf(allBlockOffsets.asSequence())
 | 
					      val globalBlockOffset = BlockCoordinate.maxOf(allBlockOffsets.asSequence())
 | 
				
			||||||
      val allBlockMaxes = trackers.map { it.value.calculateMaxBlock() }
 | 
					      val allBlockMaxes = trackers.map { it.value.calculateMaxBlock() }
 | 
				
			||||||
      val globalBlockMax = BlockPosition.maxOf(allBlockMaxes.asSequence())
 | 
					      val globalBlockMax = BlockCoordinate.maxOf(allBlockMaxes.asSequence())
 | 
				
			||||||
      val globalBlockExpanse = BlockExpanse.offsetAndMax(globalBlockOffset, globalBlockMax)
 | 
					      val globalBlockExpanse = BlockExpanse.offsetAndMax(globalBlockOffset, globalBlockMax)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      logger.info("Calculations Completed")
 | 
					      logger.info("Calculations Completed")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      val renderState = render.createState()
 | 
					      val renderer = render.create(globalBlockExpanse)
 | 
				
			||||||
      val renderPool = ScheduledThreadPoolExecutor(16)
 | 
					      val renderPool = ScheduledThreadPoolExecutor(16)
 | 
				
			||||||
 | 
					      val imagePadCount = trackers.size.toString().length
 | 
				
			||||||
      for ((i, tracker) in trackers.entries) {
 | 
					      for ((i, tracker) in trackers.entries) {
 | 
				
			||||||
        renderPool.submit {
 | 
					        renderPool.submit {
 | 
				
			||||||
          val count = trackers.size.toString().length
 | 
					          val suffix = "-${i.toString().padStart(imagePadCount, '0')}"
 | 
				
			||||||
          saveRenderImage(renderState, tracker, globalBlockExpanse, "-${i.toString().padStart(count, '0')}")
 | 
					          saveRenderImage(renderer, tracker, globalBlockExpanse, suffix)
 | 
				
			||||||
          logger.info("Rendered Timelapse $i")
 | 
					          logger.info("Rendered Timelapse $i")
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -102,25 +104,24 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo
 | 
				
			|||||||
      val time = if (exactTimeAsString != null) Instant.parse(exactTimeAsString) else null
 | 
					      val time = if (exactTimeAsString != null) Instant.parse(exactTimeAsString) else null
 | 
				
			||||||
      val tracker = buildTrackerState(time, "Single-Time")
 | 
					      val tracker = buildTrackerState(time, "Single-Time")
 | 
				
			||||||
      val expanse = BlockExpanse.offsetAndMax(tracker.calculateZeroBlockOffset(), tracker.calculateMaxBlock())
 | 
					      val expanse = BlockExpanse.offsetAndMax(tracker.calculateZeroBlockOffset(), tracker.calculateMaxBlock())
 | 
				
			||||||
      saveRenderImage(render.createState(), tracker, expanse)
 | 
					      saveRenderImage(render.create(expanse), tracker, expanse)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fun saveRenderImage(renderState: Any, tracker: BlockStateTracker, expanse: BlockExpanse, suffix: String = "") {
 | 
					  fun saveRenderImage(renderer: BlockImageRenderer, tracker: BlockLogTracker, expanse: BlockExpanse, suffix: String = "") {
 | 
				
			||||||
    val state = BlockStateImage()
 | 
					    val map = tracker.buildBlockMap(expanse.offset)
 | 
				
			||||||
    tracker.populateStateImage(state, expanse.offset)
 | 
					    val image = renderer.render(map)
 | 
				
			||||||
    val image = render.renderBufferedImage(renderState, state, expanse)
 | 
					 | 
				
			||||||
    image.savePngFile("${render.id}${suffix}.png")
 | 
					    image.savePngFile("${render.id}${suffix}.png")
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fun buildTrackerState(time: Instant?, job: String): BlockStateTracker {
 | 
					  fun buildTrackerState(time: Instant?, job: String): BlockLogTracker {
 | 
				
			||||||
    val filter = compose(
 | 
					    val filter = compose(
 | 
				
			||||||
      combine = { a, b -> a and b },
 | 
					      combine = { a, b -> a and b },
 | 
				
			||||||
      { time != null } to { BlockChangeView.time lessEq time!! }
 | 
					      { time != null } to { BlockChangeView.time lessEq time!! }
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val tracker =
 | 
					    val tracker =
 | 
				
			||||||
      BlockStateTracker(if (considerAirBlocks) BlockTrackMode.AirOnDelete else BlockTrackMode.RemoveOnDelete)
 | 
					      BlockLogTracker(if (considerAirBlocks) BlockTrackMode.AirOnDelete else BlockTrackMode.RemoveOnDelete)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val blockChangeCounter = AtomicLong()
 | 
					    val blockChangeCounter = AtomicLong()
 | 
				
			||||||
    transaction(db) {
 | 
					    transaction(db) {
 | 
				
			||||||
@ -131,7 +132,7 @@ 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 = BlockPosition(x.toLong(), y.toLong(), z.toLong())
 | 
					        val location = BlockCoordinate(x.toLong(), y.toLong(), z.toLong())
 | 
				
			||||||
        if (changeIsBreak) {
 | 
					        if (changeIsBreak) {
 | 
				
			||||||
          tracker.delete(location)
 | 
					          tracker.delete(location)
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
@ -152,7 +153,7 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo
 | 
				
			|||||||
    return tracker
 | 
					    return tracker
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fun maybeTrimState(tracker: BlockStateTracker) {
 | 
					  fun maybeTrimState(tracker: BlockLogTracker) {
 | 
				
			||||||
    if (fromCoordinate == null || toCoordinate == null) {
 | 
					    if (fromCoordinate == null || toCoordinate == null) {
 | 
				
			||||||
      return
 | 
					      return
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -160,8 +161,8 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo
 | 
				
			|||||||
    val from = fromCoordinate!!.split(",").map { it.toLong() }
 | 
					    val from = fromCoordinate!!.split(",").map { it.toLong() }
 | 
				
			||||||
    val to = toCoordinate!!.split(",").map { it.toLong() }
 | 
					    val to = toCoordinate!!.split(",").map { it.toLong() }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val fromBlock = BlockPosition(from[0], 0, from[1])
 | 
					    val fromBlock = BlockCoordinate(from[0], 0, from[1])
 | 
				
			||||||
    val toBlock = BlockPosition(to[0], 0, to[1])
 | 
					    val toBlock = BlockCoordinate(to[0], 0, to[1])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    tracker.trimOutsideXAndZRange(fromBlock, toBlock)
 | 
					    tracker.trimOutsideXAndZRange(fromBlock, toBlock)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -169,17 +170,12 @@ class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-lo
 | 
				
			|||||||
  @Suppress("unused")
 | 
					  @Suppress("unused")
 | 
				
			||||||
  enum class RenderType(
 | 
					  enum class RenderType(
 | 
				
			||||||
    val id: String,
 | 
					    val id: String,
 | 
				
			||||||
    val createState: () -> Any,
 | 
					    val create: (BlockExpanse) -> BlockImageRenderer
 | 
				
			||||||
    val renderBufferedImage: (Any, BlockStateImage, BlockExpanse) -> BufferedImage
 | 
					 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    TopDown("top-down",
 | 
					    BlockDiversity("block-diversity", { expanse -> BlockDiversityRenderer(expanse) }),
 | 
				
			||||||
      { TopDownState(RandomColorKey()) },
 | 
					    HeightMap("height-map", { expanse -> BlockHeightMapRenderer(expanse) })
 | 
				
			||||||
      { state, image, expanse -> image.buildTopDownImage(expanse, (state as TopDownState).randomColorKey) }),
 | 
					 | 
				
			||||||
    HeightMap("height-map", { }, { _, image, expanse -> image.buildHeightMapImage(expanse) })
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  class TopDownState(val randomColorKey: RandomColorKey)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Suppress("unused")
 | 
					  @Suppress("unused")
 | 
				
			||||||
  enum class TimelapseMode(val id: String, val interval: Duration) {
 | 
					  enum class TimelapseMode(val id: String, val interval: Duration) {
 | 
				
			||||||
    ByHour("hours", Duration.ofHours(1)),
 | 
					    ByHour("hours", Duration.ofHours(1)),
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					package cloud.kubelet.foundation.gjallarhorn.render
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.gjallarhorn.state.BlockExpanse
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.gjallarhorn.state.BlockMap
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.gjallarhorn.util.RandomColorKey
 | 
				
			||||||
 | 
					import java.awt.Color
 | 
				
			||||||
 | 
					import java.awt.image.BufferedImage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BlockDiversityRenderer(val expanse: BlockExpanse, quadPixelSize: Int = defaultQuadPixelSize) : BlockGridRenderer(quadPixelSize) {
 | 
				
			||||||
 | 
					  private val randomColorKey = RandomColorKey()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override fun render(map: BlockMap): BufferedImage = buildPixelQuadImage(expanse) { x, z ->
 | 
				
			||||||
 | 
					    val maybeYBlocks = map.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 = randomColorKey.map(maxBlockState.type)
 | 
				
			||||||
 | 
					    setPixelQuad(x, z, color.rgb)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,13 +0,0 @@
 | 
				
			|||||||
package cloud.kubelet.foundation.gjallarhorn.render
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class BlockExpanse(
 | 
					 | 
				
			||||||
  val offset: BlockPosition,
 | 
					 | 
				
			||||||
  val size: BlockPosition
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  companion object {
 | 
					 | 
				
			||||||
    fun offsetAndMax(offset: BlockPosition, max: BlockPosition) = BlockExpanse(
 | 
					 | 
				
			||||||
      offset,
 | 
					 | 
				
			||||||
      offset.applyAsOffset(max)
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					package cloud.kubelet.foundation.gjallarhorn.render
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.gjallarhorn.state.BlockExpanse
 | 
				
			||||||
 | 
					import java.awt.Color
 | 
				
			||||||
 | 
					import java.awt.Rectangle
 | 
				
			||||||
 | 
					import java.awt.image.BufferedImage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					abstract class BlockGridRenderer(val quadPixelSize: Int = defaultQuadPixelSize) : BlockImageRenderer {
 | 
				
			||||||
 | 
					  protected fun BufferedImage.setPixelQuad(x: Long, z: Long, rgb: Int) {
 | 
				
			||||||
 | 
					    drawSquare(x * quadPixelSize, z * quadPixelSize, quadPixelSize.toLong(), rgb)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected fun BufferedImage.drawSquare(x: Long, y: Long, side: Long, rgb: Int) {
 | 
				
			||||||
 | 
					    val graphics = createGraphics()
 | 
				
			||||||
 | 
					    graphics.color = Color(rgb)
 | 
				
			||||||
 | 
					    graphics.fill(Rectangle(x.toInt(), y.toInt(), side.toInt(), side.toInt()))
 | 
				
			||||||
 | 
					    graphics.dispose()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected fun buildPixelQuadImage(
 | 
				
			||||||
 | 
					    expanse: BlockExpanse,
 | 
				
			||||||
 | 
					    callback: BufferedImage.(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_4BYTE_ABGR)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (x in 0 until widthInBlocks) {
 | 
				
			||||||
 | 
					      for (z in 0 until heightInBlocks) {
 | 
				
			||||||
 | 
					        callback(bufferedImage, x, z)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return bufferedImage
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  companion object {
 | 
				
			||||||
 | 
					    const val defaultQuadPixelSize = 4
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					package cloud.kubelet.foundation.gjallarhorn.render
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.gjallarhorn.state.BlockExpanse
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.gjallarhorn.util.ColorGradient
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.gjallarhorn.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) { 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)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					package cloud.kubelet.foundation.gjallarhorn.render
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.gjallarhorn.state.BlockExpanse
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.gjallarhorn.state.BlockMap
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.gjallarhorn.util.FloatClamp
 | 
				
			||||||
 | 
					import java.awt.image.BufferedImage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BlockHeightMapRenderer(val expanse: BlockExpanse, quadPixelSize: Int = defaultQuadPixelSize) :
 | 
				
			||||||
 | 
					  BlockHeatMapRenderer(quadPixelSize) {
 | 
				
			||||||
 | 
					  override fun render(map: BlockMap): BufferedImage {
 | 
				
			||||||
 | 
					    val yMin = map.blocks.minOf { xSection -> xSection.value.minOf { zSection -> zSection.value.minOf { it.key } } }
 | 
				
			||||||
 | 
					    val yMax = map.blocks.maxOf { xSection -> xSection.value.maxOf { zSection -> zSection.value.maxOf { it.key } } }
 | 
				
			||||||
 | 
					    val clamp = FloatClamp(yMin, yMax)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return buildHeatMapImage(expanse, clamp) { x, z -> map.blocks[x]?.get(z)?.maxOf { it.key } }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					package cloud.kubelet.foundation.gjallarhorn.render
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.awt.image.BufferedImage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface BlockImageRenderer : BlockMapRenderer<BufferedImage>
 | 
				
			||||||
@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					package cloud.kubelet.foundation.gjallarhorn.render
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.gjallarhorn.state.BlockMap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface BlockMapRenderer<T> {
 | 
				
			||||||
 | 
					  fun render(map: BlockMap): T
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,37 +0,0 @@
 | 
				
			|||||||
package cloud.kubelet.foundation.gjallarhorn.render
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  fun applyAsOffset(position: BlockPosition) = position.copy(
 | 
					 | 
				
			||||||
    x = position.x + x,
 | 
					 | 
				
			||||||
    y = position.y + y,
 | 
					 | 
				
			||||||
    z = position.z + z
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  companion object {
 | 
					 | 
				
			||||||
    val zero = BlockPosition(0, 0, 0)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fun maxOf(positions: Sequence<BlockPosition>): BlockPosition {
 | 
					 | 
				
			||||||
      val x = positions.maxOf { it.x }
 | 
					 | 
				
			||||||
      val y = positions.maxOf { it.y }
 | 
					 | 
				
			||||||
      val z = positions.maxOf { it.z }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return BlockPosition(x, y, z)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,89 +0,0 @@
 | 
				
			|||||||
package cloud.kubelet.foundation.gjallarhorn.render
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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.Rectangle
 | 
					 | 
				
			||||||
import java.awt.image.BufferedImage
 | 
					 | 
				
			||||||
import java.util.*
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class BlockStateImage {
 | 
					 | 
				
			||||||
  private 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 buildTopDownImage(expanse: BlockExpanse, randomColorKey: RandomColorKey): BufferedImage {
 | 
					 | 
				
			||||||
    return buildPixelQuadImage(expanse) { 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 = randomColorKey.map(maxBlockState.type)
 | 
					 | 
				
			||||||
      setPixelQuad(x, z, color.rgb)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  fun buildHeightMapImage(expanse: BlockExpanse): 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(expanse, clamp) { x, z -> blocks[x]?.get(z)?.maxOf { it.key } }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  fun buildHeatMapImage(expanse: BlockExpanse, clamp: FloatClamp, calculate: (Long, Long) -> Long?): BufferedImage =
 | 
					 | 
				
			||||||
    buildPixelQuadImage(expanse) { 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) {
 | 
					 | 
				
			||||||
    drawSquare(x * quadImageSize, z * quadImageSize, quadImageSize.toLong(), rgb)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private fun BufferedImage.drawSquare(x: Long, y: Long, side: Long, rgb: Int) {
 | 
					 | 
				
			||||||
    val graphics = createGraphics()
 | 
					 | 
				
			||||||
    graphics.color = Color(rgb)
 | 
					 | 
				
			||||||
    graphics.fill(Rectangle(x.toInt(), y.toInt(), side.toInt(), side.toInt()))
 | 
					 | 
				
			||||||
    graphics.dispose()
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private fun buildPixelQuadImage(expanse: BlockExpanse, callback: BufferedImage.(Long, Long) -> Unit): BufferedImage {
 | 
					 | 
				
			||||||
    val width = expanse.size.x
 | 
					 | 
				
			||||||
    val height = expanse.size.z
 | 
					 | 
				
			||||||
    val bufferedImage =
 | 
					 | 
				
			||||||
      BufferedImage(width.toInt() * quadImageSize, height.toInt() * quadImageSize, BufferedImage.TYPE_4BYTE_ABGR)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (x in 0 until width) {
 | 
					 | 
				
			||||||
      for (z in 0 until height) {
 | 
					 | 
				
			||||||
        callback(bufferedImage, x, z)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return bufferedImage
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  companion object {
 | 
					 | 
				
			||||||
    const val quadImageSize = 4
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					package cloud.kubelet.foundation.gjallarhorn.state
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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: Sequence<BlockCoordinate>): BlockCoordinate {
 | 
				
			||||||
 | 
					      val x = coordinates.maxOf { it.x }
 | 
				
			||||||
 | 
					      val y = coordinates.maxOf { it.y }
 | 
				
			||||||
 | 
					      val z = coordinates.maxOf { it.z }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return BlockCoordinate(x, y, z)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					package cloud.kubelet.foundation.gjallarhorn.state
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BlockExpanse(
 | 
				
			||||||
 | 
					  val offset: BlockCoordinate,
 | 
				
			||||||
 | 
					  val size: BlockCoordinate
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  companion object {
 | 
				
			||||||
 | 
					    fun offsetAndMax(offset: BlockCoordinate, max: BlockCoordinate) = BlockExpanse(
 | 
				
			||||||
 | 
					      offset,
 | 
				
			||||||
 | 
					      offset.applyAsOffset(max)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,15 +1,15 @@
 | 
				
			|||||||
package cloud.kubelet.foundation.gjallarhorn.render
 | 
					package cloud.kubelet.foundation.gjallarhorn.state
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import kotlin.math.absoluteValue
 | 
					import kotlin.math.absoluteValue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BlockStateTracker(private val mode: BlockTrackMode = BlockTrackMode.RemoveOnDelete) {
 | 
					class BlockLogTracker(private val mode: BlockTrackMode = BlockTrackMode.RemoveOnDelete) {
 | 
				
			||||||
  val blocks = HashMap<BlockPosition, BlockState>()
 | 
					  val blocks = HashMap<BlockCoordinate, BlockState>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fun place(position: BlockPosition, state: BlockState) {
 | 
					  fun place(position: BlockCoordinate, state: BlockState) {
 | 
				
			||||||
    blocks[position] = state
 | 
					    blocks[position] = state
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fun delete(position: BlockPosition) {
 | 
					  fun delete(position: BlockCoordinate) {
 | 
				
			||||||
    if (mode == BlockTrackMode.AirOnDelete) {
 | 
					    if (mode == BlockTrackMode.AirOnDelete) {
 | 
				
			||||||
      blocks[position] = BlockState("minecraft:air")
 | 
					      blocks[position] = BlockState("minecraft:air")
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
@ -17,7 +17,7 @@ class BlockStateTracker(private val mode: BlockTrackMode = BlockTrackMode.Remove
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fun trimOutsideXAndZRange(min: BlockPosition, max: BlockPosition) {
 | 
					  fun trimOutsideXAndZRange(min: BlockCoordinate, max: BlockCoordinate) {
 | 
				
			||||||
    val blockPositionsToRemove = blocks.keys.filter {
 | 
					    val blockPositionsToRemove = blocks.keys.filter {
 | 
				
			||||||
      it.x < min.x ||
 | 
					      it.x < min.x ||
 | 
				
			||||||
          it.z < min.z ||
 | 
					          it.z < min.z ||
 | 
				
			||||||
@ -28,7 +28,7 @@ class BlockStateTracker(private val mode: BlockTrackMode = BlockTrackMode.Remove
 | 
				
			|||||||
    blockPositionsToRemove.forEach { blocks.remove(it) }
 | 
					    blockPositionsToRemove.forEach { blocks.remove(it) }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fun calculateZeroBlockOffset(): BlockPosition {
 | 
					  fun calculateZeroBlockOffset(): BlockCoordinate {
 | 
				
			||||||
    val x = blocks.keys.minOf { it.x }
 | 
					    val x = blocks.keys.minOf { it.x }
 | 
				
			||||||
    val y = blocks.keys.minOf { it.y }
 | 
					    val y = blocks.keys.minOf { it.y }
 | 
				
			||||||
    val z = blocks.keys.minOf { it.z }
 | 
					    val z = blocks.keys.minOf { it.z }
 | 
				
			||||||
@ -37,22 +37,24 @@ class BlockStateTracker(private val mode: BlockTrackMode = BlockTrackMode.Remove
 | 
				
			|||||||
    val yOffset = if (y < 0) y.absoluteValue else 0
 | 
					    val yOffset = if (y < 0) y.absoluteValue else 0
 | 
				
			||||||
    val zOffset = if (z < 0) z.absoluteValue else 0
 | 
					    val zOffset = if (z < 0) z.absoluteValue else 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return BlockPosition(xOffset, yOffset, zOffset)
 | 
					    return BlockCoordinate(xOffset, yOffset, zOffset)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fun calculateMaxBlock(): BlockPosition {
 | 
					  fun calculateMaxBlock(): BlockCoordinate {
 | 
				
			||||||
    val x = blocks.keys.maxOf { it.x }
 | 
					    val x = blocks.keys.maxOf { it.x }
 | 
				
			||||||
    val y = blocks.keys.maxOf { it.y }
 | 
					    val y = blocks.keys.maxOf { it.y }
 | 
				
			||||||
    val z = blocks.keys.maxOf { it.z }
 | 
					    val z = blocks.keys.maxOf { it.z }
 | 
				
			||||||
    return BlockPosition(x, y, z)
 | 
					    return BlockCoordinate(x, y, z)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fun isEmpty() = blocks.isEmpty()
 | 
					  fun isEmpty() = blocks.isEmpty()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fun populateStateImage(image: BlockStateImage, offset: BlockPosition = BlockPosition.zero) {
 | 
					  fun buildBlockMap(offset: BlockCoordinate = BlockCoordinate.zero): BlockMap {
 | 
				
			||||||
 | 
					    val map = BlockMap()
 | 
				
			||||||
    blocks.forEach { (position, state) ->
 | 
					    blocks.forEach { (position, state) ->
 | 
				
			||||||
      val realPosition = offset.applyAsOffset(position)
 | 
					      val realPosition = offset.applyAsOffset(position)
 | 
				
			||||||
      image.put(realPosition, state)
 | 
					      map.put(realPosition, state)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    return map
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					package cloud.kubelet.foundation.gjallarhorn.state
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BlockMap {
 | 
				
			||||||
 | 
					  val blocks = TreeMap<Long, TreeMap<Long, TreeMap<Long, BlockState>>>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  fun put(position: BlockCoordinate, state: BlockState) {
 | 
				
			||||||
 | 
					    blocks.getOrPut(position.x) {
 | 
				
			||||||
 | 
					      TreeMap()
 | 
				
			||||||
 | 
					    }.getOrPut(position.z) {
 | 
				
			||||||
 | 
					      TreeMap()
 | 
				
			||||||
 | 
					    }[position.y] = state
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
package cloud.kubelet.foundation.gjallarhorn.render
 | 
					package cloud.kubelet.foundation.gjallarhorn.state
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import kotlinx.serialization.Serializable
 | 
					import kotlinx.serialization.Serializable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
package cloud.kubelet.foundation.gjallarhorn.render
 | 
					package cloud.kubelet.foundation.gjallarhorn.state
 | 
				
			||||||
 | 
					
 | 
				
			||||||
enum class BlockTrackMode {
 | 
					enum class BlockTrackMode {
 | 
				
			||||||
  RemoveOnDelete,
 | 
					  RemoveOnDelete,
 | 
				
			||||||
		Reference in New Issue
	
	Block a user