Remove heimdall and tool project.

This commit is contained in:
Liv Gorence
2023-01-26 20:36:48 -08:00
parent cf2a812b75
commit cec3b1297a
84 changed files with 3 additions and 2560 deletions

View File

@ -1,10 +0,0 @@
dependencies {
implementation(project(":foundation-core"))
implementation(project(":foundation-heimdall"))
implementation("org.slf4j:slf4j-simple:1.7.32")
implementation("com.github.ajalt.clikt:clikt:3.3.0")
}
listOf(tasks.jar, tasks.shadowJar).map { it.get() }.forEach { task ->
task.manifest.attributes["Main-Class"] = "gay.pizza.foundation.gjallarhorn.MainKt"
}

View File

@ -1,37 +0,0 @@
package gay.pizza.foundation.gjallarhorn
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.int
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import org.jetbrains.exposed.sql.Database
import java.time.Duration
class GjallarhornCommand : CliktCommand(invokeWithoutSubcommand = true) {
private val jdbcConnectionUrl by option("-c", "--connection-url", help = "JDBC Connection URL")
.default("jdbc:postgresql://localhost/foundation")
private val jdbcConnectionUsername by option("-u", "--connection-username", help = "JDBC Connection Username")
.default("jdbc:postgresql://localhost/foundation")
private val jdbcConnectionPassword by option("-p", "--connection-password", help = "JDBC Connection Password")
.default("jdbc:postgresql://localhost/foundation")
private val dbPoolSize by option("--db-pool-size", help = "JDBC Pool Size").int().default(8)
override fun run() {
val pool = HikariDataSource(HikariConfig().apply {
jdbcUrl = jdbcConnectionUrl
username = jdbcConnectionUsername
password = jdbcConnectionPassword
minimumIdle = dbPoolSize / 2
maximumPoolSize = dbPoolSize
idleTimeout = Duration.ofMinutes(5).toMillis()
maxLifetime = Duration.ofMinutes(10).toMillis()
})
val db = Database.connect(pool)
currentContext.findOrSetObject { db }
}
}

View File

@ -1,153 +0,0 @@
package gay.pizza.foundation.gjallarhorn.commands
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
import com.github.ajalt.clikt.parameters.types.enum
import com.github.ajalt.clikt.parameters.types.int
import gay.pizza.foundation.gjallarhorn.render.*
import gay.pizza.foundation.gjallarhorn.state.*
import gay.pizza.foundation.gjallarhorn.util.compose
import gay.pizza.foundation.gjallarhorn.util.savePngFile
import gay.pizza.foundation.heimdall.view.BlockChangeView
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greaterEq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq
import org.jetbrains.exposed.sql.and
import org.slf4j.LoggerFactory
import java.awt.Color
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") {
private val db by requireObject<Database>()
private val timelapseIntervalLimit by option("--timelapse-limit", help = "Timelapse Limit Intervals").int()
private val timelapseMode by option("--timelapse", help = "Timelapse Mode").enum<TimelapseMode> { it.id }.required()
private val timelapseSpeedChangeThreshold by option(
"--timelapse-change-speed-threshold",
help = "Timelapse Change Speed Threshold"
).int()
private val timelapseSpeedChangeMinimumIntervalSeconds by option(
"--timelapse-change-speed-minimum-interval-seconds",
help = "Timelapse Change Speed Minimum Interval Seconds"
).int()
private val render by option("--render", help = "Render Top Down Image").enum<ImageRenderType> { it.id }.required()
private val considerAirBlocks by option("--consider-air-blocks", help = "Enable Air Block Consideration").flag()
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() {
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?.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 }
)
val changelog = BlockChangelog.query(db, filter)
logger.info("Block Changelog: ${changelog.changes.size} changes")
val timelapse = BlockMapTimelapse<BufferedImage>()
var slices = changelog.calculateChangelogSlices(timelapseMode.interval, timelapseIntervalLimit)
if (timelapseSpeedChangeThreshold != null && timelapseSpeedChangeMinimumIntervalSeconds != null) {
val minimumInterval = Duration.ofSeconds(timelapseSpeedChangeMinimumIntervalSeconds!!.toLong())
val blockChangeThreshold = timelapseSpeedChangeThreshold!!
slices = changelog.splitChangelogSlicesWithThreshold(blockChangeThreshold, minimumInterval, slices)
}
logger.info("Timelapse Slices: ${slices.size} slices")
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,
delegate = timelapse,
createRendererFunction = { expanse -> render.createNewRenderer(expanse, db) },
threadPoolExecutor = threadPoolExecutor
) { slice, result ->
val speed = slice.sliceRelativeDuration.toSeconds().toDouble() / timelapseMode.interval.toSeconds().toDouble()
val graphics = result.createGraphics()
val font = Font.decode("Arial Black").deriveFont(24.0f)
graphics.color = Color.black
graphics.font = font
val context = graphics.fontRenderContext
val text = String.format("%s @ %.4f speed (1 frame = %s sec)", slice.sliceEndTime, speed, slice.sliceRelativeDuration.toSeconds())
val layout =
TextLayout(text, font, context)
layout.draw(graphics, 60f, 60f)
graphics.dispose()
val index = slices.indexOf(slice) + 1
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)
}
private fun maybeBuildTrim(): Pair<BlockCoordinate, BlockCoordinate>? {
if (fromCoordinate == null || toCoordinate == null) {
return null
}
val from = fromCoordinate!!.split(",").map { it.toLong() }
val to = toCoordinate!!.split(",").map { it.toLong() }
val fromBlock = BlockCoordinate(from[0], 0, from[1])
val toBlock = BlockCoordinate(to[0], 0, to[1])
return fromBlock to toBlock
}
@Suppress("unused")
enum class TimelapseMode(val id: String, val interval: Duration) {
ByHour("hours", Duration.ofHours(1)),
ByDay("days", Duration.ofDays(1)),
ByFifteenMinutes("fifteen-minutes", Duration.ofMinutes(15))
}
}

View File

