mirror of
				https://github.com/GayPizzaSpecifications/foundation.git
				synced 2025-11-04 03:39:37 +00:00 
			
		
		
		
	Gjallarhorn: Implement render loop.
This commit is contained in:
		@ -1,15 +1,13 @@
 | 
			
		||||
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.PlayerLocationShareRenderer
 | 
			
		||||
import cloud.kubelet.foundation.gjallarhorn.render.BlockImageRenderer
 | 
			
		||||
import cloud.kubelet.foundation.gjallarhorn.render.*
 | 
			
		||||
import cloud.kubelet.foundation.gjallarhorn.state.*
 | 
			
		||||
import cloud.kubelet.foundation.gjallarhorn.util.compose
 | 
			
		||||
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
 | 
			
		||||
import com.github.ajalt.clikt.parameters.options.default
 | 
			
		||||
import com.github.ajalt.clikt.parameters.options.flag
 | 
			
		||||
import com.github.ajalt.clikt.parameters.options.option
 | 
			
		||||
import com.github.ajalt.clikt.parameters.options.required
 | 
			
		||||
@ -25,6 +23,7 @@ import java.awt.Font
 | 
			
		||||
import java.awt.font.TextLayout
 | 
			
		||||
import java.awt.image.BufferedImage
 | 
			
		||||
import java.time.Duration
 | 
			
		||||
import java.util.concurrent.ConcurrentHashMap
 | 
			
		||||
import java.util.concurrent.ScheduledThreadPoolExecutor
 | 
			
		||||
 | 
			
		||||
class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name = "block-change-timelapse") {
 | 
			
		||||
@ -47,15 +46,33 @@ class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name
 | 
			
		||||
  private val fromCoordinate by option("--trim-from", help = "Trim From Coordinate")
 | 
			
		||||
  private val toCoordinate by option("--trim-to", help = "Trim To Coordinate")
 | 
			
		||||
 | 
			
		||||
  private val parallelPoolSize by option("--pool-size", help = "Task Pool Size").int().default(8)
 | 
			
		||||
  private val inMemoryRender by option("--in-memory-render", help = "Render Images to Memory").flag()
 | 
			
		||||
  private val shouldRenderLoop by option("--loop-render", help = "Loop Render").flag()
 | 
			
		||||
  private val quadPixelNoop by option("--quad-pixel-noop", help = "Disable Quad Pixel Render").flag()
 | 
			
		||||
 | 
			
		||||
  private val logger = LoggerFactory.getLogger(BlockChangeTimelapseCommand::class.java)
 | 
			
		||||
 | 
			
