diff --git a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/BlockChangeTimelapseCommand.kt b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/BlockChangeTimelapseCommand.kt index 36b26a3..0c31ae8 100644 --- a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/BlockChangeTimelapseCommand.kt +++ b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/BlockChangeTimelapseCommand.kt @@ -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() + } 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? { + private fun maybeBuildTrim(): Pair? { if (fromCoordinate == null || toCoordinate == null) { return null } diff --git a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/BlockDiversityRenderer.kt b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/BlockDiversityRenderer.kt index 03bd2a0..578acbb 100644 --- a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/BlockDiversityRenderer.kt +++ b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/BlockDiversityRenderer.kt @@ -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) diff --git a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/BlockGridRenderer.kt b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/BlockGridRenderer.kt index 7fa333d..4cac6ca 100644 --- a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/BlockGridRenderer.kt +++ b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/BlockGridRenderer.kt @@ -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 } } diff --git a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/BlockHeightMapRenderer.kt b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/BlockHeightMapRenderer.kt index cc32bf3..6db6304 100644 --- a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/BlockHeightMapRenderer.kt +++ b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/BlockHeightMapRenderer.kt @@ -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) diff --git a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/BlockMapRenderer.kt b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/BlockMapRenderer.kt index eb34026..4d47ac1 100644 --- a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/BlockMapRenderer.kt +++ b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/BlockMapRenderer.kt @@ -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 { - fun render(slice: ChangelogSlice, map: BlockMap): T + fun render(slice: ChangelogSlice, map: BlockStateMap): T } diff --git a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/PlayerLocationShareRenderer.kt b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/PlayerLocationShareRenderer.kt index e16b8f5..8c28e36 100644 --- a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/PlayerLocationShareRenderer.kt +++ b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/render/PlayerLocationShareRenderer.kt @@ -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>() + val allPlayerIds = HashSet() + 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) diff --git a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockCoordinateSparseMap.kt b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockCoordinateSparseMap.kt new file mode 100644 index 0000000..792a272 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockCoordinateSparseMap.kt @@ -0,0 +1,28 @@ +package cloud.kubelet.foundation.gjallarhorn.state + +import java.util.* + +open class BlockCoordinateSparseMap { + val blocks = TreeMap>>() + + fun get(position: BlockCoordinate): T? = blocks[position.x]?.get(position.z)?.get(position.z) + fun getVerticalSection(x: Long, z: Long): Map? = blocks[x]?.get(z) + fun getXSection(x: Long): Map>? = 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) + } + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockLogTracker.kt b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockLogTracker.kt index 3475cb0..8f30b04 100644 --- a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockLogTracker.kt +++ b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockLogTracker.kt @@ -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) diff --git a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockMap.kt b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockMap.kt deleted file mode 100644 index 5ae93af..0000000 --- a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockMap.kt +++ /dev/null @@ -1,15 +0,0 @@ -package cloud.kubelet.foundation.gjallarhorn.state - -import java.util.* - -class BlockMap { - val blocks = TreeMap>>() - - fun put(position: BlockCoordinate, state: BlockState) { - blocks.getOrPut(position.x) { - TreeMap() - }.getOrPut(position.z) { - TreeMap() - }[position.y] = state - } -} diff --git a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockMapRenderPool.kt b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockMapRenderPool.kt index a3ba841..adfde4f 100644 --- a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockMapRenderPool.kt +++ b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockMapRenderPool.kt @@ -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( val changelog: BlockChangelog, @@ -51,7 +53,11 @@ class BlockMapRenderPool( delegate.onAllPlaybackComplete(this, trackers) for (future in renderJobFutures.values) { - future.get() + try { + future.get() + } catch (e: Exception) { + logger.error("Failed to render slice.", e) + } } } diff --git a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockStateMap.kt b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockStateMap.kt new file mode 100644 index 0000000..5fa37b7 --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockStateMap.kt @@ -0,0 +1,3 @@ +package cloud.kubelet.foundation.gjallarhorn.state + +class BlockStateMap : BlockCoordinateSparseMap()