@ -1,58 +0,0 @@
package gay.pizza.foundation.gjallarhorn.commands
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.requireObject
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.enum
import com.github.ajalt.clikt.parameters.types.int
import com.github.ajalt.clikt.parameters.types.path
import gay.pizza.foundation.gjallarhorn.export.ChunkExportLoader
import gay.pizza.foundation.gjallarhorn.export.CombinedChunkFormat
import gay.pizza.foundation.gjallarhorn.state.BlockExpanse
import gay.pizza.foundation.gjallarhorn.state.BlockLogTracker
import gay.pizza.foundation.gjallarhorn.state.ChangelogSlice
import gay.pizza.foundation.gjallarhorn.util.savePngFile
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream
import org.jetbrains.exposed.sql.Database
class ChunkExportLoaderCommand : CliktCommand("Chunk Export Loader", name = "chunk-export-loader") {
private val db by requireObject<Database>()
private val exportDirectoryPath by argument("export-directory-path").path()
private val world by argument("world")
private val chunkLoadLimit by option("--chunk-limit", help = "Chunk Limit").int()
private val render by option("--render", help = "Render Top Down Image").enum<ImageRenderType> { it.id }
private val loadCombinedFormat by option("--load-combined-format").flag()
private val saveCombinedFormat by option("--save-combined-format").flag()
override fun run() {
val combinedFormatFile = exportDirectoryPath.resolve("combined.json").toFile()
val format = if (loadCombinedFormat) {
Json.decodeFromStream(CombinedChunkFormat.serializer(), combinedFormatFile.inputStream())
} else {
val tracker = BlockLogTracker(isConcurrent = true)
val loader = ChunkExportLoader(tracker = tracker)
loader.loadAllChunksForWorld(exportDirectoryPath, world, fast = true, limit = chunkLoadLimit)
val expanse = BlockExpanse.zeroOffsetAndMax(tracker.calculateZeroBlockOffset(), tracker.calculateMaxBlock())
val map = tracker.buildBlockMap(expanse.offset)
CombinedChunkFormat(expanse, map)
}
if (render != null) {
val renderer = render!!.createNewRenderer(format.expanse, db)
val image = renderer.render(ChangelogSlice.none, format.map)
image.savePngFile("full.png")
}
if (saveCombinedFormat) {
if (combinedFormatFile.exists()) {
combinedFormatFile.delete()
}
Json.encodeToStream(CombinedChunkFormat.serializer(), format, combinedFormatFile.outputStream())
}
}
}

View File

@ -1,16 +0,0 @@
package gay.pizza.foundation.gjallarhorn.commands
import gay.pizza.foundation.gjallarhorn.render.*
import gay.pizza.foundation.gjallarhorn.state.BlockExpanse
import org.jetbrains.exposed.sql.Database
@Suppress("unused")
enum class ImageRenderType(
val id: String,
val createNewRenderer: (BlockExpanse, Database) -> BlockImageRenderer
) {
BlockDiversity("block-diversity", { expanse, _ -> BlockDiversityRenderer(expanse) }),
HeightMap("height-map", { expanse, _ -> BlockHeightMapRenderer(expanse) }),
PlayerPosition("player-position", { expanse, db -> PlayerLocationShareRenderer(expanse, db) }),
GraphicalSession("graphical", { expanse, _ -> LaunchGraphicalRenderSession(expanse) })
}

View File

@ -1,42 +0,0 @@
package gay.pizza.foundation.gjallarhorn.commands
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.requireObject
import com.github.ajalt.clikt.parameters.options.option
import gay.pizza.foundation.gjallarhorn.state.PlayerPositionChangelog
import gay.pizza.foundation.gjallarhorn.util.compose
import gay.pizza.foundation.heimdall.table.PlayerPositionTable
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greaterEq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.transactions.transaction
import java.time.Instant
import java.util.UUID
class PlayerPositionExport : CliktCommand(name = "export-player-positions", help = "Export Player Positions") {
private val db by requireObject<Database>()
private val playerIdString by option("--player", help = "Player ID")
private val startTimeString by option("--start-time", help = "Start Time")
private val endTimeString by option("--end-time", help = "End Time")
override fun run() {
val filter = compose(
combine = { a, b -> a and b },
{ startTimeString != null } to { PlayerPositionTable.time greaterEq Instant.parse(startTimeString) },
{ endTimeString != null } to { PlayerPositionTable.time lessEq Instant.parse(endTimeString) },
{ playerIdString != null } to { PlayerPositionTable.player eq UUID.fromString(playerIdString) }
)
println("time,player,world,x,y,z,pitch,yaw")
transaction(db) {
PlayerPositionChangelog.query(db, filter).changes.forEach { change ->
change.apply {
println("${time},${player},${world},${x},${y},${z},${pitch},${yaw}")
}
}
}
}
}

View File

@ -1,41 +0,0 @@
package gay.pizza.foundation.gjallarhorn.commands
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.requireObject
import com.github.ajalt.clikt.parameters.options.option
import gay.pizza.foundation.gjallarhorn.util.compose
import gay.pizza.foundation.heimdall.table.PlayerSessionTable
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import java.util.UUID
class PlayerSessionExport : CliktCommand(name = "export-player-sessions", help = "Export Player Sessions") {
private val db by requireObject<Database>()
private val playerIdString by option("--player-id", help = "Player ID")
private val playerNameString by option("--player-name", help = "Player Name")
override fun run() {
val filter = compose(
combine = { a, b -> a and b },
{ playerIdString != null } to { PlayerSessionTable.player eq UUID.fromString(playerIdString) },
{ playerNameString != null } to { PlayerSessionTable.name eq playerNameString!! }
)
println("id,player,name,start,end")
transaction(db) {
PlayerSessionTable.select(filter).orderBy(PlayerSessionTable.endTime).forEach { row ->
val id = row[PlayerSessionTable.id]
val player = row[PlayerSessionTable.player]
val name = row[PlayerSessionTable.name]
val start = row[PlayerSessionTable.startTime]
val end = row[PlayerSessionTable.endTime]
println("${id},${player},${name},${start},${end}")
}
}
}
}

View File

@ -1,68 +0,0 @@
package gay.pizza.foundation.gjallarhorn.export
import gay.pizza.foundation.gjallarhorn.state.BlockCoordinate
import gay.pizza.foundation.gjallarhorn.state.BlockLogTracker
import gay.pizza.foundation.gjallarhorn.state.BlockState
import gay.pizza.foundation.gjallarhorn.state.SparseBlockStateMap
import gay.pizza.foundation.heimdall.export.ExportedChunk
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import org.slf4j.LoggerFactory
import java.nio.file.Path
import java.util.zip.GZIPInputStream
import kotlin.io.path.inputStream
import kotlin.io.path.listDirectoryEntries
class ChunkExportLoader(
val map: SparseBlockStateMap? = null,
val tracker: BlockLogTracker? = null) {
fun loadAllChunksForWorld(path: Path, world: String, fast: Boolean = false, limit: Int? = null) {
var chunkFiles = path.listDirectoryEntries("${world}_chunk_*.json.gz")
if (limit != null) {
chunkFiles = chunkFiles.take(limit)
}
if (fast) {
chunkFiles.withIndex().toList().parallelStream().forEach { loadChunkFile(it.value, id = it.index) }
} else {
for (filePath in chunkFiles) {
loadChunkFile(filePath, id = chunkFiles.indexOf(filePath))
}
}
}
fun loadChunkFile(path: Path, id: Int = 0) {
val fileInputStream = path.inputStream()
val gzipInputStream = GZIPInputStream(fileInputStream)
val chunk = Json.decodeFromStream(ExportedChunk.serializer(), gzipInputStream)
var blockCount = 0L
val allBlocks = if (tracker != null) mutableMapOf<BlockCoordinate, BlockState>() else null
for (section in chunk.sections) {
val x = (chunk.x * 16) + section.x
val z = (chunk.z * 16) + section.z
for ((y, block) in section.blocks.withIndex()) {
if (block.type == "minecraft:air") {
continue
}
val coordinate = BlockCoordinate(x.toLong(), y.toLong(), z.toLong())
val state = BlockState.cached(block.type)
map?.put(coordinate, state)
if (allBlocks != null) {
allBlocks[coordinate] = state
}
blockCount++
}
}
if (allBlocks != null) {
tracker?.placeAll(allBlocks)
}
logger.info("($id) Chunk X=${chunk.x} Z=${chunk.z} had $blockCount blocks")
}
companion object {
private val logger = LoggerFactory.getLogger(ChunkExportLoader::class.java)
}
}

