mirror of
				https://github.com/GayPizzaSpecifications/foundation.git
				synced 2025-11-04 03:39:37 +00:00 
			
		
		
		
	Heimdall: It's back!
This commit is contained in:
		@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 | 
				
			|||||||
plugins {
 | 
					plugins {
 | 
				
			||||||
  java
 | 
					  java
 | 
				
			||||||
  id("gay.pizza.foundation.concrete-root") version "0.7.0"
 | 
					  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
 | 
					  id("gay.pizza.foundation.concrete-plugin") version "0.7.0" apply false
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -53,7 +54,7 @@ subprojects {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  tasks.withType<KotlinCompile> {
 | 
					  tasks.withType<KotlinCompile> {
 | 
				
			||||||
    kotlinOptions {
 | 
					    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(
 | 
					include(
 | 
				
			||||||
 | 
					  ":common-heimdall",
 | 
				
			||||||
  ":foundation-core",
 | 
					  ":foundation-core",
 | 
				
			||||||
  ":foundation-bifrost",
 | 
					  ":foundation-bifrost",
 | 
				
			||||||
  ":foundation-chaos",
 | 
					  ":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
 | 
					mkdir -p artifacts/build/manifests
 | 
				
			||||||
cp build/manifests/update.json 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
 | 
					do
 | 
				
			||||||
  DN=`dirname ${jar}`
 | 
					  DN=`dirname ${jar}`
 | 
				
			||||||
  mkdir -p "artifacts/$DN"
 | 
					  mkdir -p "artifacts/$DN"
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user