mirror of
https://github.com/GayPizzaSpecifications/foundation.git
synced 2025-08-02 21:20:55 +00:00
Heimdall: It's back!
This commit is contained in:
parent
086f7dba10
commit
7289e5cb9f
@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
plugins {
|
||||
java
|
||||
id("gay.pizza.foundation.concrete-root") version "0.7.0"
|
||||
id("gay.pizza.foundation.concrete-library") version "0.7.0" apply false
|
||||
id("gay.pizza.foundation.concrete-plugin") version "0.7.0" apply false
|
||||
}
|
||||
|
||||
@ -53,7 +54,7 @@ subprojects {
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs += "-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi"
|
||||
freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
10
common-heimdall/build.gradle.kts
Normal file
10
common-heimdall/build.gradle.kts
Normal file
@ -0,0 +1,10 @@
|
||||
plugins {
|
||||
id("gay.pizza.foundation.concrete-library")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api("org.postgresql:postgresql:42.5.1")
|
||||
api("org.jetbrains.exposed:exposed-jdbc:0.41.1")
|
||||
api("org.jetbrains.exposed:exposed-java-time:0.41.1")
|
||||
api("com.zaxxer:HikariCP:5.0.1")
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package gay.pizza.foundation.heimdall.export
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ExportedBlock(
|
||||
val type: String
|
||||
)
|
@ -0,0 +1,10 @@
|
||||
package gay.pizza.foundation.heimdall.export
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ExportedChunk(
|
||||
val x: Int,
|
||||
val z: Int,
|
||||
val sections: List<ExportedChunkSection>
|
||||
)
|
@ -0,0 +1,10 @@
|
||||
package gay.pizza.foundation.heimdall.export
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ExportedChunkSection(
|
||||
val x: Int,
|
||||
val z: Int,
|
||||
val blocks: List<ExportedBlock>
|
||||
)
|
@ -0,0 +1,16 @@
|
||||
package gay.pizza.foundation.heimdall.table
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import org.jetbrains.exposed.sql.javatime.timestamp
|
||||
|
||||
object BlockBreakTable : Table("block_breaks") {
|
||||
val time = timestamp("time")
|
||||
val player = uuid("player")
|
||||
val world = uuid("world")
|
||||
val x = double("x")
|
||||
val y = double("y")
|
||||
val z = double("z")
|
||||
val pitch = double("pitch")
|
||||
val yaw = double("yaw")
|
||||
val block = text("block")
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package gay.pizza.foundation.heimdall.table
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import org.jetbrains.exposed.sql.javatime.timestamp
|
||||
|
||||
object BlockPlaceTable : Table("block_places") {
|
||||
val time = timestamp("time")
|
||||
val player = uuid("player")
|
||||
val world = uuid("world")
|
||||
val x = double("x")
|
||||
val y = double("y")
|
||||
val z = double("z")
|
||||
val pitch = double("pitch")
|
||||
val yaw = double("yaw")
|
||||
val block = text("block")
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package gay.pizza.foundation.heimdall.table
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import org.jetbrains.exposed.sql.javatime.timestamp
|
||||
|
||||
object EntityKillTable : Table("entity_kills") {
|
||||
val time = timestamp("time")
|
||||
val player = uuid("player")
|
||||
val entity = uuid("entity")
|
||||
val world = uuid("world")
|
||||
val x = double("x")
|
||||
val y = double("y")
|
||||
val z = double("z")
|
||||
val pitch = double("pitch")
|
||||
val yaw = double("yaw")
|
||||
val entityType = text("entity_type")
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package gay.pizza.foundation.heimdall.table
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import org.jetbrains.exposed.sql.javatime.timestamp
|
||||
|
||||
object PlayerAdvancementTable : Table("player_advancements") {
|
||||
val time = timestamp("time")
|
||||
val player = uuid("player")
|
||||
val world = uuid("world")
|
||||
val x = double("x")
|
||||
val y = double("y")
|
||||
val z = double("z")
|
||||
val pitch = double("pitch")
|
||||
val yaw = double("yaw")
|
||||
val advancement = text("advancement")
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package gay.pizza.foundation.heimdall.table
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import org.jetbrains.exposed.sql.javatime.timestamp
|
||||
|
||||
object PlayerDeathTable : Table("player_deaths") {
|
||||
val time = timestamp("time")
|
||||
val world = uuid("world")
|
||||
val player = uuid("player")
|
||||
val x = double("x")
|
||||
val y = double("y")
|
||||
val z = double("z")
|
||||
val pitch = double("pitch")
|
||||
val yaw = double("yaw")
|
||||
val experience = double("experience")
|
||||
val message = text("message").nullable()
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package gay.pizza.foundation.heimdall.table
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import org.jetbrains.exposed.sql.javatime.timestamp
|
||||
|
||||
object PlayerPositionTable : Table("player_positions") {
|
||||
val time = timestamp("time")
|
||||
val player = uuid("player")
|
||||
val world = uuid("world")
|
||||
val x = double("x")
|
||||
val y = double("y")
|
||||
val z = double("z")
|
||||
val pitch = double("pitch")
|
||||
val yaw = double("yaw")
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package gay.pizza.foundation.heimdall.table
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import org.jetbrains.exposed.sql.javatime.timestamp
|
||||
|
||||
object PlayerSessionTable : Table("player_sessions") {
|
||||
val id = uuid("id")
|
||||
val player = uuid("player")
|
||||
val name = text("name")
|
||||
val startTime = timestamp("start")
|
||||
val endTime = timestamp("end")
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package gay.pizza.foundation.heimdall.table
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import org.jetbrains.exposed.sql.javatime.timestamp
|
||||
|
||||
object WorldChangeTable : Table("world_changes") {
|
||||
val time = timestamp("time")
|
||||
val player = uuid("player")
|
||||
val fromWorld = uuid("from_world")
|
||||
val toWorld = uuid("to_world")
|
||||
val fromWorldName = text("from_world_name")
|
||||
val toWorldName = text("to_world_name")
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package gay.pizza.foundation.heimdall.view
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import org.jetbrains.exposed.sql.javatime.timestamp
|
||||
|
||||
object BlockChangeView : Table("block_changes") {
|
||||
val isBreak = bool("break")
|
||||
val time = timestamp("time")
|
||||
val player = uuid("player")
|
||||
val world = uuid("world")
|
||||
val x = double("x")
|
||||
val y = double("y")
|
||||
val z = double("z")
|
||||
val pitch = double("pitch")
|
||||
val yaw = double("yaw")
|
||||
val block = text("block")
|
||||
}
|
7
foundation-heimdall/build.gradle.kts
Normal file
7
foundation-heimdall/build.gradle.kts
Normal file
@ -0,0 +1,7 @@
|
||||
plugins {
|
||||
id("gay.pizza.foundation.concrete-plugin")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":common-heimdall"))
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package gay.pizza.foundation.heimdall.plugin
|
||||
|
||||
fun String.sqlSplitStatements(): List<String> {
|
||||
val statements = mutableListOf<String>()
|
||||
val buffer = StringBuilder()
|
||||
fun flush() {
|
||||
val trimmed = buffer.toString().trim()
|
||||
if (trimmed.isNotEmpty()) {
|
||||
statements.add(trimmed)
|
||||
}
|
||||
}
|
||||
for (line in lines()) {
|
||||
if (line.trim() == "--") {
|
||||
flush()
|
||||
} else {
|
||||
buffer.append(line).append("\n")
|
||||
}
|
||||
}
|
||||
flush()
|
||||
return statements
|
||||
}
|
@ -0,0 +1,201 @@
|
||||
package gay.pizza.foundation.heimdall.plugin
|
||||
|
||||
import gay.pizza.foundation.heimdall.plugin.buffer.BufferFlushThread
|
||||
import gay.pizza.foundation.heimdall.plugin.buffer.EventBuffer
|
||||
import gay.pizza.foundation.heimdall.plugin.event.*
|
||||
import gay.pizza.foundation.heimdall.plugin.model.HeimdallConfig
|
||||
import gay.pizza.foundation.heimdall.plugin.export.ExportChunksCommand
|
||||
import com.charleskorn.kaml.Yaml
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer
|
||||
import org.bukkit.event.EventHandler
|
||||
import org.bukkit.event.Listener
|
||||
import org.bukkit.event.block.BlockBreakEvent
|
||||
import org.bukkit.event.block.BlockPlaceEvent
|
||||
import org.bukkit.event.entity.EntityDeathEvent
|
||||
import org.bukkit.event.entity.PlayerDeathEvent
|
||||
import org.bukkit.event.player.*
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.postgresql.Driver
|
||||
import org.slf4j.Logger
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.io.path.inputStream
|
||||
|
||||
class HeimdallPlugin : JavaPlugin(), Listener {
|
||||
private lateinit var config: HeimdallConfig
|
||||
private lateinit var pool: HikariDataSource
|
||||
internal var db: Database? = null
|
||||
|
||||
private val buffer = EventBuffer()
|
||||
private val bufferFlushThread = BufferFlushThread(this, buffer)
|
||||
|
||||
private val playerJoinTimes = ConcurrentHashMap<UUID, Instant>()
|
||||
|
||||
private val legacyComponentSerializer = LegacyComponentSerializer.builder().build()
|
||||
|
||||
override fun onEnable() {
|
||||
val exportChunksCommand = getCommand("export_all_chunks") ?: throw Exception("Failed to get export_all_chunks command")
|
||||
exportChunksCommand.setExecutor(ExportChunksCommand(this))
|
||||
|
||||
val pluginDataPath = dataFolder.toPath()
|
||||
pluginDataPath.toFile().mkdir()
|
||||
|
||||
val configPath = copyDefaultConfig<HeimdallPlugin>(
|
||||
slF4JLogger,
|
||||
pluginDataPath,
|
||||
"heimdall.yaml"
|
||||
)
|
||||
config = Yaml.default.decodeFromStream(HeimdallConfig.serializer(), configPath.inputStream())
|
||||
if (!config.enabled) {
|
||||
slF4JLogger.info("Heimdall is not enabled.")
|
||||
return
|
||||
}
|
||||
slF4JLogger.info("Heimdall is enabled.")
|
||||
if (!Driver.isRegistered()) {
|
||||
Driver.register()
|
||||
}
|
||||
pool = HikariDataSource(HikariConfig().apply {
|
||||
jdbcUrl = config.db.url
|
||||
username = config.db.username
|
||||
password = config.db.password
|
||||
maximumPoolSize = 10
|
||||
idleTimeout = Duration.ofMinutes(5).toMillis()
|
||||
maxLifetime = Duration.ofMinutes(10).toMillis()
|
||||
})
|
||||
val initMigrationContent = HeimdallPlugin::class.java.getResourceAsStream(
|
||||
"/init.sql"
|
||||
)?.readAllBytes()?.decodeToString() ?: throw RuntimeException("Unable to find Heimdall init.sql")
|
||||
|
||||
val statements = initMigrationContent.sqlSplitStatements()
|
||||
|
||||
pool.connection.use { conn ->
|
||||
conn.autoCommit = false
|
||||
try {
|
||||
for (statementAsString in statements) {
|
||||
conn.prepareStatement(statementAsString).use {
|
||||
it.execute()
|
||||
}
|
||||
}
|
||||
conn.commit()
|
||||
} catch (e: Exception) {
|
||||
conn.rollback()
|
||||
throw e
|
||||
} finally {
|
||||
conn.autoCommit = true
|
||||
}
|
||||
}
|
||||
|
||||
db = Database.connect(pool)
|
||||
server.pluginManager.registerEvents(this, this)
|
||||
bufferFlushThread.start()
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
fun onPlayerMove(event: PlayerMoveEvent) = buffer.push(PlayerPosition(event))
|
||||
|
||||
@EventHandler
|
||||
fun onBlockBroken(event: BlockPlaceEvent) = buffer.push(BlockPlace(event))
|
||||
|
||||
@EventHandler
|
||||
fun onBlockBroken(event: BlockBreakEvent) = buffer.push(BlockBreak(event))
|
||||
|
||||
@EventHandler
|
||||
fun onPlayerJoin(event: PlayerJoinEvent) {
|
||||
playerJoinTimes[event.player.uniqueId] = Instant.now()
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
fun onPlayerQuit(event: PlayerQuitEvent) {
|
||||
val startTime = playerJoinTimes.remove(event.player.uniqueId) ?: return
|
||||
val endTime = Instant.now()
|
||||
buffer.push(PlayerSession(event.player.uniqueId, event.player.name, startTime, endTime))
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
fun onPlayerDeath(event: PlayerDeathEvent) {
|
||||
val deathMessage = event.deathMessage()
|
||||
val deathMessageString = if (deathMessage != null) {
|
||||
legacyComponentSerializer.serialize(deathMessage)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
buffer.push(PlayerDeath(event, deathMessageString))
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
fun onPlayerAdvancementDone(event: PlayerAdvancementDoneEvent) = buffer.push(PlayerAdvancement(event))
|
||||
|
||||
@EventHandler
|
||||
fun onWorldLoad(event: PlayerChangedWorldEvent) = buffer.push(
|
||||
WorldChange(
|
||||
event.player.uniqueId,
|
||||
event.from.uid,
|
||||
event.from.name,
|
||||
event.player.world.uid,
|
||||
event.player.world.name
|
||||
)
|
||||
)
|
||||
|
||||
@EventHandler
|
||||
fun onEntityDeath(event: EntityDeathEvent) {
|
||||
val killer = event.entity.killer ?: return
|
||||
buffer.push(
|
||||
EntityKill(
|
||||
killer.uniqueId,
|
||||
killer.location,
|
||||
event.entity.uniqueId,
|
||||
event.entityType.key.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDisable() {
|
||||
bufferFlushThread.stop()
|
||||
val endTime = Instant.now()
|
||||
for (playerId in playerJoinTimes.keys().toList()) {
|
||||
val startTime = playerJoinTimes.remove(playerId) ?: continue
|
||||
buffer.push(PlayerSession(
|
||||
playerId,
|
||||
server.getPlayer(playerId)?.name ?: "__unknown__",
|
||||
startTime,
|
||||
endTime
|
||||
))
|
||||
}
|
||||
bufferFlushThread.flush()
|
||||
}
|
||||
|
||||
private inline fun <reified T> copyDefaultConfig(log: Logger, targetPath: Path, resourceName: String): Path {
|
||||
if (resourceName.startsWith("/")) {
|
||||
throw IllegalArgumentException("resourceName starts with slash")
|
||||
}
|
||||
|
||||
if (!targetPath.toFile().exists()) {
|
||||
throw Exception("Configuration output path does not exist!")
|
||||
}
|
||||
val outPath = targetPath.resolve(resourceName)
|
||||
val outFile = outPath.toFile()
|
||||
if (outFile.exists()) {
|
||||
log.debug("Configuration file already exists.")
|
||||
return outPath
|
||||
}
|
||||
|
||||
val resourceStream = T::class.java.getResourceAsStream("/$resourceName")
|
||||
?: throw Exception("Configuration resource does not exist!")
|
||||
val outputStream = outFile.outputStream()
|
||||
|
||||
resourceStream.use {
|
||||
outputStream.use {
|
||||
log.info("Copied default configuration to $outPath")
|
||||
resourceStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
return outPath
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package gay.pizza.foundation.heimdall.plugin.buffer
|
||||
|
||||
import gay.pizza.foundation.heimdall.plugin.HeimdallPlugin
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class BufferFlushThread(val plugin: HeimdallPlugin, val buffer: EventBuffer) {
|
||||
private val running = AtomicBoolean(false)
|
||||
private var thread: Thread? = null
|
||||
|
||||
fun start() {
|
||||
running.set(true)
|
||||
val thread = Thread {
|
||||
plugin.slF4JLogger.info("Buffer Flusher Started")
|
||||
while (running.get()) {
|
||||
flush()
|
||||
Thread.sleep(5000)
|
||||
}
|
||||
plugin.slF4JLogger.info("Buffer Flusher Stopped")
|
||||
}
|
||||
thread.name = "Heimdall Buffer Flush"
|
||||
thread.isDaemon = false
|
||||
thread.start()
|
||||
this.thread = thread
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
running.set(false)
|
||||
thread?.join()
|
||||
}
|
||||
|
||||
fun flush() {
|
||||
try {
|
||||
val db = plugin.db
|
||||
if (db == null) {
|
||||
buffer.clear()
|
||||
return
|
||||
}
|
||||
transaction(plugin.db) {
|
||||
val count = buffer.flush(this)
|
||||
if (count > 0) {
|
||||
plugin.slF4JLogger.debug("Flushed $count Events")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
plugin.slF4JLogger.warn("Failed to flush buffer.", e)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package gay.pizza.foundation.heimdall.plugin.buffer
|
||||
|
||||
import gay.pizza.foundation.heimdall.plugin.event.HeimdallEvent
|
||||
import org.jetbrains.exposed.sql.Transaction
|
||||
|
||||
class EventBuffer {
|
||||
private var events = mutableListOf<HeimdallEvent>()
|
||||
|
||||
fun flush(transaction: Transaction): Long {
|
||||
val referenceOfEvents = events
|
||||
this.events = mutableListOf()
|
||||
var count = 0L
|
||||
while (referenceOfEvents.isNotEmpty()) {
|
||||
val event = referenceOfEvents.removeAt(0)
|
||||
event.store(transaction)
|
||||
count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
fun push(event: HeimdallEvent) {
|
||||
events.add(event)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
events = mutableListOf()
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package gay.pizza.foundation.heimdall.plugin.event
|
||||
|
||||
import gay.pizza.foundation.heimdall.table.BlockBreakTable
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.event.block.BlockBreakEvent
|
||||
import org.jetbrains.exposed.sql.Transaction
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class BlockBreak(
|
||||
val playerUniqueIdentity: UUID,
|
||||
val location: Location,
|
||||
val material: Material,
|
||||
val timestamp: Instant = Instant.now()
|
||||
) : HeimdallEvent() {
|
||||
constructor(event: BlockBreakEvent) : this(event.player.uniqueId, event.block.location, event.block.type)
|
||||
|
||||
override fun store(transaction: Transaction) {
|
||||
transaction.apply {
|
||||
BlockBreakTable.insert {
|
||||
it[time] = timestamp
|
||||
it[player] = playerUniqueIdentity
|
||||
it[world] = location.world.uid
|
||||
it[x] = location.x
|
||||
it[y] = location.y
|
||||
it[z] = location.z
|
||||
it[pitch] = location.pitch.toDouble()
|
||||
it[yaw] = location.yaw.toDouble()
|
||||
it[block] = material.key.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package gay.pizza.foundation.heimdall.plugin.event
|
||||
|
||||
import gay.pizza.foundation.heimdall.table.BlockPlaceTable
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.event.block.BlockPlaceEvent
|
||||
import org.jetbrains.exposed.sql.Transaction
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class BlockPlace(
|
||||
val playerUniqueIdentity: UUID,
|
||||
val location: Location,
|
||||
val material: Material,
|
||||
val timestamp: Instant = Instant.now()
|
||||
) : HeimdallEvent() {
|
||||
constructor(event: BlockPlaceEvent) : this(event.player.uniqueId, event.block.location, event.block.type)
|
||||
|
||||
override fun store(transaction: Transaction) {
|
||||
transaction.apply {
|
||||
BlockPlaceTable.insert {
|
||||
it[time] = timestamp
|
||||
it[player] = playerUniqueIdentity
|
||||
it[world] = location.world.uid
|
||||
it[x] = location.x
|
||||
it[y] = location.y
|
||||
it[z] = location.z
|
||||
it[pitch] = location.pitch.toDouble()
|
||||
it[yaw] = location.yaw.toDouble()
|
||||
it[block] = material.key.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package gay.pizza.foundation.heimdall.plugin.event
|
||||
|
||||
import gay.pizza.foundation.heimdall.table.EntityKillTable
|
||||
import org.bukkit.Location
|
||||
import org.jetbrains.exposed.sql.Transaction
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class EntityKill(
|
||||
val playerUniqueIdentity: UUID,
|
||||
val location: Location,
|
||||
val entityUniqueIdentity: UUID,
|
||||
val entityTypeName: String,
|
||||
val timestamp: Instant = Instant.now()
|
||||
) : HeimdallEvent() {
|
||||
override fun store(transaction: Transaction) {
|
||||
transaction.apply {
|
||||
EntityKillTable.insert {
|
||||
it[time] = timestamp
|
||||
it[player] = playerUniqueIdentity
|
||||
it[world] = location.world.uid
|
||||
it[x] = location.x
|
||||
it[y] = location.y
|
||||
it[z] = location.z
|
||||
it[pitch] = location.pitch.toDouble()
|
||||
it[yaw] = location.yaw.toDouble()
|
||||
it[entity] = entityUniqueIdentity
|
||||
it[entityType] = entityTypeName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package gay.pizza.foundation.heimdall.plugin.event
|
||||
|
||||
import org.jetbrains.exposed.sql.Transaction
|
||||
|
||||
abstract class HeimdallEvent {
|
||||
abstract fun store(transaction: Transaction)
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package gay.pizza.foundation.heimdall.plugin.event
|
||||
|
||||
import gay.pizza.foundation.heimdall.table.PlayerAdvancementTable
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.advancement.Advancement
|
||||
import org.bukkit.event.player.PlayerAdvancementDoneEvent
|
||||
import org.jetbrains.exposed.sql.Transaction
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class PlayerAdvancement(
|
||||
val playerUniqueIdentity: UUID,
|
||||
val location: Location,
|
||||
val advancement: Advancement,
|
||||
val timestamp: Instant = Instant.now()
|
||||
) : HeimdallEvent() {
|
||||
constructor(event: PlayerAdvancementDoneEvent) : this(event.player.uniqueId, event.player.location, event.advancement)
|
||||
|
||||
override fun store(transaction: Transaction) {
|
||||
transaction.apply {
|
||||
PlayerAdvancementTable.insert {
|
||||
it[time] = timestamp
|
||||
it[player] = playerUniqueIdentity
|
||||
it[world] = location.world.uid
|
||||
it[x] = location.x
|
||||
it[y] = location.y
|
||||
it[z] = location.z
|
||||
it[pitch] = location.pitch.toDouble()
|
||||
it[yaw] = location.yaw.toDouble()
|
||||
it[advancement] = this@PlayerAdvancement.advancement.key.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package gay.pizza.foundation.heimdall.plugin.event
|
||||
|
||||
import gay.pizza.foundation.heimdall.table.PlayerDeathTable
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.event.entity.PlayerDeathEvent
|
||||
import org.jetbrains.exposed.sql.Transaction
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class PlayerDeath(
|
||||
val playerUniqueIdentity: UUID,
|
||||
val location: Location,
|
||||
val experienceLevel: Float,
|
||||
val deathMessage: String?,
|
||||
val timestamp: Instant = Instant.now()
|
||||
) : HeimdallEvent() {
|
||||
constructor(event: PlayerDeathEvent, deathMessage: String? = null) : this(
|
||||
event.player.uniqueId,
|
||||
event.player.location,
|
||||
event.player.exp,
|
||||
deathMessage
|
||||
)
|
||||
|
||||
override fun store(transaction: Transaction) {
|
||||
transaction.apply {
|
||||
PlayerDeathTable.insert {
|
||||
it[time] = timestamp
|
||||
it[player] = playerUniqueIdentity
|
||||
it[world] = location.world.uid
|
||||
it[x] = location.x
|
||||
it[y] = location.y
|
||||
it[z] = location.z
|
||||
it[pitch] = location.pitch.toDouble()
|
||||
it[yaw] = location.yaw.toDouble()
|
||||
it[experience] = experienceLevel.toDouble()
|
||||
it[message] = deathMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package gay.pizza.foundation.heimdall.plugin.event
|
||||
|
||||
import gay.pizza.foundation.heimdall.table.PlayerPositionTable
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.event.player.PlayerMoveEvent
|
||||
import org.jetbrains.exposed.sql.Transaction
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class PlayerPosition(
|
||||
val playerUniqueIdentity: UUID,
|
||||
val location: Location,
|
||||
val timestamp: Instant = Instant.now()
|
||||
) : HeimdallEvent() {
|
||||
constructor(event: PlayerMoveEvent) : this(event.player.uniqueId, event.to)
|
||||
|
||||
override fun store(transaction: Transaction) {
|
||||
transaction.apply {
|
||||
PlayerPositionTable.insert {
|
||||
it[time] = timestamp
|
||||
it[player] = playerUniqueIdentity
|
||||
it[world] = location.world.uid
|
||||
it[x] = location.x
|
||||
it[y] = location.y
|
||||
it[z] = location.z
|
||||
it[pitch] = location.pitch.toDouble()
|
||||
it[yaw] = location.yaw.toDouble()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package gay.pizza.foundation.heimdall.plugin.event
|
||||
|
||||
import gay.pizza.foundation.heimdall.table.PlayerSessionTable
|
||||
import org.jetbrains.exposed.sql.Transaction
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class PlayerSession(
|
||||
val playerUniqueIdentity: UUID,
|
||||
val playerName: String,
|
||||
val startTimeInstant: Instant,
|
||||
val endTimeInstant: Instant
|
||||
) : HeimdallEvent() {
|
||||
override fun store(transaction: Transaction) {
|
||||
transaction.apply {
|
||||
PlayerSessionTable.insert {
|
||||
it[id] = UUID.randomUUID()
|
||||
it[player] = playerUniqueIdentity
|
||||
it[name] = playerName
|
||||
it[startTime] = startTimeInstant
|
||||
it[endTime] = endTimeInstant
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package gay.pizza.foundation.heimdall.plugin.event
|
||||
|
||||
import gay.pizza.foundation.heimdall.table.WorldChangeTable
|
||||
import org.jetbrains.exposed.sql.Transaction
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class WorldChange(
|
||||
val playerUniqueIdentity: UUID,
|
||||
val fromWorldId: UUID,
|
||||
val fromWorldActualName: String,
|
||||
val toWorldId: UUID,
|
||||
val toWorldActualName: String,
|
||||
val timestamp: Instant = Instant.now()
|
||||
) : HeimdallEvent() {
|
||||
override fun store(transaction: Transaction) {
|
||||
transaction.apply {
|
||||
WorldChangeTable.insert {
|
||||
it[time] = timestamp
|
||||
it[player] = playerUniqueIdentity
|
||||
it[fromWorld] = fromWorldId
|
||||
it[fromWorldName] = fromWorldActualName
|
||||
it[toWorld] = toWorldId
|
||||
it[toWorldName] = toWorldActualName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
package gay.pizza.foundation.heimdall.plugin.export
|
||||
|
||||
import gay.pizza.foundation.heimdall.export.ExportedBlock
|
||||
import gay.pizza.foundation.heimdall.export.ExportedChunk
|
||||
import gay.pizza.foundation.heimdall.export.ExportedChunkSection
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.encodeToStream
|
||||
import org.bukkit.Chunk
|
||||
import org.bukkit.ChunkSnapshot
|
||||
import org.bukkit.World
|
||||
import org.bukkit.plugin.Plugin
|
||||
import java.io.File
|
||||
import java.util.zip.GZIPOutputStream
|
||||
|
||||
class ChunkExporter(private val plugin: Plugin, val world: World) {
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
fun exportLoadedChunksAsync() {
|
||||
exportChunkListAsync(world.loadedChunks.toList())
|
||||
}
|
||||
|
||||
private fun exportChunkListAsync(chunks: List<Chunk>) {
|
||||
plugin.slF4JLogger.info("Exporting ${chunks.size} Chunks")
|
||||
val snapshots = chunks.map { it.chunkSnapshot }
|
||||
Thread {
|
||||
for (snapshot in snapshots) {
|
||||
exportChunkSnapshot(snapshot)
|
||||
}
|
||||
plugin.slF4JLogger.info("Exported ${chunks.size} Chunks")
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun exportChunkSnapshot(snapshot: ChunkSnapshot) {
|
||||
val sections = mutableListOf<ExportedChunkSection>()
|
||||
val yRange = world.minHeight until world.maxHeight
|
||||
val chunkRange = 0..15
|
||||
for (x in chunkRange) {
|
||||
for (z in chunkRange) {
|
||||
sections.add(exportChunkSection(snapshot, yRange, x, z))
|
||||
}
|
||||
}
|
||||
|
||||
val exported = ExportedChunk(snapshot.x, snapshot.z, sections)
|
||||
saveChunkSnapshot(snapshot, exported)
|
||||
}
|
||||
|
||||
private fun saveChunkSnapshot(snapshot: ChunkSnapshot, chunk: ExportedChunk) {
|
||||
val file = File("exported_chunks/${snapshot.worldName}_chunk_${snapshot.x}_${snapshot.z}.json.gz")
|
||||
if (!file.parentFile.exists()) {
|
||||
file.parentFile.mkdirs()
|
||||
}
|
||||
|
||||
val fileOutputStream = file.outputStream()
|
||||
val gzipOutputStream = GZIPOutputStream(fileOutputStream)
|
||||
json.encodeToStream(ExportedChunk.serializer(), chunk, gzipOutputStream)
|
||||
gzipOutputStream.close()
|
||||
}
|
||||
|
||||
private fun exportChunkSection(snapshot: ChunkSnapshot, yRange: IntRange, x: Int, z: Int): ExportedChunkSection {
|
||||
val blocks = mutableListOf<ExportedBlock>()
|
||||
for (y in yRange) {
|
||||
val blockData = snapshot.getBlockData(x, y, z)
|
||||
val block = ExportedBlock(blockData.material.key.toString())
|
||||
blocks.add(block)
|
||||
}
|
||||
return ExportedChunkSection(x, z, blocks)
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package gay.pizza.foundation.heimdall.plugin.export
|
||||
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
import org.bukkit.plugin.Plugin
|
||||
|
||||
class ExportChunksCommand(private val plugin: Plugin) : CommandExecutor {
|
||||
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
|
||||
plugin.slF4JLogger.info("Exporting All Chunks")
|
||||
for (world in sender.server.worlds) {
|
||||
val export = ChunkExporter(plugin, world)
|
||||
export.exportLoadedChunksAsync()
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package gay.pizza.foundation.heimdall.plugin.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class HeimdallConfig(
|
||||
val enabled: Boolean = false,
|
||||
val db: DbConfig
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DbConfig(
|
||||
val url: String,
|
||||
val username: String,
|
||||
val password: String
|
||||
)
|
11
foundation-heimdall/src/main/resources/heimdall.yaml
Normal file
11
foundation-heimdall/src/main/resources/heimdall.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
# Whether Heimdall should be enabled for tracking events.
|
||||
enabled: false
|
||||
|
||||
# Database connection information.
|
||||
db:
|
||||
# JDBC URL
|
||||
url: "jdbc:postgresql://localhost/heimdall"
|
||||
# JDBC Username
|
||||
username: "heimdall"
|
||||
# JDBC Password
|
||||
password: "heimdall"
|
147
foundation-heimdall/src/main/resources/init.sql
Normal file
147
foundation-heimdall/src/main/resources/init.sql
Normal file
@ -0,0 +1,147 @@
|
||||
create extension if not exists "uuid-ossp";
|
||||
--
|
||||
create extension if not exists timescaledb;
|
||||
--
|
||||
create schema if not exists heimdall;
|
||||
--
|
||||
create table if not exists player_positions (
|
||||
time timestamp not null,
|
||||
player uuid not null,
|
||||
world uuid not null,
|
||||
x double precision not null,
|
||||
y double precision not null,
|
||||
z double precision not null,
|
||||
pitch double precision not null,
|
||||
yaw double precision not null,
|
||||
PRIMARY KEY (time, player, world)
|
||||
);
|
||||
--
|
||||
select create_hypertable('player_positions', 'time', 'player', 4, if_not_exists => TRUE);
|
||||
--
|
||||
alter table player_positions set (
|
||||
timescaledb.compress,
|
||||
timescaledb.compress_segmentby = 'player,world',
|
||||
timescaledb.compress_orderby = 'time'
|
||||
);
|
||||
--
|
||||
select add_compression_policy('player_positions', interval '3 days', if_not_exists => true);
|
||||
--
|
||||
create table if not exists block_breaks (
|
||||
time timestamp not null,
|
||||
player uuid not null,
|
||||
world uuid not null,
|
||||
x double precision not null,
|
||||
y double precision not null,
|
||||
z double precision not null,
|
||||
pitch double precision not null,
|
||||
yaw double precision not null,
|
||||
block text not null,
|
||||
PRIMARY KEY (time, player, world)
|
||||
);
|
||||
--
|
||||
select create_hypertable('block_breaks', 'time', 'player', 4, if_not_exists => TRUE);
|
||||
--
|
||||
create table if not exists block_places (
|
||||
time timestamp not null,
|
||||
player uuid not null,
|
||||
world uuid not null,
|
||||
x double precision not null,
|
||||
y double precision not null,
|
||||
z double precision not null,
|
||||
pitch double precision not null,
|
||||
yaw double precision not null,
|
||||
block text not null,
|
||||
PRIMARY KEY (time, player, world)
|
||||
);
|
||||
--
|
||||
select create_hypertable('block_places', 'time', 'player', 4, if_not_exists => TRUE);
|
||||
--
|
||||
create table if not exists player_sessions (
|
||||
id uuid not null,
|
||||
player uuid not null,
|
||||
name text not null,
|
||||
"start" timestamp not null,
|
||||
"end" timestamp not null,
|
||||
primary key (id, player, start)
|
||||
);
|
||||
--
|
||||
select create_hypertable('player_sessions', 'start', 'player', 4, if_not_exists => TRUE);
|
||||
--
|
||||
create table if not exists world_changes (
|
||||
time timestamp not null,
|
||||
player uuid not null,
|
||||
from_world uuid not null,
|
||||
from_world_name text not null,
|
||||
to_world uuid not null,
|
||||
to_world_name text not null,
|
||||
primary key (time, player)
|
||||
);
|
||||
--
|
||||
select create_hypertable('world_changes', 'time', 'player', 4, if_not_exists => TRUE);
|
||||
--
|
||||
create table if not exists player_deaths (
|
||||
time timestamp not null,
|
||||
player uuid not null,
|
||||
world uuid not null,
|
||||
x double precision not null,
|
||||
y double precision not null,
|
||||
z double precision not null,
|
||||
pitch double precision not null,
|
||||
yaw double precision not null,
|
||||
experience double precision not null,
|
||||
message text null,
|
||||
primary key (time, player)
|
||||
);
|
||||
--
|
||||
select create_hypertable('player_deaths', 'time', 'player', 4, if_not_exists => TRUE);
|
||||
--
|
||||
create table if not exists player_advancements (
|
||||
time timestamp not null,
|
||||
player uuid not null,
|
||||
world uuid not null,
|
||||
x double precision not null,
|
||||
y double precision not null,
|
||||
z double precision not null,
|
||||
pitch double precision not null,
|
||||
yaw double precision not null,
|
||||
advancement text not null,
|
||||
primary key (time, player, advancement)
|
||||
);
|
||||
--
|
||||
select create_hypertable('player_advancements', 'time', 'player', 4, if_not_exists => TRUE);
|
||||
--
|
||||
create table if not exists entity_kills (
|
||||
time timestamp not null,
|
||||
player uuid not null,
|
||||
entity uuid not null,
|
||||
world uuid not null,
|
||||
x double precision not null,
|
||||
y double precision not null,
|
||||
z double precision not null,
|
||||
pitch double precision not null,
|
||||
yaw double precision not null,
|
||||
entity_type text not null,
|
||||
primary key (time, entity, player)
|
||||
);
|
||||
--
|
||||
select create_hypertable('entity_kills', 'time', 'player', 4, if_not_exists => TRUE);
|
||||
--
|
||||
create or replace view block_changes as
|
||||
select true as break, *
|
||||
from block_breaks
|
||||
union all
|
||||
select false as break, * from block_places;
|
||||
--
|
||||
create or replace view player_names as
|
||||
with unique_player_ids as (
|
||||
select distinct player
|
||||
from player_sessions
|
||||
)
|
||||
select player, (
|
||||
select name
|
||||
from player_sessions
|
||||
where player = unique_player_ids.player
|
||||
order by "end" desc
|
||||
limit 1
|
||||
) as name
|
||||
from unique_player_ids;
|
13
foundation-heimdall/src/main/resources/plugin.yml
Normal file
13
foundation-heimdall/src/main/resources/plugin.yml
Normal file
@ -0,0 +1,13 @@
|
||||
name: Heimdall
|
||||
version: '${version}'
|
||||
main: io.kexec.heimdall.plugin.HeimdallPlugin
|
||||
api-version: 1.18
|
||||
prefix: Heimdall
|
||||
load: STARTUP
|
||||
authors:
|
||||
- kendfinger
|
||||
commands:
|
||||
export_all_chunks:
|
||||
description: Export All Chunks
|
||||
usage: /export_all_chunks
|
||||
permission: heimdall.command.export_all_chunks
|
@ -0,0 +1,64 @@
|
||||
WITH
|
||||
unique_player_ids AS (
|
||||
SELECT
|
||||
DISTINCT player
|
||||
FROM player_sessions
|
||||
),
|
||||
player_names AS (
|
||||
SELECT
|
||||
player,
|
||||
(
|
||||
SELECT name
|
||||
FROM 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 world_changes
|
||||
),
|
||||
world_names AS (
|
||||
SELECT
|
||||
world,
|
||||
(
|
||||
SELECT to_world_name
|
||||
FROM world_changes
|
||||
WHERE world = 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 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
|
@ -9,7 +9,10 @@ pluginManagement {
|
||||
}
|
||||
|
||||
include(
|
||||
":common-heimdall",
|
||||
":foundation-core",
|
||||
":foundation-bifrost",
|
||||
":foundation-chaos",
|
||||
":foundation-heimdall",
|
||||
":tool-gjallarhorn",
|
||||
)
|
||||
|
21
tool-gjallarhorn/build.gradle.kts
Normal file
21
tool-gjallarhorn/build.gradle.kts
Normal file
@ -0,0 +1,21 @@
|
||||
plugins {
|
||||
id("gay.pizza.foundation.concrete-library")
|
||||
id("com.github.johnrengelman.shadow")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":common-heimdall"))
|
||||
|
||||
implementation("com.github.ajalt.clikt:clikt:3.5.0")
|
||||
implementation("org.slf4j:slf4j-simple:1.7.36")
|
||||
}
|
||||
|
||||
tasks.jar {
|
||||
manifest.attributes(
|
||||
"Main-Class" to "io.kexec.heimdall.tool.MainKt"
|
||||
)
|
||||
}
|
||||
|
||||
tasks.assemble {
|
||||
dependsOn("shadowJar")
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package gay.pizza.foundation.heimdall.tool
|
||||
|
||||
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/heimdall")
|
||||
|
||||
private val jdbcConnectionUsername by option("-u", "--connection-username", help = "JDBC Connection Username")
|
||||
.default("heimdall")
|
||||
|
||||
private val jdbcConnectionPassword by option("-p", "--connection-password", help = "JDBC Connection Password")
|
||||
.default("heimdall")
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
@ -0,0 +1,156 @@
|
||||
package gay.pizza.foundation.heimdall.tool.commands
|
||||
|
||||
import gay.pizza.foundation.heimdall.tool.render.*
|
||||
import gay.pizza.foundation.heimdall.tool.state.*
|
||||
import gay.pizza.foundation.heimdall.tool.util.compose
|
||||
import gay.pizza.foundation.heimdall.tool.util.savePngFile
|
||||
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.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 renderImageFormat by option("--render-image-format", help = "Render Image Format")
|
||||
.enum<ImageFormatType> { it.id }
|
||||
.default(ImageFormatType.Png)
|
||||
|
||||
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 { gay.pizza.foundation.heimdall.view.BlockChangeView.x greaterEq trim!!.first.x },
|
||||
{ trim?.first?.z != null } to { gay.pizza.foundation.heimdall.view.BlockChangeView.z greaterEq trim!!.first.z },
|
||||
{ trim?.second?.x != null } to { gay.pizza.foundation.heimdall.view.BlockChangeView.x lessEq trim!!.second.x },
|
||||
{ trim?.second?.z != null } to { gay.pizza.foundation.heimdall.view.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')}"
|
||||
renderImageFormat.save(result, "${render.id}${suffix}.${renderImageFormat.extension}")
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package gay.pizza.foundation.heimdall.tool.commands
|
||||
|
||||
import gay.pizza.foundation.heimdall.tool.export.ChunkExportLoader
|
||||
import gay.pizza.foundation.heimdall.tool.export.CombinedChunkFormat
|
||||
import gay.pizza.foundation.heimdall.tool.state.BlockExpanse
|
||||
import gay.pizza.foundation.heimdall.tool.state.BlockLogTracker
|
||||
import gay.pizza.foundation.heimdall.tool.state.ChangelogSlice
|
||||
import gay.pizza.foundation.heimdall.tool.util.savePngFile
|
||||
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 kotlinx.serialization.ExperimentalSerializationApi
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package gay.pizza.foundation.heimdall.tool.commands
|
||||
|
||||
import gay.pizza.foundation.heimdall.tool.util.saveJpegFile
|
||||
import gay.pizza.foundation.heimdall.tool.util.savePngFile
|
||||
import java.awt.image.BufferedImage
|
||||
|
||||
enum class ImageFormatType(val id: String, val extension: String, val save: (BufferedImage, String) -> Unit) {
|
||||
Png("png", "png", { image, path -> image.savePngFile(path) }),
|
||||
Jpeg("jpeg", "jpg", { image, path -> image.saveJpegFile(path) })
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package gay.pizza.foundation.heimdall.tool.commands
|
||||
|
||||
import gay.pizza.foundation.heimdall.tool.render.*
|
||||
import gay.pizza.foundation.heimdall.tool.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) })
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package gay.pizza.foundation.heimdall.tool.commands
|
||||
|
||||
import gay.pizza.foundation.heimdall.tool.state.PlayerPositionChangelog
|
||||
import gay.pizza.foundation.heimdall.tool.util.compose
|
||||
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.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.*
|
||||
|
||||
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 { gay.pizza.foundation.heimdall.table.PlayerPositionTable.time greaterEq Instant.parse(startTimeString) },
|
||||
{ endTimeString != null } to { gay.pizza.foundation.heimdall.table.PlayerPositionTable.time lessEq Instant.parse(endTimeString) },
|
||||
{ playerIdString != null } to { gay.pizza.foundation.heimdall.table.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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package gay.pizza.foundation.heimdall.tool.commands
|
||||
|
||||
import gay.pizza.foundation.heimdall.tool.util.compose
|
||||
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.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.*
|
||||
|
||||
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 { gay.pizza.foundation.heimdall.table.PlayerSessionTable.player eq UUID.fromString(playerIdString) },
|
||||
{ playerNameString != null } to { gay.pizza.foundation.heimdall.table.PlayerSessionTable.name eq playerNameString!! }
|
||||
)
|
||||
|
||||
println("id,player,name,start,end")
|
||||
transaction(db) {
|
||||
gay.pizza.foundation.heimdall.table.PlayerSessionTable.select(filter).orderBy(gay.pizza.foundation.heimdall.table.PlayerSessionTable.endTime).forEach { row ->
|
||||
val id = row[gay.pizza.foundation.heimdall.table.PlayerSessionTable.id]
|
||||
val player = row[gay.pizza.foundation.heimdall.table.PlayerSessionTable.player]
|
||||
val name = row[gay.pizza.foundation.heimdall.table.PlayerSessionTable.name]
|
||||
val start = row[gay.pizza.foundation.heimdall.table.PlayerSessionTable.startTime]
|
||||
val end = row[gay.pizza.foundation.heimdall.table.PlayerSessionTable.endTime]
|
||||
|
||||
println("${id},${player},${name},${start},${end}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package gay.pizza.foundation.heimdall.tool.export
|
||||
|
||||
import gay.pizza.foundation.heimdall.export.ExportedChunk
|
||||
import gay.pizza.foundation.heimdall.tool.state.BlockCoordinate
|
||||
import gay.pizza.foundation.heimdall.tool.state.BlockLogTracker
|
||||
import gay.pizza.foundation.heimdall.tool.state.BlockState
|
||||
import gay.pizza.foundation.heimdall.tool.state.SparseBlockStateMap
|
||||
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(gay.pizza.foundation.heimdall.export.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)
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package gay.pizza.foundation.heimdall.tool.export
|
||||
|
||||
import gay.pizza.foundation.heimdall.tool.state.BlockExpanse
|
||||
import gay.pizza.foundation.heimdall.tool.state.SparseBlockStateMap
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class CombinedChunkFormat(
|
||||
val expanse: BlockExpanse,
|
||||
val map: SparseBlockStateMap
|
||||
)
|
@ -0,0 +1,14 @@
|
||||
package gay.pizza.foundation.heimdall.tool
|
||||
|
||||
import gay.pizza.foundation.heimdall.tool.commands.BlockChangeTimelapseCommand
|
||||
import gay.pizza.foundation.heimdall.tool.commands.ChunkExportLoaderCommand
|
||||
import gay.pizza.foundation.heimdall.tool.commands.PlayerPositionExport
|
||||
import gay.pizza.foundation.heimdall.tool.commands.PlayerSessionExport
|
||||
import com.github.ajalt.clikt.core.subcommands
|
||||
|
||||
fun main(args: Array<String>) = GjallarhornCommand().subcommands(
|
||||
BlockChangeTimelapseCommand(),
|
||||
PlayerSessionExport(),
|
||||
PlayerPositionExport(),
|
||||
ChunkExportLoaderCommand()
|
||||
).main(args)
|
@ -0,0 +1,30 @@
|
||||
package gay.pizza.foundation.heimdall.tool.render
|
||||
|
||||
import gay.pizza.foundation.heimdall.tool.state.BlockExpanse
|
||||
import gay.pizza.foundation.heimdall.tool.state.BlockStateMap
|
||||
import gay.pizza.foundation.heimdall.tool.state.ChangelogSlice
|
||||
import gay.pizza.foundation.heimdall.tool.util.BlockColorKey
|
||||
import gay.pizza.foundation.heimdall.tool.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)
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package gay.pizza.foundation.heimdall.tool.render
|
||||
|
||||
import gay.pizza.foundation.heimdall.tool.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
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package gay.pizza.foundation.heimdall.tool.render
|
||||
|
||||
import gay.pizza.foundation.heimdall.tool.state.BlockExpanse
|
||||
import gay.pizza.foundation.heimdall.tool.util.ColorGradient
|
||||
import gay.pizza.foundation.heimdall.tool.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)
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package gay.pizza.foundation.heimdall.tool.render
|
||||
|
||||
import gay.pizza.foundation.heimdall.tool.state.BlockExpanse
|
||||
import gay.pizza.foundation.heimdall.tool.state.BlockStateMap
|
||||
import gay.pizza.foundation.heimdall.tool.state.SparseBlockStateMap
|
||||
import gay.pizza.foundation.heimdall.tool.state.ChangelogSlice
|
||||
import gay.pizza.foundation.heimdall.tool.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 } }
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package gay.pizza.foundation.heimdall.tool.render
|
||||
|
||||
import java.awt.image.BufferedImage
|
||||
|
||||
interface BlockImageRenderer : BlockMapRenderer<BufferedImage>
|
@ -0,0 +1,8 @@
|
||||
package gay.pizza.foundation.heimdall.tool.render
|
||||
|
||||
import gay.pizza.foundation.heimdall.tool.state.BlockStateMap
|
||||
import gay.pizza.foundation.heimdall.tool.state.ChangelogSlice
|
||||
|
||||
interface BlockMapRenderer<T> {
|
||||
fun render(slice: ChangelogSlice, map: BlockStateMap): T
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package gay.pizza.foundation.heimdall.tool.render
|
||||
|
||||
import gay.pizza.foundation.heimdall.tool.state.BlockExpanse
|
||||
import gay.pizza.foundation.heimdall.tool.state.BlockStateMap
|
||||
import gay.pizza.foundation.heimdall.tool.state.SparseBlockStateMap
|
||||
import gay.pizza.foundation.heimdall.tool.state.ChangelogSlice
|
||||
import gay.pizza.foundation.heimdall.tool.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 } }
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package gay.pizza.foundation.heimdall.tool.render
|
||||
|
||||
import gay.pizza.foundation.heimdall.tool.render.ui.GraphicalRenderSession
|
||||
import gay.pizza.foundation.heimdall.tool.state.BlockExpanse
|
||||
import gay.pizza.foundation.heimdall.tool.state.BlockStateMap
|
||||
import gay.pizza.foundation.heimdall.tool.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)
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package gay.pizza.foundation.heimdall.tool.render
|
||||
|
||||
import gay.pizza.foundation.heimdall.table.PlayerPositionTable
|
||||
import gay.pizza.foundation.heimdall.tool.state.*
|
||||
import gay.pizza.foundation.heimdall.tool.util.BlockColorKey
|
||||
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.*
|
||||
|
||||
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) {
|
||||
gay.pizza.foundation.heimdall.table.PlayerPositionTable.select {
|
||||
(gay.pizza.foundation.heimdall.table.PlayerPositionTable.time greater start) and
|
||||
(gay.pizza.foundation.heimdall.table.PlayerPositionTable.time lessEq end)
|
||||
}.forEach {
|
||||
val x = it[gay.pizza.foundation.heimdall.table.PlayerPositionTable.x].toLong()
|
||||
val y = it[gay.pizza.foundation.heimdall.table.PlayerPositionTable.y].toLong()
|
||||
val z = it[gay.pizza.foundation.heimdall.table.PlayerPositionTable.z].toLong()
|
||||
val coordinate = expanse.offset.applyAsOffset(BlockCoordinate(x, y, z))
|
||||
val player = it[gay.pizza.foundation.heimdall.table.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package gay.pizza.foundation.heimdall.tool.render.ui
|
||||
|
||||
import gay.pizza.foundation.heimdall.tool.render.BlockDiversityRenderer
|
||||
import gay.pizza.foundation.heimdall.tool.render.BlockHeightMapRenderer
|
||||
import gay.pizza.foundation.heimdall.tool.render.BlockVerticalFillMapRenderer
|
||||
import gay.pizza.foundation.heimdall.tool.state.BlockExpanse
|
||||
import gay.pizza.foundation.heimdall.tool.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)
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package gay.pizza.foundation.heimdall.tool.render.ui
|
||||
|
||||
import gay.pizza.foundation.heimdall.tool.render.BlockImageRenderer
|
||||
import gay.pizza.foundation.heimdall.tool.state.BlockStateMap
|
||||
import gay.pizza.foundation.heimdall.tool.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)
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package gay.pizza.foundation.heimdall.tool.state
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
data class BlockChange(
|
||||
val time: Instant,
|
||||
val type: BlockChangeType,
|
||||
val location: BlockCoordinate,
|
||||
val from: BlockState,
|
||||
val to: BlockState
|
||||
)
|
@ -0,0 +1,9 @@
|
||||
package gay.pizza.foundation.heimdall.tool.state
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class BlockChangeType {
|
||||
Place,
|
||||
Break
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
package gay.pizza.foundation.heimdall.tool.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 timeSlice = fullTimeSlice
|
||||
val start = timeSlice.rootStartTime
|
||||
val end = timeSlice.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(gay.pizza.foundation.heimdall.view.BlockChangeView.select(filter).orderBy(gay.pizza.foundation.heimdall.view.BlockChangeView.time).map { row ->
|
||||
val time = row[gay.pizza.foundation.heimdall.view.BlockChangeView.time]
|
||||
val changeIsBreak = row[gay.pizza.foundation.heimdall.view.BlockChangeView.isBreak]
|
||||
val x = row[gay.pizza.foundation.heimdall.view.BlockChangeView.x]
|
||||
val y = row[gay.pizza.foundation.heimdall.view.BlockChangeView.y]
|
||||
val z = row[gay.pizza.foundation.heimdall.view.BlockChangeView.z]
|
||||
val block = row[gay.pizza.foundation.heimdall.view.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
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package gay.pizza.foundation.heimdall.tool.state
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.*
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package gay.pizza.foundation.heimdall.tool.state
|
||||
|
||||
import gay.pizza.foundation.heimdall.tool.util.maxOfAll
|
||||
import gay.pizza.foundation.heimdall.tool.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)
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package gay.pizza.foundation.heimdall.tool.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)
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package gay.pizza.foundation.heimdall.tool.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)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package gay.pizza.foundation.heimdall.tool.state
|
||||
|
||||
import gay.pizza.foundation.heimdall.tool.util.maxOfAll
|
||||
import gay.pizza.foundation.heimdall.tool.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]
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package gay.pizza.foundation.heimdall.tool.state
|
||||
|
||||
import gay.pizza.foundation.heimdall.tool.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)
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package gay.pizza.foundation.heimdall.tool.state
|
||||
|
||||
interface BlockMapRenderPoolDelegate<T> {
|
||||
fun onSinglePlaybackComplete(pool: BlockMapRenderPool<T>, slice: ChangelogSlice, tracker: BlockLogTracker)
|
||||
fun onAllPlaybackComplete(pool: BlockMapRenderPool<T>, trackers: Map<ChangelogSlice, BlockLogTracker>)
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package gay.pizza.foundation.heimdall.tool.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package gay.pizza.foundation.heimdall.tool.state
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@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) }
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package gay.pizza.foundation.heimdall.tool.state
|
||||
|
||||
typealias BlockStateMap = BlockCoordinateStore<BlockState>
|
@ -0,0 +1,20 @@
|
||||
package gay.pizza.foundation.heimdall.tool.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)
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package gay.pizza.foundation.heimdall.tool.state
|
||||
|
||||
enum class BlockTrackMode {
|
||||
RemoveOnDelete,
|
||||
AirOnDelete
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package gay.pizza.foundation.heimdall.tool.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)
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package gay.pizza.foundation.heimdall.tool.state
|
||||
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
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
|
||||
)
|
@ -0,0 +1,28 @@
|
||||
package gay.pizza.foundation.heimdall.tool.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(gay.pizza.foundation.heimdall.table.PlayerPositionTable.select(filter).orderBy(gay.pizza.foundation.heimdall.table.PlayerPositionTable.time).map { row ->
|
||||
val time = row[gay.pizza.foundation.heimdall.table.PlayerPositionTable.time]
|
||||
val player = row[gay.pizza.foundation.heimdall.table.PlayerPositionTable.player]
|
||||
val world = row[gay.pizza.foundation.heimdall.table.PlayerPositionTable.world]
|
||||
val x = row[gay.pizza.foundation.heimdall.table.PlayerPositionTable.x]
|
||||
val y = row[gay.pizza.foundation.heimdall.table.PlayerPositionTable.y]
|
||||
val z = row[gay.pizza.foundation.heimdall.table.PlayerPositionTable.z]
|
||||
val pitch = row[gay.pizza.foundation.heimdall.table.PlayerPositionTable.z]
|
||||
val yaw = row[gay.pizza.foundation.heimdall.table.PlayerPositionTable.z]
|
||||
|
||||
PlayerPositionChange(time, player, world, x, y, z, pitch, yaw)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package gay.pizza.foundation.heimdall.tool.state
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable(SparseBlockStateMapSerializer::class)
|
||||
class SparseBlockStateMap(blocks: Map<Long, Map<Long, Map<Long, BlockState>>> = mutableMapOf()) :
|
||||
BlockCoordinateSparseMap<BlockState>(blocks)
|
@ -0,0 +1,23 @@
|
||||
package gay.pizza.foundation.heimdall.tool.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)
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package gay.pizza.foundation.heimdall.tool.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())
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package gay.pizza.foundation.heimdall.tool.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")
|
||||
)
|
@ -0,0 +1,57 @@
|
||||
package gay.pizza.foundation.heimdall.tool.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)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package gay.pizza.foundation.heimdall.tool.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()
|
||||
)
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package gay.pizza.foundation.heimdall.tool.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)
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package gay.pizza.foundation.heimdall.tool.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.")
|
||||
}
|
||||
}
|
||||
|
||||
fun BufferedImage.saveJpegFile(path: String) {
|
||||
if (!ImageIO.write(this, "jpeg", File(path))) {
|
||||
throw RuntimeException("Unable to write JPEG.")
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package gay.pizza.foundation.heimdall.tool.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)
|
@ -0,0 +1,39 @@
|
||||
package gay.pizza.foundation.heimdall.tool.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)
|
@ -7,7 +7,7 @@ mkdir -p artifacts/
|
||||
mkdir -p artifacts/build/manifests
|
||||
cp build/manifests/update.json artifacts/build/manifests/
|
||||
|
||||
find . -name "*-plugin.jar" | while read -r jar
|
||||
find . -name "*-plugin.jar" | grep "foundation-" | while read -r jar
|
||||
do
|
||||
DN=`dirname ${jar}`
|
||||
mkdir -p "artifacts/$DN"
|
||||
|
Loading…
Reference in New Issue
Block a user