View File

@ -1,11 +0,0 @@
package gay.pizza.foundation.gjallarhorn.export
import gay.pizza.foundation.gjallarhorn.state.BlockExpanse
import gay.pizza.foundation.gjallarhorn.state.SparseBlockStateMap
import kotlinx.serialization.Serializable
@Serializable
class CombinedChunkFormat(
val expanse: BlockExpanse,
val map: SparseBlockStateMap
)

View File

@ -1,14 +0,0 @@
package gay.pizza.foundation.gjallarhorn
import com.github.ajalt.clikt.core.subcommands
import gay.pizza.foundation.gjallarhorn.commands.BlockChangeTimelapseCommand
import gay.pizza.foundation.gjallarhorn.commands.ChunkExportLoaderCommand
import gay.pizza.foundation.gjallarhorn.commands.PlayerPositionExport
import gay.pizza.foundation.gjallarhorn.commands.PlayerSessionExport
fun main(args: Array<String>) = GjallarhornCommand().subcommands(
BlockChangeTimelapseCommand(),
PlayerSessionExport(),
PlayerPositionExport(),
ChunkExportLoaderCommand()
).main(args)

View File

@ -1,30 +0,0 @@
package gay.pizza.foundation.gjallarhorn.render
import gay.pizza.foundation.gjallarhorn.state.BlockExpanse
import gay.pizza.foundation.gjallarhorn.state.BlockStateMap
import gay.pizza.foundation.gjallarhorn.state.ChangelogSlice
import gay.pizza.foundation.gjallarhorn.util.BlockColorKey
import gay.pizza.foundation.gjallarhorn.util.defaultBlockColorMap
import java.awt.Color
import java.awt.image.BufferedImage
class BlockDiversityRenderer(val expanse: BlockExpanse, quadPixelSize: Int = defaultQuadPixelSize) :
BlockGridRenderer(quadPixelSize) {
private val blockColorKey = BlockColorKey(defaultBlockColorMap)
override fun render(slice: ChangelogSlice, map: BlockStateMap): BufferedImage = buildPixelQuadImage(expanse) { graphics, x, z ->
val maybeYBlocks = map.getVerticalSection(x, z)
if (maybeYBlocks == null) {
setPixelQuad(graphics, x, z, Color.white)
return@buildPixelQuadImage
}
val maxBlockState = maybeYBlocks.maxByOrNull { it.key }?.value
if (maxBlockState == null) {
setPixelQuad(graphics, x, z, Color.white)
return@buildPixelQuadImage
}
val color = blockColorKey.map(maxBlockState.type)
setPixelQuad(graphics, x, z, color)
}
}

View File

@ -1,47 +0,0 @@
package gay.pizza.foundation.gjallarhorn.render
import gay.pizza.foundation.gjallarhorn.state.BlockExpanse
import java.awt.Color
import java.awt.Graphics2D
import java.awt.Rectangle
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)
}
protected fun drawSquare(graphics: Graphics2D, x: Long, y: Long, side: Long, color: Color) {
graphics.color = color
graphics.fill(Rectangle(x.toInt(), y.toInt(), side.toInt(), side.toInt()))
}
protected fun buildPixelQuadImage(
expanse: BlockExpanse,
callback: BufferedImage.(Graphics2D, Long, Long) -> Unit
): BufferedImage {
val widthInBlocks = expanse.size.x
val heightInBlocks = expanse.size.z
val widthInPixels = widthInBlocks.toInt() * quadPixelSize
val heightInPixels = heightInBlocks.toInt() * quadPixelSize
val bufferedImage =
BufferedImage(widthInPixels, heightInPixels, BufferedImage.TYPE_3BYTE_BGR)
val graphics = bufferedImage.createGraphics()
for (x in 0 until widthInBlocks) {
for (z in 0 until heightInBlocks) {
callback(bufferedImage, graphics, x, z)
}
}
graphics.dispose()
return bufferedImage
}
companion object {
const val defaultQuadPixelSize = 4
var globalQuadPixelNoop = false
}
}

View File

@ -1,26 +0,0 @@
package gay.pizza.foundation.gjallarhorn.render
import gay.pizza.foundation.gjallarhorn.state.BlockExpanse
import gay.pizza.foundation.gjallarhorn.util.ColorGradient
import gay.pizza.foundation.gjallarhorn.util.FloatClamp
import java.awt.Color
import java.awt.image.BufferedImage
abstract class BlockHeatMapRenderer(quadPixelSize: Int = defaultQuadPixelSize) : BlockGridRenderer(quadPixelSize) {
protected fun buildHeatMapImage(
expanse: BlockExpanse,
clamp: FloatClamp,
calculate: (Long, Long) -> Long?
): BufferedImage =
buildPixelQuadImage(expanse) { graphics, x, z ->
val value = calculate(x, z)
val color = if (value != null) {
val floatValue = clamp.convert(value)
ColorGradient.HeatMap.getColorAtValue(floatValue)
} else {
Color.white
}
setPixelQuad(graphics, x, z, color)
}
}

View File

@ -1,20 +0,0 @@
package gay.pizza.foundation.gjallarhorn.render
import gay.pizza.foundation.gjallarhorn.state.BlockExpanse
import gay.pizza.foundation.gjallarhorn.state.BlockStateMap
import gay.pizza.foundation.gjallarhorn.state.ChangelogSlice
import gay.pizza.foundation.gjallarhorn.state.SparseBlockStateMap
import gay.pizza.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: BlockStateMap): BufferedImage {
val blockMap = map as SparseBlockStateMap
val yMin = blockMap.blocks.minOf { xSection -> xSection.value.minOf { zSection -> zSection.value.minOf { it.key } } }
val yMax = blockMap.blocks.maxOf { xSection -> xSection.value.maxOf { zSection -> zSection.value.maxOf { it.key } } }
val clamp = FloatClamp(yMin, yMax)
return buildHeatMapImage(expanse, clamp) { x, z -> blockMap.blocks[x]?.get(z)?.maxOf { it.key } }
}
}