		||||
  override fun run() {
 | 
			
		||||
    val threadPoolExecutor = ScheduledThreadPoolExecutor(16)
 | 
			
		||||
    if (quadPixelNoop) {
 | 
			
		||||
      BlockGridRenderer.globalQuadPixelNoop = true
 | 
			
		||||
    }
 | 
			
		||||
    val threadPoolExecutor = ScheduledThreadPoolExecutor(parallelPoolSize)
 | 
			
		||||
    if (shouldRenderLoop) {
 | 
			
		||||
      while (true) {
 | 
			
		||||
        perform(threadPoolExecutor)
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      perform(threadPoolExecutor)
 | 
			
		||||
    }
 | 
			
		||||
    threadPoolExecutor.shutdown()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun perform(threadPoolExecutor: ScheduledThreadPoolExecutor) {
 | 
			
		||||
    val trim = maybeBuildTrim()
 | 
			
		||||
    val filter = compose(
 | 
			
		||||
      combine = { a, b -> a and b },
 | 
			
		||||
    { trim?.first?.x != null } to { BlockChangeView.x greaterEq trim!!.first.x },
 | 
			
		||||
      { trim?.first?.x != null } to { BlockChangeView.x greaterEq trim!!.first.x },
 | 
			
		||||
      { trim?.first?.z != null } to { BlockChangeView.z greaterEq trim!!.first.z },
 | 
			
		||||
      { trim?.second?.x != null } to { BlockChangeView.x lessEq trim!!.second.x },
 | 
			
		||||
      { trim?.second?.z != null } to { BlockChangeView.z lessEq trim!!.second.z }
 | 
			
		||||
@ -77,6 +94,12 @@ class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name
 | 
			
		||||
 | 
			
		||||
    val imagePadCount = slices.size.toString().length
 | 
			
		||||
 | 
			
		||||
    val inMemoryPool = if (inMemoryRender) {
 | 
			
		||||
      ConcurrentHashMap<ChangelogSlice, BufferedImage>()
 | 
			
		||||
    } else {
 | 
			
		||||
      null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val pool = BlockMapRenderPool(
 | 
			
		||||
      changelog = changelog,
 | 
			
		||||
      blockTrackMode = if (considerAirBlocks) BlockTrackMode.AirOnDelete else BlockTrackMode.RemoveOnDelete,
 | 
			
		||||
@ -96,16 +119,19 @@ class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name
 | 
			
		||||
      layout.draw(graphics, 60f, 60f)
 | 
			
		||||
      graphics.dispose()
 | 
			
		||||
      val index = slices.indexOf(slice) + 1
 | 
			
		||||
      val suffix = "-${index.toString().padStart(imagePadCount, '0')}"
 | 
			
		||||
      result.savePngFile("${render.id}${suffix}.png")
 | 
			
		||||
      if (inMemoryRender) {
 | 
			
		||||
        inMemoryPool?.put(slice, result)
 | 
			
		||||
      } else {
 | 
			
		||||
        val suffix = "-${index.toString().padStart(imagePadCount, '0')}"
 | 
			
		||||
        result.savePngFile("${render.id}${suffix}.png")
 | 
			
		||||
      }
 | 
			
		||||
      logger.info("Rendered Timelapse Slice $index")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pool.render(slices)
 | 
			
		||||
    threadPoolExecutor.shutdown()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fun maybeBuildTrim(): Pair<BlockCoordinate, BlockCoordinate>? {
 | 
			
		||||
  private fun maybeBuildTrim(): Pair<BlockCoordinate, BlockCoordinate>? {
 | 
			
		||||
    if (fromCoordinate == null || toCoordinate == null) {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
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.state.BlockStateMap
 | 
			
		||||
import cloud.kubelet.foundation.gjallarhorn.state.ChangelogSlice
 | 
			
		||||
import cloud.kubelet.foundation.gjallarhorn.util.BlockColorKey
 | 
			
		||||
import cloud.kubelet.foundation.gjallarhorn.util.defaultBlockColorMap
 | 
			
		||||
@ -12,7 +12,7 @@ class BlockDiversityRenderer(val expanse: BlockExpanse, quadPixelSize: Int = def
 | 
			
		||||
  BlockGridRenderer(quadPixelSize) {
 | 
			
		||||
  private val blockColorKey = BlockColorKey(defaultBlockColorMap)
 | 
			
		||||
 | 
			
		||||
  override fun render(slice: ChangelogSlice, map: BlockMap): BufferedImage = buildPixelQuadImage(expanse) { graphics, x, z ->
 | 
			
		||||
  override fun render(slice: ChangelogSlice, map: BlockStateMap): BufferedImage = buildPixelQuadImage(expanse) { graphics, x, z ->
 | 
			
		||||
    val maybeYBlocks = map.blocks[x]?.get(z)
 | 
			
		||||
    if (maybeYBlocks == null) {
 | 
			
		||||
      setPixelQuad(graphics, x, z, Color.white)
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,9 @@ import java.awt.image.BufferedImage
 | 
			
		||||
 | 
			
		||||
abstract class BlockGridRenderer(val quadPixelSize: Int = defaultQuadPixelSize) : BlockImageRenderer {
 | 
			
		||||
  protected fun setPixelQuad(graphics: Graphics2D, x: Long, z: Long, color: Color) {
 | 
			
		||||
    if (globalQuadPixelNoop) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    drawSquare(graphics, x * quadPixelSize, z * quadPixelSize, quadPixelSize.toLong(), color)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -39,5 +42,6 @@ abstract class BlockGridRenderer(val quadPixelSize: Int = defaultQuadPixelSize)
 | 
			
		||||
 | 
			
		||||
  companion object {
 | 
			
		||||
    const val defaultQuadPixelSize = 4
 | 
			
		||||
    var globalQuadPixelNoop = false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,14 @@
 | 
			
		||||
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.state.BlockStateMap
 | 
			
		||||
import cloud.kubelet.foundation.gjallarhorn.state.ChangelogSlice
 | 
			
		||||
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(slice: ChangelogSlice, map: BlockMap): BufferedImage {
 | 
			
		||||
  override fun render(slice: ChangelogSlice, map: BlockStateMap): 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)
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
package cloud.kubelet.foundation.gjallarhorn.render
 | 
			
		||||
 | 
			
		||||
import cloud.kubelet.foundation.gjallarhorn.state.BlockMap
 | 
			
		||||
import cloud.kubelet.foundation.gjallarhorn.state.BlockStateMap
 | 
			
		||||
import cloud.kubelet.foundation.gjallarhorn.state.ChangelogSlice
 | 
			
		||||
 | 
			
		||||
interface BlockMapRenderer<T> {
 | 
			
		||||
  fun render(slice: ChangelogSlice, map: BlockMap): T
 | 
			
		||||
  fun render(slice: ChangelogSlice, map: BlockStateMap): T
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,6 @@
 | 
			
		||||
package cloud.kubelet.foundation.gjallarhorn.render
 | 
			
		||||
 | 
			
		||||
import cloud.kubelet.foundation.gjallarhorn.state.BlockCoordinate
 | 
			
		||||
import cloud.kubelet.foundation.gjallarhorn.state.BlockExpanse
 | 
			
		||||
import cloud.kubelet.foundation.gjallarhorn.state.BlockMap
 | 
			
		||||
import cloud.kubelet.foundation.gjallarhorn.state.ChangelogSlice
 | 
			
		||||
import cloud.kubelet.foundation.gjallarhorn.state.*
 | 
			
		||||
import cloud.kubelet.foundation.gjallarhorn.util.BlockColorKey
 | 
			
		||||
import cloud.kubelet.foundation.heimdall.table.PlayerPositionTable
 | 
			
		||||
import org.jetbrains.exposed.sql.Database
 | 
			
		||||
@ -12,37 +9,44 @@ import org.jetbrains.exposed.sql.select
 | 
			
		||||
import org.jetbrains.exposed.sql.transactions.transaction
 | 
			
		||||
import java.awt.Color
 | 
			
		||||
import java.awt.image.BufferedImage
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
class PlayerLocationShareRenderer(
 | 
			
		||||
  val expanse: BlockExpanse,
 | 
			
		||||
  val db: Database,
 | 
			
		||||
  quadPixelSize: Int = defaultQuadPixelSize) : BlockGridRenderer(quadPixelSize) {
 | 
			
		||||
  quadPixelSize: Int = defaultQuadPixelSize
 | 
			
		||||
) : BlockGridRenderer(quadPixelSize) {
 | 
			
		||||
  private val colorKey = BlockColorKey(mapOf())
 | 
			
		||||
 | 
			
		||||
  override fun render(slice: ChangelogSlice, map: BlockMap): BufferedImage {
 | 
			
		||||
  override fun render(slice: ChangelogSlice, map: BlockStateMap): BufferedImage {
 | 
			
		||||
    val start = slice.relativeChangeRange.start
 | 
			
		||||
    val end = slice.relativeChangeRange.endInclusive
 | 
			
		||||
 | 
			
		||||
    val playersToUniquePositions = transaction(db) {
 | 
			
		||||
    val playerSparseMap = BlockCoordinateSparseMap<MutableList<UUID>>()
 | 
			
		||||
    val allPlayerIds = HashSet<UUID>()
 | 
			
		||||
    transaction(db) {
 | 
			
		||||
      PlayerPositionTable.select {
 | 
			
		||||
        (PlayerPositionTable.time greater start) and
 | 
			
		||||
            (PlayerPositionTable.time lessEq end)
 | 
			
		||||
      }.map {
 | 
			
		||||
      }.forEach {
 | 
			
		||||
        val x = it[PlayerPositionTable.x].toLong()
 | 
			
		||||
        val y = it[PlayerPositionTable.y].toLong()
 | 
			
		||||
        val z = it[PlayerPositionTable.z].toLong()
 | 
			
		||||
        val coordinate = expanse.offset.applyAsOffset(BlockCoordinate(x, y, z))
 | 
			
		||||
        it[PlayerPositionTable.player] to coordinate
 | 
			
		||||
      }.distinct()
 | 
			
		||||
        val player = it[PlayerPositionTable.player]
 | 
			
		||||
        playerSparseMap.createOrModify(
 | 
			
		||||
          coordinate,
 | 
			
		||||
          create = { mutableListOf(player) },
 | 
			
		||||
          modify = { players -> players.add(player) })
 | 
			
		||||
        allPlayerIds.add(player)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val colorOfPlayers = playersToUniquePositions.map { it.first }
 | 
			
		||||
      .distinct()
 | 
			
		||||
      .associateWith { colorKey.map(it.toString()) }
 | 
			
		||||
    val colorOfPlayers = allPlayerIds.associateWith { colorKey.map(it.toString()) }
 | 
			
		||||
 | 
			
		||||
    return buildPixelQuadImage(expanse) { g, x, z ->
 | 
			
		||||
      val players = playersToUniquePositions.filter { it.second.x == x && it.second.z == z }.map { it.first }.distinct()
 | 
			
		||||
      if (players.isNotEmpty()) {
 | 
			
		||||
      val players = playerSparseMap.getVerticalSection(x, z)?.flatMap { it.value }?.distinct()
 | 
			
		||||
      if (players != null) {
 | 
			
		||||
        setPixelQuad(g, x, z, colorOfPlayers[players.first()]!!)
 | 
			
		||||
      } else {
 | 
			
		||||
        setPixelQuad(g, x, z, Color.white)
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,28 @@
 | 
			
		||||
package cloud.kubelet.foundation.gjallarhorn.state
 | 
			
		||||
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
open class BlockCoordinateSparseMap<T> {
 | 
			
		||||
  val blocks = TreeMap<Long, TreeMap<Long, TreeMap<Long, T>>>()
 | 
			
		||||
 | 
			
		||||
  fun get(position: BlockCoordinate): T? = blocks[position.x]?.get(position.z)?.get(position.z)
 | 
			
		||||
  fun getVerticalSection(x: Long, z: Long): Map<Long, T>? = blocks[x]?.get(z)
 | 
			
		||||
  fun getXSection(x: Long): Map<Long, Map<Long, T>>? = blocks[x]
 | 
			
		||||
 | 
			
		||||
  fun put(position: BlockCoordinate, value: T) {
 | 
			
		||||
    blocks.getOrPut(position.x) {
 | 
			
		||||
      TreeMap()
 | 
			
		||||
    }.getOrPut(position.z) {
 | 
			
		||||
      TreeMap()
 | 
			
		||||
    }[position.y] = value
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fun createOrModify(position: BlockCoordinate, create: () -> T, modify: (T) -> Unit) {
 | 
			
		||||
    val existing = get(position)
 | 
			
		||||
    if (existing == null) {
 | 
			
		||||
      put(position, create())
 | 
			
		||||
    } else {
 | 
			
		||||
      modify(existing)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -39,8 +39,8 @@ class BlockLogTracker(private val mode: BlockTrackMode = BlockTrackMode.RemoveOn
 | 
			
		||||
  fun isEmpty() = blocks.isEmpty()
 | 
			
		||||
  fun isNotEmpty() = !isEmpty()
 | 
			
		||||
 | 
			
		||||
  fun buildBlockMap(offset: BlockCoordinate = BlockCoordinate.zero): BlockMap {
 | 
			
		||||
    val map = BlockMap()
 | 
			
		||||
  fun buildBlockMap(offset: BlockCoordinate = BlockCoordinate.zero): BlockStateMap {
 | 
			
		||||
    val map = BlockStateMap()
 | 
			
		||||
    blocks.forEach { (position, state) ->
 | 
			
		||||
      val realPosition = offset.applyAsOffset(position)
 | 
			
		||||
      map.put(realPosition, state)
 | 
			
		||||
 | 
			
		||||
@ -1,15 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -2,7 +2,9 @@ package cloud.kubelet.foundation.gjallarhorn.state
 | 
			
		||||
 | 
			
		||||
import cloud.kubelet.foundation.gjallarhorn.render.BlockMapRenderer
 | 
			
		||||
import org.slf4j.LoggerFactory
 | 
			
		||||
import java.util.concurrent.*
 | 
			
		||||
import java.util.concurrent.ConcurrentHashMap
 | 
			
		||||
import java.util.concurrent.Future
 | 
			
		||||
import java.util.concurrent.ThreadPoolExecutor
 | 
			
		||||
 | 
			
		||||
class BlockMapRenderPool<T>(
 | 
			
		||||
  val changelog: BlockChangelog,
 | 
			
		||||
@ -51,7 +53,11 @@ class BlockMapRenderPool<T>(
 | 
			
		||||
    delegate.onAllPlaybackComplete(this, trackers)
 | 
			
		||||
 | 
			
		||||
    for (future in renderJobFutures.values) {
 | 
			
		||||
      future.get()
 | 
			
		||||
      try {
 | 
			
		||||
        future.get()
 | 
			
		||||
      } catch (e: Exception) {
 | 
			
		||||
        logger.error("Failed to render slice.", e)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,3 @@
 | 
			
		||||
package cloud.kubelet.foundation.gjallarhorn.state
 | 
			
		||||
 | 
			
		||||
class BlockStateMap : BlockCoordinateSparseMap<BlockState>()
 | 
			
		||||
		Reference in New Issue
	
	Block a user