mirror of
				https://github.com/GayPizzaSpecifications/foundation.git
				synced 2025-11-04 11:39:39 +00:00 
			
		
		
		
	Gjallarhorn: Timelapse Mode
This commit is contained in:
		
							
								
								
									
										16
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								README.md
									
									
									
									
									
								
							@ -1,17 +1,25 @@
 | 
				
			|||||||
# Foundation
 | 
					# Foundation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Foundation is a set of plugins that implement the core functionality for a small community Minecraft
 | 
					Foundation is a set of plugins that implement the core functionality for a small community Minecraft
 | 
				
			||||||
server.
 | 
					server.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Plugins
 | 
					## Plugins
 | 
				
			||||||
* foundation-core - Core functionality
 | 
					
 | 
				
			||||||
* foundation-bifrost - Discord chat bridge
 | 
					* foundation-core: Core functionality
 | 
				
			||||||
* foundation-heimdall - Event tracking
 | 
					* foundation-bifrost: Discord chat bridge
 | 
				
			||||||
 | 
					* foundation-heimdall: Event tracking
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Tools
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* tool-gjallarhorn - Heimdall swiss army knife
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Installation
 | 
					## Installation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The following command downloads and runs a script that will fetch the latest update manifest, and
 | 
					The following command downloads and runs a script that will fetch the latest update manifest, and
 | 
				
			||||||
install all plugins available. It can also be used to update plugins to the latest version
 | 
					install all plugins available. It can also be used to update plugins to the latest version
 | 
				
			||||||
available.
 | 
					available.
 | 
				
			||||||