View File

@ -1,5 +0,0 @@
package gay.pizza.foundation.gjallarhorn.render
import java.awt.image.BufferedImage
interface BlockImageRenderer : BlockMapRenderer<BufferedImage>

View File

@ -1,8 +0,0 @@
package gay.pizza.foundation.gjallarhorn.render
import gay.pizza.foundation.gjallarhorn.state.BlockStateMap
import gay.pizza.foundation.gjallarhorn.state.ChangelogSlice
interface BlockMapRenderer<T> {
fun render(slice: ChangelogSlice, map: BlockStateMap): T
}

View File

@ -1,20 +0,0 @@
package gay.pizza.foundation.gjallarhorn.render
import gay.pizza.foundation.gjallarhorn.state.BlockExpanse
import gay.pizza.foundation.gjallarhorn.state.BlockStateMap
import gay.pizza.foundation.gjallarhorn.state.ChangelogSlice
import gay.pizza.foundation.gjallarhorn.state.SparseBlockStateMap
import gay.pizza.foundation.gjallarhorn.util.FloatClamp
import java.awt.image.BufferedImage
class BlockVerticalFillMapRenderer(val expanse: BlockExpanse, quadPixelSize: Int = defaultQuadPixelSize) :
BlockHeatMapRenderer(quadPixelSize) {
override fun render(slice: ChangelogSlice, map: BlockStateMap): BufferedImage {
val blockMap = map as SparseBlockStateMap
val yMin = blockMap.blocks.minOf { xSection -> xSection.value.minOf { zSection -> zSection.value.size } }
val yMax = blockMap.blocks.maxOf { xSection -> xSection.value.maxOf { zSection -> zSection.value.size } }
val clamp = FloatClamp(yMin.toLong(), yMax.toLong())
return buildHeatMapImage(expanse, clamp) { x, z -> blockMap.blocks[x]?.get(z)?.maxOf { it.key } }
}
}

View File

@ -1,20 +0,0 @@
package gay.pizza.foundation.gjallarhorn.render
import gay.pizza.foundation.gjallarhorn.render.ui.GraphicalRenderSession
import gay.pizza.foundation.gjallarhorn.state.BlockExpanse
import gay.pizza.foundation.gjallarhorn.state.BlockStateMap
import gay.pizza.foundation.gjallarhorn.state.ChangelogSlice
import java.awt.image.BufferedImage
import javax.swing.WindowConstants
class LaunchGraphicalRenderSession(val expanse: BlockExpanse) : BlockImageRenderer {
override fun render(slice: ChangelogSlice, map: BlockStateMap): BufferedImage {
val session = GraphicalRenderSession(expanse, map)
session.isVisible = true
session.defaultCloseOperation = WindowConstants.HIDE_ON_CLOSE
while (session.isVisible) {
Thread.sleep(1000)
}
return BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR)
}
}

View File

@ -1,60 +0,0 @@
package gay.pizza.foundation.gjallarhorn.render
import gay.pizza.foundation.gjallarhorn.state.BlockCoordinate
import gay.pizza.foundation.gjallarhorn.state.BlockCoordinateSparseMap
import gay.pizza.foundation.gjallarhorn.state.BlockExpanse
import gay.pizza.foundation.gjallarhorn.state.BlockStateMap
import gay.pizza.foundation.gjallarhorn.state.ChangelogSlice
import gay.pizza.foundation.gjallarhorn.util.BlockColorKey
import gay.pizza.foundation.heimdall.table.PlayerPositionTable
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.and
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.UUID
class PlayerLocationShareRenderer(
val expanse: BlockExpanse,
val db: Database,
quadPixelSize: Int = defaultQuadPixelSize
) : BlockGridRenderer(quadPixelSize) {
private val colorKey = BlockColorKey(mapOf())
override fun render(slice: ChangelogSlice, map: BlockStateMap): BufferedImage {
val start = slice.sliceChangeRange.start
val end = slice.sliceChangeRange.endInclusive
val playerSparseMap = BlockCoordinateSparseMap<MutableList<UUID>>()
val allPlayerIds = HashSet<UUID>()
transaction(db) {
PlayerPositionTable.select {
(PlayerPositionTable.time greater start) and
(PlayerPositionTable.time lessEq end)
}.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))
val player = it[PlayerPositionTable.player]
playerSparseMap.createOrModify(
coordinate,
create = { mutableListOf(player) },
modify = { players -> players.add(player) })
allPlayerIds.add(player)
}
}
val colorOfPlayers = allPlayerIds.associateWith { colorKey.map(it.toString()) }
return buildPixelQuadImage(expanse) { g, x, z ->
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)
}
}
}
}

View File

@ -1,22 +0,0 @@
package gay.pizza.foundation.gjallarhorn.render.ui
import gay.pizza.foundation.gjallarhorn.render.BlockDiversityRenderer
import gay.pizza.foundation.gjallarhorn.render.BlockHeightMapRenderer
import gay.pizza.foundation.gjallarhorn.render.BlockVerticalFillMapRenderer
import gay.pizza.foundation.gjallarhorn.state.BlockExpanse
import gay.pizza.foundation.gjallarhorn.state.BlockStateMap
import java.awt.Dimension
import javax.swing.JFrame
import javax.swing.JTabbedPane
class GraphicalRenderSession(val expanse: BlockExpanse, val map: BlockStateMap) : JFrame() {
init {
name = "Gjallarhorn Renderer"
size = Dimension(1024, 1024)
val pane = JTabbedPane()
pane.addTab("Block Diversity", LazyImageRenderer(map, BlockDiversityRenderer(expanse)))
pane.addTab("Height Map", LazyImageRenderer(map, BlockHeightMapRenderer(expanse)))
pane.addTab("Vertical Fill Map", LazyImageRenderer(map, BlockVerticalFillMapRenderer(expanse)))
add(pane)
}
}

View File

@ -1,21 +0,0 @@
package gay.pizza.foundation.gjallarhorn.render.ui
import gay.pizza.foundation.gjallarhorn.render.BlockImageRenderer
import gay.pizza.foundation.gjallarhorn.state.BlockStateMap
import gay.pizza.foundation.gjallarhorn.state.ChangelogSlice
import java.awt.Graphics
import javax.swing.JComponent
class LazyImageRenderer(val map: BlockStateMap, private val renderer: BlockImageRenderer) : JComponent() {
private val image by lazy {
renderer.render(ChangelogSlice.none, map)
}
override fun paint(g: Graphics?) {
g?.drawImage(image, 0, 0, this)
}
override fun paintComponent(g: Graphics?) {
g?.drawImage(image, 0, 0, this)
}
}

View File

@ -1,11 +0,0 @@
package gay.pizza.foundation.gjallarhorn.state
import java.time.Instant
data class BlockChange(
val time: Instant,
val type: BlockChangeType,
val location: BlockCoordinate,
val from: BlockState,
val to: BlockState
)

