mirror of
https://github.com/GayPizzaSpecifications/foundation.git
synced 2025-08-02 13:10:55 +00:00
Gjallarhorn: Timelapse Mode
This commit is contained in:
parent
cc6fbaae83
commit
bc2d3e28ae
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.")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user