```
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
# Always validate the contents of a script from the internet!
 | 
					# Always validate the contents of a script from the internet!
 | 
				
			||||||
bash -c "$(curl -sL https://git.gorence.io/lgorence/foundation/-/raw/main/install.sh)"
 | 
					bash -c "$(curl -sL https://git.gorence.io/lgorence/foundation/-/raw/main/install.sh)"
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
				
			|||||||
@ -10,6 +10,9 @@ plugins {
 | 
				
			|||||||
  id("com.github.johnrengelman.shadow") version "7.1.1" apply false
 | 
					  id("com.github.johnrengelman.shadow") version "7.1.1" apply false
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun Project.isFoundationPlugin() = name.startsWith("foundation-")
 | 
				
			||||||
 | 
					fun Project.isFoundationTool() = !isFoundationPlugin()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Disable the JAR task for the root project.
 | 
					// Disable the JAR task for the root project.
 | 
				
			||||||
tasks["jar"].enabled = false
 | 
					tasks["jar"].enabled = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -39,7 +42,7 @@ tasks.create("updateManifests") {
 | 
				
			|||||||
    writer.use {
 | 
					    writer.use {
 | 
				
			||||||
      val rootPath = rootProject.rootDir.toPath()
 | 
					      val rootPath = rootProject.rootDir.toPath()
 | 
				
			||||||
      val updateManifest = subprojects.mapNotNull { project ->
 | 
					      val updateManifest = subprojects.mapNotNull { project ->
 | 
				
			||||||
        if (project.name == "foundation-gjallarhorn") {
 | 
					        if (project.isFoundationTool()) {
 | 
				
			||||||
          return@mapNotNull null
 | 
					          return@mapNotNull null
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        val files = project.tasks.getByName("shadowJar").outputs
 | 
					        val files = project.tasks.getByName("shadowJar").outputs
 | 
				
			||||||
@ -122,7 +125,7 @@ subprojects {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (project.name != "foundation-gjallarhorn") {
 | 
					  if (project.isFoundationTool()) {
 | 
				
			||||||
    tasks.withType<ShadowJar> {
 | 
					    tasks.withType<ShadowJar> {
 | 
				
			||||||
      archiveClassifier.set("plugin")
 | 
					      archiveClassifier.set("plugin")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,17 +0,0 @@
 | 
				
			|||||||
package cloud.kubelet.foundation.gjallarhorn
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
data class BlockOffset(
 | 
					 | 
				
			||||||
  val x: Long,
 | 
					 | 
				
			||||||
  val y: Long,
 | 
					 | 
				
			||||||
  val z: Long
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  fun apply(position: BlockPosition) = position.copy(
 | 
					 | 
				
			||||||
    x = position.x + x,
 | 
					 | 
				
			||||||
    y = position.y + y,
 | 
					 | 
				
			||||||
    z = position.z + z
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  companion object {
 | 
					 | 
				
			||||||
    val none = BlockOffset(0, 0, 0)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,19 +0,0 @@
 | 
				
			|||||||
package cloud.kubelet.foundation.gjallarhorn
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,81 +0,0 @@
 | 
				
			|||||||
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
 | 
					 | 
				
			||||||
import com.github.ajalt.clikt.parameters.options.flag
 | 
					 | 
				
			||||||
import com.github.ajalt.clikt.parameters.options.option
 | 
					 | 
				
			||||||
import org.jetbrains.exposed.sql.Database
 | 
					 | 
				
			||||||
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.time.Instant
 | 
					 | 
				
			||||||
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 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(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]
 | 
					 | 
				
			||||||
        val x = row[BlockChangeView.x]
 | 
					 | 
				
			||||||
        val y = row[BlockChangeView.y]
 | 
					 | 
				
			||||||
        val z = row[BlockChangeView.z]
 | 
					 | 
				
			||||||
        val block = row[BlockChangeView.block]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        val location = BlockPosition(x.toLong(), y.toLong(), z.toLong())
 | 
					 | 
				
			||||||
        if (changeIsBreak) {
 | 
					 | 
				
			||||||
          tracker.delete(location)
 | 
					 | 
				
			||||||
        } 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()}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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 = 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) {
 | 
					 | 
				
			||||||
        println("${position.x},${position.y},${position.z},${block.type}")
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,7 +0,0 @@
 | 
				
			|||||||
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))
 | 
					 | 
				
			||||||
@ -1,7 +1,6 @@
 | 
				
			|||||||
package cloud.kubelet.foundation.heimdall.event
 | 
					package cloud.kubelet.foundation.heimdall.event
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import cloud.kubelet.foundation.heimdall.storageBlockName
 | 
					import cloud.kubelet.foundation.heimdall.storageBlockName
 | 
				
			||||||
import cloud.kubelet.foundation.heimdall.table.BlockBreakTable
 | 
					 | 
				
			||||||
import cloud.kubelet.foundation.heimdall.table.BlockPlaceTable
 | 
					import cloud.kubelet.foundation.heimdall.table.BlockPlaceTable
 | 
				
			||||||
import org.bukkit.Location
 | 
					import org.bukkit.Location
 | 
				
			||||||
import org.bukkit.Material
 | 
					import org.bukkit.Material
 | 
				
			||||||
@ -22,15 +21,15 @@ class BlockPlace(
 | 
				
			|||||||
  override fun store(transaction: Transaction) {
 | 
					  override fun store(transaction: Transaction) {
 | 
				
			||||||
    transaction.apply {
 | 
					    transaction.apply {
 | 
				
			||||||
      BlockPlaceTable.insert {
 | 
					      BlockPlaceTable.insert {
 | 
				
			||||||
        it[BlockBreakTable.time] = timestamp
 | 
					        it[time] = timestamp
 | 
				
			||||||
        it[BlockBreakTable.player] = playerUniqueIdentity
 | 
					        it[player] = playerUniqueIdentity
 | 
				
			||||||
        it[BlockBreakTable.world] = location.world.uid
 | 
					        it[world] = location.world.uid
 | 
				
			||||||
        it[BlockBreakTable.x] = location.x
 | 
					        it[x] = location.x
 | 
				
			||||||
        it[BlockBreakTable.y] = location.y
 | 
					        it[y] = location.y
 | 
				
			||||||
        it[BlockBreakTable.z] = location.z
 | 
					        it[z] = location.z
 | 
				
			||||||
        it[BlockBreakTable.pitch] = location.pitch.toDouble()
 | 
					        it[pitch] = location.pitch.toDouble()
 | 
				
			||||||
        it[BlockBreakTable.yaw] = location.yaw.toDouble()
 | 
					        it[yaw] = location.yaw.toDouble()
 | 
				
			||||||
        it[BlockBreakTable.block] = material.storageBlockName
 | 
					        it[block] = material.storageBlockName
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -4,5 +4,5 @@ include(
 | 
				
			|||||||
  ":foundation-core",
 | 
					  ":foundation-core",
 | 
				
			||||||
  ":foundation-bifrost",
 | 
					  ":foundation-bifrost",
 | 
				
			||||||
  ":foundation-heimdall",
 | 
					  ":foundation-heimdall",
 | 
				
			||||||
  ":foundation-gjallarhorn",
 | 
					  ":tool-gjallarhorn",
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
				
			|||||||
@ -12,7 +12,7 @@ class GjallarhornCommand : CliktCommand(invokeWithoutSubcommand = true) {
 | 
				
			|||||||
  private val jdbcConnectionUsername by option("-u", "--connection-username", help = "JDBC Connection Username")
 | 
					  private val jdbcConnectionUsername by option("-u", "--connection-username", help = "JDBC Connection Username")
 | 
				
			||||||
    .default("jdbc:postgresql://localhost/foundation")
 | 
					    .default("jdbc:postgresql://localhost/foundation")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private val jdbcConnectionPassword by option("-p", "--connection-password", help = "JDBC Connection Passowrd")
 | 
					  private val jdbcConnectionPassword by option("-p", "--connection-password", help = "JDBC Connection Password")
 | 
				
			||||||
    .default("jdbc:postgresql://localhost/foundation")
 | 
					    .default("jdbc:postgresql://localhost/foundation")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  override fun run() {
 | 
					  override fun run() {
 | 
				
			||||||
@ -0,0 +1,130 @@
 | 
				
			|||||||
 | 
					package cloud.kubelet.foundation.gjallarhorn.commands
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.gjallarhorn.compose
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.gjallarhorn.render.*
 | 
				
			||||||
 | 
					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.flag
 | 
				
			||||||
 | 
					import com.github.ajalt.clikt.parameters.options.option
 | 
				
			||||||
 | 
					import com.github.ajalt.clikt.parameters.options.required
 | 
				
			||||||
 | 
					import com.github.ajalt.clikt.parameters.types.enum
 | 
				
			||||||
 | 
					import jetbrains.exodus.kotlin.notNull
 | 
				
			||||||
 | 
					import org.jetbrains.exposed.sql.*
 | 
				
			||||||
 | 
					import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq
 | 
				
			||||||
 | 
					import org.jetbrains.exposed.sql.transactions.transaction
 | 
				
			||||||
 | 
					import org.slf4j.LoggerFactory
 | 
				
			||||||
 | 
					import java.awt.image.BufferedImage
 | 
				
			||||||
 | 
					import java.time.Duration
 | 
				
			||||||
 | 
					import java.time.Instant
 | 
				
			||||||
 | 
					import java.util.concurrent.atomic.AtomicLong
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BlockLogReplay : CliktCommand("Replay Block Logs", name = "replay-block-log") {
 | 
				
			||||||
 | 
					  private val db by requireObject<Database>()
 | 
				
			||||||
 | 
					  private val exactTimeAsString by option("--time", help = "Replay Time")
 | 
				
			||||||
 | 
					  private val timeLapseMode by option("--timelapse", help = "Timelapse Mode").enum<TimeLapseMode> { it.id }
 | 
				
			||||||
 | 
					  private val render by option("--render", help = "Render Top Down Image").enum<RenderType> { it.id }.required()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private val considerAirBlocks by option("--consider-air-blocks", help = "Enable Air Block Consideration").flag()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private val logger = LoggerFactory.getLogger(BlockLogReplay::class.java)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override fun run() {
 | 
				
			||||||
 | 
					    if (timeLapseMode != null) {
 | 
				
			||||||
 | 
					      val (start, end) = transaction(db) {
 | 
				
			||||||
 | 
					        val minTimeColumn = BlockChangeView.time.min().notNull
 | 
				
			||||||
 | 
					        val maxTimeColumn = BlockChangeView.time.max().notNull
 | 
				
			||||||
 | 
					        val row = BlockChangeView.slice(minTimeColumn, maxTimeColumn).selectAll().single()
 | 
				
			||||||
 | 
					        row[minTimeColumn]!! to row[maxTimeColumn]!!
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      val intervals = mutableListOf<Instant>()
 | 
				
			||||||
 | 
					      var current = start
 | 
				
			||||||
 | 
					      while (!current.isAfter(end)) {
 | 
				
			||||||
 | 
					        intervals.add(current)
 | 
				
			||||||
 | 
					        current = current.plus(timeLapseMode!!.interval)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      val trackers = mutableMapOf<Int, BlockStateTracker>()
 | 
				
			||||||
 | 
					      for (time in intervals) {
 | 
				
			||||||
 | 
					        val index = intervals.indexOf(time) + 1
 | 
				
			||||||
 | 
					        val tracker = buildTrackerState(time, "Timelapse-${index}")
 | 
				
			||||||
 | 
					        if (tracker.isEmpty()) {
 | 
				
			||||||
 | 
					          continue
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        trackers[index] = tracker
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      val allBlockOffsets = trackers.map { it.value.calculateZeroBlockOffset() }
 | 
				
			||||||
 | 
					      val globalBlockOffset = BlockPosition.maxOf(allBlockOffsets.asSequence())
 | 
				
			||||||
 | 
					      val allBlockMaxes = trackers.map { it.value.calculateZeroBlockOffset() }
 | 
				
			||||||
 | 
					      val globalBlockMax = BlockPosition.maxOf(allBlockMaxes.asSequence())
 | 
				
			||||||
 | 
					      val globalBlockExpanse = BlockExpanse.offsetAndMax(globalBlockOffset, globalBlockMax)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      for ((i, tracker) in trackers.entries) {
 | 
				
			||||||
 | 
					        saveRenderImage(tracker, globalBlockExpanse, "-${i}")
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      val time = if (exactTimeAsString != null) Instant.parse(exactTimeAsString) else null
 | 
				
			||||||
 | 
					      val tracker = buildTrackerState(time, "Single-Time")
 | 
				
			||||||
 | 
					      val expanse = BlockExpanse.offsetAndMax(tracker.calculateZeroBlockOffset(), tracker.calculateMaxBlock())
 | 
				
			||||||
 | 
					      saveRenderImage(tracker, expanse)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  fun saveRenderImage(tracker: BlockStateTracker, expanse: BlockExpanse, suffix: String = "") {
 | 
				
			||||||
 | 
					    val state = BlockStateImage()
 | 
				
			||||||
 | 
					    tracker.populateStateImage(state, expanse.offset)
 | 
				
			||||||
 | 
					    val image = render.renderBufferedImage(state, expanse)
 | 
				
			||||||
 | 
					    image.savePngFile("${render.id}${suffix}.png")
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  fun buildTrackerState(time: Instant?, job: String): BlockStateTracker {
 | 
				
			||||||
 | 
					    val filter = compose(
 | 
				
			||||||
 | 
					      combine = { a, b -> a and b },
 | 
				
			||||||
 | 
					      { time != null } to { BlockChangeView.time lessEq time!! }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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]
 | 
				
			||||||
 | 
					        val x = row[BlockChangeView.x]
 | 
				
			||||||
 | 
					        val y = row[BlockChangeView.y]
 | 
				
			||||||
 | 
					        val z = row[BlockChangeView.z]
 | 
				
			||||||
 | 
					        val block = row[BlockChangeView.block]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val location = BlockPosition(x.toLong(), y.toLong(), z.toLong())
 | 
				
			||||||
 | 
					        if (changeIsBreak) {
 | 
				
			||||||
 | 
					          tracker.delete(location)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          tracker.place(location, BlockState(block))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val count = blockChangeCounter.addAndGet(1)
 | 
				
			||||||
 | 
					        if (count % 1000L == 0L) {
 | 
				
			||||||
 | 
					          logger.info("Job $job Calculating Block Changes... $count")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    logger.info("Job $job Total Block Changes... ${blockChangeCounter.get()}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    val uniqueBlockPositions = tracker.blocks.size
 | 
				
			||||||
 | 
					    logger.info("Job $job Unique Block Positions... $uniqueBlockPositions")
 | 
				
			||||||
 | 
					    return tracker
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  enum class RenderType(val id: String, val renderBufferedImage: (BlockStateImage, BlockExpanse) -> BufferedImage) {
 | 
				
			||||||
 | 
					    TopDown("top-down", { image, expanse -> image.buildTopDownImage(expanse) }),
 | 
				
			||||||
 | 
					    HeightMap("height-map", { image, expanse -> image.buildHeightMapImage(expanse) })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  enum class TimeLapseMode(val id: String, val interval: Duration) {
 | 
				
			||||||
 | 
					    ByHour("hours", Duration.ofHours(1)),
 | 
				
			||||||
 | 
					    ByDay("days", Duration.ofDays(1))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
package cloud.kubelet.foundation.gjallarhorn
 | 
					package cloud.kubelet.foundation.gjallarhorn.commands
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.gjallarhorn.compose
 | 
				
			||||||
import cloud.kubelet.foundation.heimdall.table.PlayerSessionTable
 | 
					import cloud.kubelet.foundation.heimdall.table.PlayerSessionTable
 | 
				
			||||||
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
 | 
				
			||||||
@ -2,6 +2,7 @@ package cloud.kubelet.foundation.gjallarhorn
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import cloud.kubelet.foundation.gjallarhorn.commands.BlockLogReplay
 | 
					import cloud.kubelet.foundation.gjallarhorn.commands.BlockLogReplay
 | 
				
			||||||
import cloud.kubelet.foundation.gjallarhorn.commands.PlayerPositionExport
 | 
					import cloud.kubelet.foundation.gjallarhorn.commands.PlayerPositionExport
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.gjallarhorn.commands.PlayerSessionExport
 | 
				
			||||||
import com.github.ajalt.clikt.core.subcommands
 | 
					import com.github.ajalt.clikt.core.subcommands
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun main(args: Array<String>) = GjallarhornCommand().subcommands(
 | 
					fun main(args: Array<String>) = GjallarhornCommand().subcommands(
 | 
				
			||||||
@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					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,37 @@
 | 
				
			|||||||
 | 
					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,4 +1,4 @@
 | 
				
			|||||||
package cloud.kubelet.foundation.gjallarhorn
 | 
					package cloud.kubelet.foundation.gjallarhorn.render
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import kotlinx.serialization.Serializable
 | 
					import kotlinx.serialization.Serializable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
package cloud.kubelet.foundation.gjallarhorn
 | 
					package cloud.kubelet.foundation.gjallarhorn.render
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import cloud.kubelet.foundation.gjallarhorn.util.ColorGradient
 | 
					import cloud.kubelet.foundation.gjallarhorn.util.ColorGradient
 | 
				
			||||||
import cloud.kubelet.foundation.gjallarhorn.util.FloatClamp
 | 
					import cloud.kubelet.foundation.gjallarhorn.util.FloatClamp
 | 
				
			||||||
@ -18,9 +18,9 @@ class BlockStateImage {
 | 
				
			|||||||
    }[position.y] = state
 | 
					    }[position.y] = state
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fun buildTopDownImage(): BufferedImage {
 | 
					  fun buildTopDownImage(expanse: BlockExpanse): BufferedImage {
 | 
				
			||||||
    val colorKey = RandomColorKey()
 | 
					    val colorKey = RandomColorKey()
 | 
				
			||||||
    return buildPixelQuadImage { x, z ->
 | 
					    return buildPixelQuadImage(expanse) { x, z ->
 | 
				
			||||||
      val maybeYBlocks = blocks[x]?.get(z)
 | 
					      val maybeYBlocks = blocks[x]?.get(z)
 | 
				
			||||||
      if (maybeYBlocks == null) {
 | 
					      if (maybeYBlocks == null) {
 | 
				
			||||||
        setPixelQuad(x, z, Color.white.rgb)
 | 
					        setPixelQuad(x, z, Color.white.rgb)
 | 
				
			||||||
@ -37,16 +37,16 @@ class BlockStateImage {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fun buildHeightMapImage(): BufferedImage {
 | 
					  fun buildHeightMapImage(expanse: BlockExpanse): BufferedImage {
 | 
				
			||||||
    val yMin = blocks.minOf { xSection -> xSection.value.minOf { zSection -> zSection.value.minOf { it.key } } }
 | 
					    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 yMax = blocks.maxOf { xSection -> xSection.value.maxOf { zSection -> zSection.value.maxOf { it.key } } }
 | 
				
			||||||
    val clamp = FloatClamp(yMin, yMax)
 | 
					    val clamp = FloatClamp(yMin, yMax)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return buildHeatMapImage(clamp) { x, z -> blocks[x]?.get(z)?.maxOf { it.key } }
 | 
					    return buildHeatMapImage(expanse, clamp) { x, z -> blocks[x]?.get(z)?.maxOf { it.key } }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fun buildHeatMapImage(clamp: FloatClamp, calculate: (Long, Long) -> Long?): BufferedImage =
 | 
					  fun buildHeatMapImage(expanse: BlockExpanse, clamp: FloatClamp, calculate: (Long, Long) -> Long?): BufferedImage =
 | 
				
			||||||
    buildPixelQuadImage { x, z ->
 | 
					    buildPixelQuadImage(expanse) { x, z ->
 | 
				
			||||||
      val value = calculate(x, z)
 | 
					      val value = calculate(x, z)
 | 
				
			||||||
      val color = if (value != null) {
 | 
					      val color = if (value != null) {
 | 
				
			||||||
        val floatValue = clamp.convert(value)
 | 
					        val floatValue = clamp.convert(value)
 | 
				
			||||||
@ -65,13 +65,13 @@ class BlockStateImage {
 | 
				
			|||||||
    setRGB((x.toInt() * 2) + 1, (z.toInt() * 2) + 1, rgb)
 | 
					    setRGB((x.toInt() * 2) + 1, (z.toInt() * 2) + 1, rgb)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private fun buildPixelQuadImage(callback: BufferedImage.(Long, Long) -> Unit): BufferedImage {
 | 
					  private fun buildPixelQuadImage(expanse: BlockExpanse, callback: BufferedImage.(Long, Long) -> Unit): BufferedImage {
 | 
				
			||||||
    val xMax = blocks.keys.maxOf { it }
 | 
					    val width = expanse.size.x
 | 
				
			||||||
    val zMax = blocks.maxOf { xSection -> xSection.value.maxOf { zSection -> zSection.key } }
 | 
					    val height = expanse.size.z
 | 
				
			||||||
    val bufferedImage = BufferedImage(xMax.toInt() * 2, zMax.toInt() * 2, BufferedImage.TYPE_4BYTE_ABGR)
 | 
					    val bufferedImage = BufferedImage(width.toInt() * 2, height.toInt() * 2, BufferedImage.TYPE_4BYTE_ABGR)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (x in 0 until xMax) {
 | 
					    for (x in 0 until width) {
 | 
				
			||||||
      for (z in 0 until zMax) {
 | 
					      for (z in 0 until height) {
 | 
				
			||||||
        callback(bufferedImage, x, z)
 | 
					        callback(bufferedImage, x, z)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
package cloud.kubelet.foundation.gjallarhorn
 | 
					package cloud.kubelet.foundation.gjallarhorn.render
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import kotlin.math.absoluteValue
 | 
					import kotlin.math.absoluteValue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -17,7 +17,7 @@ class BlockStateTracker(private val mode: BlockTrackMode = BlockTrackMode.Remove
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fun calculateZeroBlockOffset(): BlockOffset {
 | 
					  fun calculateZeroBlockOffset(): BlockPosition {
 | 
				
			||||||
    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 }
 | 
				
			||||||
@ -26,12 +26,21 @@ 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 BlockOffset(xOffset, yOffset, zOffset)
 | 
					    return BlockPosition(xOffset, yOffset, zOffset)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fun populate(image: BlockStateImage, offset: BlockOffset = BlockOffset.none) {
 | 
					  fun calculateMaxBlock(): BlockPosition {
 | 
				
			||||||
 | 
					    val x = blocks.keys.maxOf { it.x }
 | 
				
			||||||
 | 
					    val y = blocks.keys.maxOf { it.y }
 | 
				
			||||||
 | 
					    val z = blocks.keys.maxOf { it.z }
 | 
				
			||||||
 | 
					    return BlockPosition(x, y, z)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  fun isEmpty() = blocks.isEmpty()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  fun populateStateImage(image: BlockStateImage, offset: BlockPosition = BlockPosition.zero) {
 | 
				
			||||||
    blocks.forEach { (position, state) ->
 | 
					    blocks.forEach { (position, state) ->
 | 
				
			||||||
      val realPosition = offset.apply(position)
 | 
					      val realPosition = offset.applyAsOffset(position)
 | 
				
			||||||
      image.put(realPosition, state)
 | 
					      image.put(realPosition, state)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
package cloud.kubelet.foundation.gjallarhorn
 | 
					package cloud.kubelet.foundation.gjallarhorn.render
 | 
				
			||||||
 | 
					
 | 
				
			||||||
enum class BlockTrackMode {
 | 
					enum class BlockTrackMode {
 | 
				
			||||||
  RemoveOnDelete,
 | 
					  RemoveOnDelete,
 | 
				
			||||||
@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					package cloud.kubelet.foundation.gjallarhorn.util
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.awt.image.BufferedImage
 | 
				
			||||||
 | 
					import java.io.File
 | 
				
			||||||
 | 
					import javax.imageio.ImageIO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun BufferedImage.savePngFile(path: String) {
 | 
				
			||||||
 | 
					  if (!ImageIO.write(this, "png", File(path))) {
 | 
				
			||||||
 | 
					    throw RuntimeException("Unable to write PNG.")
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user