View File

@ -1,9 +0,0 @@
package gay.pizza.foundation.gjallarhorn.state
import kotlinx.serialization.Serializable
@Serializable
enum class BlockChangeType {
Place,
Break
}

View File

@ -1,93 +0,0 @@
package gay.pizza.foundation.gjallarhorn.state
import gay.pizza.foundation.heimdall.view.BlockChangeView
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import java.time.Duration
import java.time.Instant
import java.util.stream.Stream
class BlockChangelog(
val changes: List<BlockChange>
) {
fun slice(slice: ChangelogSlice): BlockChangelog = BlockChangelog(changes.filter {
slice.isTimeWithinFullRange(it.time)
})
fun countRelativeChangesInSlice(slice: ChangelogSlice): Int = changes.count {
slice.isTimeWithinSliceRange(it.time)
}
val fullTimeSlice: ChangelogSlice
get() = ChangelogSlice(changes.minOf { it.time }, changes.maxOf { it.time })
fun calculateChangelogSlices(interval: Duration, limit: Int? = null): List<ChangelogSlice> {
val start = fullTimeSlice.rootStartTime
val end = fullTimeSlice.sliceEndTime
var intervals = mutableListOf<Instant>()
var current = start
while (!current.isAfter(end)) {
intervals.add(current)
current = current.plus(interval)
}
if (limit != null) {
intervals = intervals.takeLast(limit).toMutableList()
}
return intervals.map { ChangelogSlice(start, it, interval) }
}
fun splitChangelogSlicesWithThreshold(
targetChangeThreshold: Int,
minimumTimeInterval: Duration,
slices: List<ChangelogSlice>
): List<ChangelogSlice> {
return slices.parallelStream().flatMap { slice ->
val count = countRelativeChangesInSlice(slice)
if (count < targetChangeThreshold ||
slice.sliceRelativeDuration < minimumTimeInterval
) {
return@flatMap Stream.of(slice)
}
val split = slice.split()
return@flatMap splitChangelogSlicesWithThreshold(targetChangeThreshold, minimumTimeInterval, split).parallelStream()
}.toList()
}
companion object {
fun query(db: Database, filter: Op<Boolean> = Op.TRUE): BlockChangelog = transaction(db) {
BlockChangelog(BlockChangeView.select(filter).orderBy(BlockChangeView.time).map { row ->
val time = row[BlockChangeView.time]
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 = BlockCoordinate(x.toLong(), y.toLong(), z.toLong())
val fromBlock = if (changeIsBreak) {
BlockState.cached(block)
} else {
BlockState.AirBlock
}
val toBlock = if (changeIsBreak) {
BlockState.AirBlock
} else {
BlockState.cached(block)
}
BlockChange(
time,
if (changeIsBreak) BlockChangeType.Break else BlockChangeType.Place,
location,
fromBlock,
toBlock
)
})
}
}
}

View File

@ -1,47 +0,0 @@
package gay.pizza.foundation.gjallarhorn.state
import kotlinx.serialization.Serializable
import java.util.Objects
@Serializable
data class BlockCoordinate(
val x: Long,
val y: Long,
val z: Long
) {
override fun equals(other: Any?): Boolean {
if (other !is BlockCoordinate) {
return false
}
return other.x == x && other.y == y && other.z == z
}
override fun hashCode(): Int = Objects.hash(x, y, z)
fun applyAsOffset(coordinate: BlockCoordinate) = coordinate.copy(
x = coordinate.x + x,
y = coordinate.y + y,
z = coordinate.z + z
)
companion object {
val zero = BlockCoordinate(0, 0, 0)
fun maxOf(coordinates: List<BlockCoordinate>): BlockCoordinate {
val x = coordinates.maxOf { it.x }
val y = coordinates.maxOf { it.y }
val z = coordinates.maxOf { it.z }
return BlockCoordinate(x, y, z)
}
fun minOf(coordinates: List<BlockCoordinate>): BlockCoordinate {
val x = coordinates.minOf { it.x }
val y = coordinates.minOf { it.y }
val z = coordinates.minOf { it.z }
return BlockCoordinate(x, y, z)
}
}
}

View File

@ -1,65 +0,0 @@
package gay.pizza.foundation.gjallarhorn.state
import gay.pizza.foundation.gjallarhorn.util.maxOfAll
import gay.pizza.foundation.gjallarhorn.util.minOfAll
import kotlin.math.absoluteValue
open class BlockCoordinateSparseMap<T>(blocks: Map<Long, Map<Long, Map<Long, T>>> = mutableMapOf()) : BlockCoordinateStore<T> {
private var internalBlocks = blocks
val blocks: Map<Long, Map<Long, Map<Long, T>>>
get() = internalBlocks
override fun get(position: BlockCoordinate): T? = internalBlocks[position.x]?.get(position.z)?.get(position.z)
override fun getVerticalSection(x: Long, z: Long): Map<Long, T>? = internalBlocks[x]?.get(z)
override fun getXSection(x: Long): Map<Long, Map<Long, T>>? = internalBlocks[x]
override fun put(position: BlockCoordinate, value: T) {
(((internalBlocks as MutableMap).getOrPut(position.x) {
mutableMapOf()
} as MutableMap).getOrPut(position.z) {
mutableMapOf()
} as MutableMap)[position.y] = value
}
override fun createOrModify(position: BlockCoordinate, create: () -> T, modify: (T) -> Unit) {
val existing = get(position)
if (existing == null) {
put(position, create())
} else {
modify(existing)
}
}
fun coordinateSequence(): Sequence<BlockCoordinate> = internalBlocks.asSequence().flatMap { x ->
x.value.asSequence().flatMap { z ->
z.value.asSequence().map { y -> BlockCoordinate(x.key, z.key, y.key) }
}
}
fun calculateZeroBlockOffset(): BlockCoordinate {
val (x, y, z) = coordinateSequence().minOfAll(3) { listOf(it.x, it.y, it.z) }
val xOffset = if (x < 0) x.absoluteValue else 0
val yOffset = if (y < 0) y.absoluteValue else 0
val zOffset = if (z < 0) z.absoluteValue else 0
return BlockCoordinate(xOffset, yOffset, zOffset)
}
fun calculateMaxBlock(): BlockCoordinate {
val (x, y, z) = coordinateSequence().maxOfAll(3) { listOf(it.x, it.y, it.z) }
return BlockCoordinate(x, y, z)
}
fun applyCoordinateOffset(offset: BlockCoordinate) {
val root = mutableMapOf<Long, MutableMap<Long, MutableMap<Long, T>>>()
internalBlocks = internalBlocks.map { xSection ->
val zSectionMap = mutableMapOf<Long, MutableMap<Long, T>>()
(xSection.key + offset.x) to xSection.value.map { zSection ->
val ySectionMap = mutableMapOf<Long, T>()
(zSection.key + offset.z) to zSection.value.mapKeys {
(it.key + offset.y)
}.toMap(ySectionMap)
}.toMap(zSectionMap)
}.toMap(root)
}
}

View File

@ -1,9 +0,0 @@
package gay.pizza.foundation.gjallarhorn.state
interface BlockCoordinateStore<T> {
fun get(position: BlockCoordinate): T?
fun getVerticalSection(x: Long, z: Long): Map<Long, T>?
fun getXSection(x: Long): Map<Long, Map<Long, T>>?
fun put(position: BlockCoordinate, value: T)
fun createOrModify(position: BlockCoordinate, create: () -> T, modify: (T) -> Unit)
}

View File

@ -1,16 +0,0 @@
package gay.pizza.foundation.gjallarhorn.state
import kotlinx.serialization.Serializable
@Serializable
data class BlockExpanse(
val offset: BlockCoordinate,
val size: BlockCoordinate
) {
companion object {
fun zeroOffsetAndMax(offset: BlockCoordinate, max: BlockCoordinate) = BlockExpanse(
offset,
offset.applyAsOffset(max)
)
}
}

View File

@ -1,62 +0,0 @@
package gay.pizza.foundation.gjallarhorn.state
import gay.pizza.foundation.gjallarhorn.util.maxOfAll
import gay.pizza.foundation.gjallarhorn.util.minOfAll
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.absoluteValue
class BlockLogTracker(private val mode: BlockTrackMode = BlockTrackMode.RemoveOnDelete, isConcurrent: Boolean = false) {
internal val blocks: MutableMap<BlockCoordinate, BlockState> = if (isConcurrent) ConcurrentHashMap() else mutableMapOf()
fun place(position: BlockCoordinate, state: BlockState) {
blocks[position] = state
}
fun placeAll(map: Map<BlockCoordinate, BlockState>) {
blocks.putAll(map)
}
fun delete(position: BlockCoordinate) {
if (mode == BlockTrackMode.AirOnDelete) {
blocks[position] = BlockState.AirBlock
} else {
blocks.remove(position)
}
}
fun calculateZeroBlockOffset(): BlockCoordinate {
val (x, y, z) = blocks.keys.minOfAll(3) { listOf(it.x, it.y, it.z) }
val xOffset = if (x < 0) x.absoluteValue else 0
val yOffset = if (y < 0) y.absoluteValue else 0
val zOffset = if (z < 0) z.absoluteValue else 0
return BlockCoordinate(xOffset, yOffset, zOffset)
}
fun calculateMaxBlock(): BlockCoordinate {
val (x, y, z) = blocks.keys.maxOfAll(3) { listOf(it.x, it.y, it.z) }
return BlockCoordinate(x, y, z)
}
fun isEmpty() = blocks.isEmpty()
fun isNotEmpty() = !isEmpty()
fun buildBlockMap(offset: BlockCoordinate = BlockCoordinate.zero): SparseBlockStateMap {
val map = SparseBlockStateMap()
blocks.forEach { (position, state) ->
val realPosition = offset.applyAsOffset(position)
map.put(realPosition, state)
}
return map
}
fun replay(changelog: BlockChangelog) = changelog.changes.forEach { change ->
if (change.type == BlockChangeType.Break) {
delete(change.location)
} else {
place(change.location, change.to)
}
}
fun get(position: BlockCoordinate): BlockState? = blocks[position]
}

View File

@ -1,81 +0,0 @@
package gay.pizza.foundation.gjallarhorn.state
import gay.pizza.foundation.gjallarhorn.render.BlockMapRenderer
import org.slf4j.LoggerFactory
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Future
import java.util.concurrent.ThreadPoolExecutor
class BlockMapRenderPool<T>(
val changelog: BlockChangelog,
val blockTrackMode: BlockTrackMode,
val createRendererFunction: (BlockExpanse) -> BlockMapRenderer<T>,
val delegate: BlockMapRenderPoolDelegate<T>,
val threadPoolExecutor: ThreadPoolExecutor,
val renderResultCallback: (ChangelogSlice, T) -> Unit
) {
private val trackers = ConcurrentHashMap<ChangelogSlice, BlockLogTracker>()
private val playbackJobFutures = ConcurrentHashMap<ChangelogSlice, Future<*>>()
private val renderJobFutures = ConcurrentHashMap<ChangelogSlice, Future<*>>()
fun submitPlaybackJob(id: String, slice: ChangelogSlice) {
val future = threadPoolExecutor.submit {
try {
runPlaybackSlice(id, slice)
} catch (e: Exception) {
logger.error("Failed to run playback job for slice $id", e)
}
}
playbackJobFutures[slice] = future
}
fun submitRenderJob(slice: ChangelogSlice, callback: () -> T) {
val future = threadPoolExecutor.submit {
try {
val result = callback()
renderResultCallback(slice, result)
} catch (e: Exception) {
logger.error("Failed to run render job for slice $slice", e)
}
}
renderJobFutures[slice] = future
}
fun render(slices: List<ChangelogSlice>) {
for (slice in slices) {
submitPlaybackJob((slices.indexOf(slice) + 1).toString(), slice)
}
for (future in playbackJobFutures.values) {
future.get()
}
delegate.onAllPlaybackComplete(this, trackers)
for (future in renderJobFutures.values) {
try {
future.get()
} catch (e: Exception) {
logger.error("Failed to render slice.", e)
}
}
}
private fun runPlaybackSlice(id: String, slice: ChangelogSlice) {
val start = System.currentTimeMillis()
val sliced = changelog.slice(slice)
val tracker = BlockLogTracker(blockTrackMode)
tracker.replay(sliced)
if (tracker.isNotEmpty()) {
trackers[slice] = tracker
delegate.onSinglePlaybackComplete(this, slice, tracker)
}
val end = System.currentTimeMillis()
val timeInMilliseconds = end - start
logger.debug("Playback Completed for Slice $id in ${timeInMilliseconds}ms")
}
companion object {
private val logger = LoggerFactory.getLogger(BlockMapRenderPool::class.java)
}
}

View File

@ -1,6 +0,0 @@
package gay.pizza.foundation.gjallarhorn.state
interface BlockMapRenderPoolDelegate<T> {
fun onSinglePlaybackComplete(pool: BlockMapRenderPool<T>, slice: ChangelogSlice, tracker: BlockLogTracker)
fun onAllPlaybackComplete(pool: BlockMapRenderPool<T>, trackers: Map<ChangelogSlice, BlockLogTracker>)
}

View File

@ -1,30 +0,0 @@
package gay.pizza.foundation.gjallarhorn.state
class BlockMapTimelapse<T> :
BlockMapRenderPoolDelegate<T> {
override fun onSinglePlaybackComplete(pool: BlockMapRenderPool<T>, slice: ChangelogSlice, tracker: BlockLogTracker) {
}
override fun onAllPlaybackComplete(
pool: BlockMapRenderPool<T>,
trackers: Map<ChangelogSlice, BlockLogTracker>
) {
if (trackers.isEmpty()) {
return
}
val allBlockOffsets = trackers.map { it.value.calculateZeroBlockOffset() }
val globalBlockOffset = BlockCoordinate.maxOf(allBlockOffsets)
val allBlockMaxes = trackers.map { it.value.calculateMaxBlock() }
val globalBlockMax = BlockCoordinate.maxOf(allBlockMaxes)
val globalBlockExpanse = BlockExpanse.zeroOffsetAndMax(globalBlockOffset, globalBlockMax)
val renderer = pool.createRendererFunction(globalBlockExpanse)
for ((slice, tracker) in trackers) {
pool.submitRenderJob(slice) {
val map = tracker.buildBlockMap(globalBlockExpanse.offset)
renderer.render(slice, map)
}
}
}
}

View File

@ -1,15 +0,0 @@
package gay.pizza.foundation.gjallarhorn.state
import kotlinx.serialization.Serializable
import java.util.concurrent.ConcurrentHashMap
@Serializable(BlockStateSerializer::class)
data class BlockState(val type: String) {
companion object {
private val cache = ConcurrentHashMap<String, BlockState>()
val AirBlock: BlockState = cached("minecraft:air")
fun cached(type: String): BlockState = cache.computeIfAbsent(type) { BlockState(type) }
}
}

View File

@ -1,3 +0,0 @@
package gay.pizza.foundation.gjallarhorn.state
typealias BlockStateMap = BlockCoordinateStore<BlockState>

View File

@ -1,20 +0,0 @@
package gay.pizza.foundation.gjallarhorn.state
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
class BlockStateSerializer : KSerializer<BlockState> {
override val descriptor: SerialDescriptor
get() = String.serializer().descriptor
override fun deserialize(decoder: Decoder): BlockState {
return BlockState.cached(decoder.decodeString())
}
override fun serialize(encoder: Encoder, value: BlockState) {
encoder.encodeString(value.type)
}
}

View File

@ -1,6 +0,0 @@
package gay.pizza.foundation.gjallarhorn.state
enum class BlockTrackMode {
RemoveOnDelete,
AirOnDelete
}

View File

@ -1,29 +0,0 @@
package gay.pizza.foundation.gjallarhorn.state
import java.time.Duration
import java.time.Instant
data class ChangelogSlice(val rootStartTime: Instant, val sliceEndTime: Instant, val sliceRelativeDuration: Duration) {
constructor(from: Instant, to: Instant) : this(from, to, Duration.ofMillis(to.toEpochMilli() - from.toEpochMilli()))
val sliceStartTime: Instant = sliceEndTime.minus(sliceRelativeDuration)
val fullTimeRange: ClosedRange<Instant> = rootStartTime..sliceEndTime
val sliceChangeRange: ClosedRange<Instant> = sliceStartTime..sliceEndTime
fun isTimeWithinFullRange(time: Instant) = time in fullTimeRange
fun isTimeWithinSliceRange(time: Instant) = time in sliceChangeRange
fun split(): List<ChangelogSlice> {
val half = sliceRelativeDuration.dividedBy(2)
val initial = sliceEndTime.minus(sliceRelativeDuration)
val first = initial.plus(half)
return listOf(
ChangelogSlice(rootStartTime, first, half),
ChangelogSlice(rootStartTime, sliceEndTime, half)
)
}
companion object {
val none = ChangelogSlice(Instant.MIN, Instant.MIN, Duration.ZERO)
}
}

View File

@ -1,15 +0,0 @@
package gay.pizza.foundation.gjallarhorn.state
import java.time.Instant
import java.util.UUID
data class PlayerPositionChange(
val time: Instant,
val player: UUID,
val world: UUID,
val x: Double,
val y: Double,
val z: Double,
val pitch: Double,
val yaw: Double
)

View File

@ -1,28 +0,0 @@
package gay.pizza.foundation.gjallarhorn.state
import gay.pizza.foundation.heimdall.table.PlayerPositionTable
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
class PlayerPositionChangelog(
val changes: List<PlayerPositionChange>
) {
companion object {
fun query(db: Database, filter: Op<Boolean> = Op.TRUE): PlayerPositionChangelog = transaction(db) {
PlayerPositionChangelog(PlayerPositionTable.select(filter).orderBy(PlayerPositionTable.time).map { row ->
val time = row[PlayerPositionTable.time]
val player = row[PlayerPositionTable.player]
val world = row[PlayerPositionTable.world]
val x = row[PlayerPositionTable.x]
val y = row[PlayerPositionTable.y]
val z = row[PlayerPositionTable.z]
val pitch = row[PlayerPositionTable.z]
val yaw = row[PlayerPositionTable.z]
PlayerPositionChange(time, player, world, x, y, z, pitch, yaw)
})
}
}
}

View File

@ -1,7 +0,0 @@
package gay.pizza.foundation.gjallarhorn.state
import kotlinx.serialization.Serializable
@Serializable(SparseBlockStateMapSerializer::class)
class SparseBlockStateMap(blocks: Map<Long, Map<Long, Map<Long, BlockState>>> = mutableMapOf()) :
BlockCoordinateSparseMap<BlockState>(blocks)

View File

@ -1,23 +0,0 @@
package gay.pizza.foundation.gjallarhorn.state
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
class SparseBlockStateMapSerializer : KSerializer<SparseBlockStateMap> {
private val internal = MapSerializer(Long.serializer(), MapSerializer(Long.serializer(), MapSerializer(Long.serializer(), BlockState.serializer())))
override val descriptor: SerialDescriptor
get() = internal.descriptor
override fun deserialize(decoder: Decoder): SparseBlockStateMap {
val data = internal.deserialize(decoder)
return SparseBlockStateMap(data)
}
override fun serialize(encoder: Encoder, value: SparseBlockStateMap) {
internal.serialize(encoder, value.blocks)
}
}

View File

@ -1,20 +0,0 @@
package gay.pizza.foundation.gjallarhorn.util
import java.awt.Color
import java.util.concurrent.ConcurrentHashMap
class BlockColorKey(assigned: Map<String, Color>) {
private val colors = ConcurrentHashMap(assigned)
fun map(key: String): Color = colors.computeIfAbsent(key) { findUniqueColor() }
private fun findUniqueColor(): Color {
var random = randomColor()
while (colors.values.any { it.rgb == random.rgb }) {
random = randomColor()
}
return random
}
private fun randomColor() = Color((Math.random() * 0x1000000).toInt())
}

View File

@ -1,18 +0,0 @@
package gay.pizza.foundation.gjallarhorn.util
import java.awt.Color
val defaultBlockColorMap = mapOf<String, Color>(
"minecraft:air" to Color.black,
"minecraft:dirt" to Color.decode("#9b7653"),
"minecraft:farmland" to Color.decode("#5d3f2a"),
"minecraft:stone" to Color.decode("#787366"),
"minecraft:cobblestone" to Color.decode("#c4bca7"),
"minecraft:wheat" to Color.decode("#9e884c"),
"minecraft:carrots" to Color.decode("#f89d40"),
"minecraft:stone_brick_stairs" to Color.decode("#b8a18c"),
"minecraft:dirt_path" to Color.decode("#8f743d"),
"minecraft:deepslate_tiles" to Color.decode("#49494b"),
"minecraft:spruce_planks" to Color.decode("#60492d"),
"minecraft:water" to Color.decode("#1f54ff")
)

View File

@ -1,57 +0,0 @@
package gay.pizza.foundation.gjallarhorn.util
import java.awt.Color
import kotlin.math.max
class ColorGradient constructor() {
constructor(vararg points: ColorGradientPoint) : this() {
for (point in points) {
addColorPoint(point)
}
}
private val points = mutableListOf<ColorGradientPoint>()
fun addColorPoint(point: ColorGradientPoint) {
for (x in 0 until points.size) {
if (point.value < points[x].value) {
points.add(x, point)
return
}
}
points.add(point)
}
fun getColorAtValue(value: Float): Color {
if (points.isEmpty()) {
return ColorGradientPoint(0f, 0f, 0f, value).toColor()
}
for (x in 0 until points.size) {
val current = points[x]
if (value < current.value) {
val previous = points[max(0, x - 1)]
val diff = previous.value - current.value
val fractionBetween = if (diff == 0f) 0f else (value - current.value) / diff
return ColorGradientPoint(
(previous.r - current.r) * fractionBetween + current.r,
(previous.g - current.g) * fractionBetween + current.g,
(previous.b - current.b) * fractionBetween + current.b,
value
).toColor()
}
}
return points.last().copy(value = value).toColor()
}
companion object {
val HeatMap = ColorGradient(
ColorGradientPoint(0f, 0f, 1f, 0.0f),
ColorGradientPoint(0f, 1f, 1f, 0.25f),
ColorGradientPoint(0f, 1f, 0f, 0.5f),
ColorGradientPoint(1f, 1f, 0f, 0.75f),
ColorGradientPoint(1f, 0f, 0f, 1.0f)
)
}
}

View File

@ -1,16 +0,0 @@
package gay.pizza.foundation.gjallarhorn.util
import java.awt.Color
data class ColorGradientPoint(
val r: Float,
val g: Float,
val b: Float,
val value: Float
) {
fun toColor() = Color(
FloatClamp.ColorRgbComponent.convert(r).toInt(),
FloatClamp.ColorRgbComponent.convert(g).toInt(),
FloatClamp.ColorRgbComponent.convert(b).toInt()
)
}

View File

@ -1,12 +0,0 @@
package gay.pizza.foundation.gjallarhorn.util
import kotlin.math.roundToLong
class FloatClamp(val min: Long, val max: Long) {
fun convert(value: Float): Long = (value * max.toFloat()).roundToLong() + min
fun convert(value: Long): Float = (value - min.toFloat()) / max
companion object {
val ColorRgbComponent = FloatClamp(0, 255)
}
}

View File

@ -1,11 +0,0 @@
package gay.pizza.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.")
}
}

View File

@ -1,12 +0,0 @@
package gay.pizza.foundation.gjallarhorn.util
import org.jetbrains.exposed.sql.Op
fun compose(
combine: (Op<Boolean>, Op<Boolean>) -> Op<Boolean>,
vararg filters: Pair<() -> Boolean, () -> Op<Boolean>>
): Op<Boolean> = filters.toMap().entries
.asSequence()
.filter { it.key() }
.map { it.value() }
.fold(Op.TRUE as Op<Boolean>, combine)

View File

@ -1,39 +0,0 @@
package gay.pizza.foundation.gjallarhorn.util
fun <T> Iterable<T>.minOfAll(fieldCount: Int, block: (value: T) -> List<Long>): List<Long> {
val fieldRange = 0 until fieldCount
val results = fieldRange.map { Long.MAX_VALUE }.toMutableList()
for (item in this) {
val numerics = block(item)
for (field in fieldRange) {
val current = results[field]
val number = numerics[field]
if (number < current) {
results[field] = number
}
}
}
return results
}
fun <T> Iterable<T>.maxOfAll(fieldCount: Int, block: (value: T) -> List<Long>): List<Long> {
val fieldRange = 0 until fieldCount
val results = fieldRange.map { Long.MIN_VALUE }.toMutableList()
for (item in this) {
val numerics = block(item)
for (field in fieldRange) {
val current = results[field]
val number = numerics[field]
if (number > current) {
results[field] = number
}
}
}
return results
}
fun <T> Sequence<T>.minOfAll(fieldCount: Int, block: (value: T) -> List<Long>): List<Long> =
asIterable().minOfAll(fieldCount, block)
fun <T> Sequence<T>.maxOfAll(fieldCount: Int, block: (value: T) -> List<Long>): List<Long> =
asIterable().maxOfAll(fieldCount, block)

View File

@ -1,64 +0,0 @@
WITH
unique_player_ids AS (
SELECT
DISTINCT player
FROM heimdall.player_sessions
),
player_names AS (
SELECT
player,
(
SELECT name
FROM heimdall.player_sessions
WHERE player = unique_player_ids.player
ORDER BY "end" DESC
LIMIT 1
) AS name
FROM unique_player_ids
),
unique_world_ids AS (
SELECT
DISTINCT to_world AS world
FROM heimdall.world_changes
),
world_names AS (
SELECT
world,
(
SELECT to_world_name
FROM heimdall.world_changes
WHERE world = heimdall.world_changes.to_world
ORDER BY time DESC
LIMIT 1
) AS name
FROM unique_world_ids
),
player_calculated_positions AS (
SELECT
player,
world,
AVG(x) AS avg_x,
AVG(y) AS avg_y,
AVG(z) AS avg_z,
MAX(x) AS max_x,
MAX(y) AS max_y,
MAX(z) AS max_z,
MIN(x) AS min_x,
MIN(y) AS min_y,
MIN(z) AS min_z,
COUNT(*) AS count,
MODE() WITHIN GROUP (ORDER BY x) AS mode_x,
MODE() WITHIN GROUP (ORDER BY y) AS mode_y,
MODE() WITHIN GROUP (ORDER BY z) AS mode_z
FROM heimdall.player_positions
GROUP BY player, world
)
SELECT
player_names.name AS player_name,
world_names.name AS world_name,
player_calculated_positions.*
FROM player_calculated_positions
JOIN player_names
ON player_names.player = player_calculated_positions.player
JOIN world_names
ON world_names.world = player_calculated_positions.world