Remove heimdall and tool project.

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

View File

@ -2,8 +2,8 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
java
id("gay.pizza.foundation.concrete-root") version "0.6.0-SNAPSHOT"
id("gay.pizza.foundation.concrete-plugin") version "0.6.0-SNAPSHOT" apply false
id("gay.pizza.foundation.concrete-root") version "0.6.0"
id("gay.pizza.foundation.concrete-plugin") version "0.6.0" apply false
}
allprojects {
@ -60,7 +60,7 @@ subprojects {
concrete {
minecraftServerPath.set("server")
paperVersionGroup.set("1.18")
paperServerVersionGroup.set("1.18")
paperApiVersion.set("1.18.2-R0.1-SNAPSHOT")
acceptServerEula.set(true)
}

View File

@ -1,7 +0,0 @@
dependencies {
api("org.postgresql:postgresql:42.3.1")
api("org.jetbrains.exposed:exposed-jdbc:0.36.2")
api("org.jetbrains.exposed:exposed-java-time:0.36.2")
api("com.zaxxer:HikariCP:5.0.0")
compileOnly(project(":foundation-core"))
}

View File

@ -1,21 +0,0 @@
package gay.pizza.foundation.heimdall
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
}

View File

@ -1,183 +0,0 @@
package gay.pizza.foundation.heimdall
import com.charleskorn.kaml.Yaml
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import gay.pizza.foundation.core.FoundationCorePlugin
import gay.pizza.foundation.core.Util
import gay.pizza.foundation.heimdall.buffer.BufferFlushThread
import gay.pizza.foundation.heimdall.buffer.EventBuffer
import gay.pizza.foundation.heimdall.event.BlockBreak
import gay.pizza.foundation.heimdall.event.BlockPlace
import gay.pizza.foundation.heimdall.event.EntityKill
import gay.pizza.foundation.heimdall.event.PlayerAdvancement
import gay.pizza.foundation.heimdall.event.PlayerDeath
import gay.pizza.foundation.heimdall.event.PlayerPosition
import gay.pizza.foundation.heimdall.event.PlayerSession
import gay.pizza.foundation.heimdall.event.WorldChange
import gay.pizza.foundation.heimdall.export.ExportChunksCommand
import gay.pizza.foundation.heimdall.model.HeimdallConfig
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.PlayerAdvancementDoneEvent
import org.bukkit.event.player.PlayerChangedWorldEvent
import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.event.player.PlayerMoveEvent
import org.bukkit.event.player.PlayerQuitEvent
import org.bukkit.plugin.java.JavaPlugin
import org.jetbrains.exposed.sql.Database
import org.postgresql.Driver
import java.time.Duration
import java.time.Instant
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import kotlin.io.path.inputStream
class FoundationHeimdallPlugin : 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 foundation = server.pluginManager.getPlugin("Foundation") as FoundationCorePlugin
val configPath = Util.copyDefaultConfig<FoundationHeimdallPlugin>(
slF4JLogger,
foundation.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
schema = "heimdall"
maximumPoolSize = 10
idleTimeout = Duration.ofMinutes(5).toMillis()
maxLifetime = Duration.ofMinutes(10).toMillis()
})
val initMigrationContent = FoundationHeimdallPlugin::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()
}
}

View File

@ -1,49 +0,0 @@
package gay.pizza.foundation.heimdall.buffer
import gay.pizza.foundation.heimdall.FoundationHeimdallPlugin
import org.jetbrains.exposed.sql.transactions.transaction
import java.util.concurrent.atomic.AtomicBoolean
class BufferFlushThread(val plugin: FoundationHeimdallPlugin, 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)
}
}
}

View File

@ -1,28 +0,0 @@
package gay.pizza.foundation.heimdall.buffer
import gay.pizza.foundation.heimdall.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()
}
}

View File

@ -1,35 +0,0 @@
package gay.pizza.foundation.heimdall.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.UUID
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()
}
}
}
}

View File

@ -1,35 +0,0 @@
package gay.pizza.foundation.heimdall.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.UUID
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()
}
}
}
}

View File

@ -1,33 +0,0 @@
package gay.pizza.foundation.heimdall.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.UUID
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
}
}
}
}

View File

@ -1,7 +0,0 @@
package gay.pizza.foundation.heimdall.event
import org.jetbrains.exposed.sql.Transaction
abstract class HeimdallEvent {
abstract fun store(transaction: Transaction)
}

View File

@ -1,35 +0,0 @@
package gay.pizza.foundation.heimdall.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.UUID
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()
}
}
}
}

View File

@ -1,41 +0,0 @@
package gay.pizza.foundation.heimdall.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.UUID
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
}
}
}
}

View File

@ -1,32 +0,0 @@
package gay.pizza.foundation.heimdall.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.UUID
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()
}
}
}
}

View File

@ -1,26 +0,0 @@
package gay.pizza.foundation.heimdall.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.UUID
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
}
}
}
}

View File

@ -1,29 +0,0 @@
package gay.pizza.foundation.heimdall.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.UUID
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
}
}
}
}

View File

@ -1,68 +0,0 @@
package gay.pizza.foundation.heimdall.export
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.encodeToStream
import org.bukkit.Chunk
import org.bukkit.ChunkSnapshot
import org.bukkit.Server
import org.bukkit.World
import org.bukkit.plugin.Plugin
import java.io.File
import java.util.zip.GZIPOutputStream
class ChunkExporter(private val plugin: Plugin, private val server: Server, 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)
}
}

View File

@ -1,17 +0,0 @@
package gay.pizza.foundation.heimdall.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, sender.server, world)
export.exportLoadedChunksAsync()
}
return true
}
}

View File

@ -1,8 +0,0 @@
package gay.pizza.foundation.heimdall.export
import kotlinx.serialization.Serializable
@Serializable
data class ExportedBlock(
val type: String
)

View File

@ -1,10 +0,0 @@
package gay.pizza.foundation.heimdall.export
import kotlinx.serialization.Serializable
@Serializable
data class ExportedChunk(
val x: Int,
val z: Int,
val sections: List<ExportedChunkSection>
)

View File

@ -1,10 +0,0 @@
package gay.pizza.foundation.heimdall.export
import kotlinx.serialization.Serializable
@Serializable
data class ExportedChunkSection(
val x: Int,
val z: Int,
val blocks: List<ExportedBlock>
)

View File

@ -1,16 +0,0 @@
package gay.pizza.foundation.heimdall.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
)

View File

@ -1,16 +0,0 @@
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")
}

View File

@ -1,16 +0,0 @@
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")
}

View File

@ -1,17 +0,0 @@
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")
}

View File

@ -1,16 +0,0 @@
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")
}

View File

@ -1,17 +0,0 @@
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()
}

View File

@ -1,15 +0,0 @@
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")
}

View File

@ -1,12 +0,0 @@
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")
}

View File

@ -1,13 +0,0 @@
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")
}

View File

@ -1,17 +0,0 @@
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")
}

View File

@ -1,11 +0,0 @@
# Whether Heimdall should be enabled for tracking events.
enabled: false
# Database connection information.
db:
# JDBC URL
url: "jdbc:postgresql://localhost/foundation"
# JDBC Username
username: "foundation"
# JDBC Password
password: "foundation"

View File

@ -1,147 +0,0 @@
create extension if not exists "uuid-ossp";
--
create extension if not exists timescaledb;
--
create schema if not exists heimdall;
--
create table if not exists heimdall.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('heimdall.player_positions', 'time', 'player', 4, if_not_exists => TRUE);
--
alter table heimdall.player_positions set (
timescaledb.compress,
timescaledb.compress_segmentby = 'player,world',
timescaledb.compress_orderby = 'time'
);
--
select add_compression_policy('heimdall.player_positions', interval '3 days', if_not_exists => true);
--
create table if not exists heimdall.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('heimdall.block_breaks', 'time', 'player', 4, if_not_exists => TRUE);
--
create table if not exists heimdall.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('heimdall.block_places', 'time', 'player', 4, if_not_exists => TRUE);
--
create table if not exists heimdall.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('heimdall.player_sessions', 'start', 'player', 4, if_not_exists => TRUE);
--
create table if not exists heimdall.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('heimdall.world_changes', 'time', 'player', 4, if_not_exists => TRUE);
--
create table if not exists heimdall.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('heimdall.player_deaths', 'time', 'player', 4, if_not_exists => TRUE);
--
create table if not exists heimdall.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('heimdall.player_advancements', 'time', 'player', 4, if_not_exists => TRUE);
--
create table if not exists heimdall.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('heimdall.entity_kills', 'time', 'player', 4, if_not_exists => TRUE);
--
create or replace view heimdall.block_changes as
select true as break, *
from heimdall.block_breaks
union all
select false as break, * from heimdall.block_places;
--
create or replace view heimdall.player_names as
with unique_player_ids as (
select distinct player
from heimdall.player_sessions
)
select player, (
select name
from heimdall.player_sessions
where player = unique_player_ids.player
order by "end" desc
limit 1
) as name
from unique_player_ids;

View File

@ -1,15 +0,0 @@
name: Foundation-Heimdall
version: '${version}'
main: gay.pizza.foundation.heimdall.FoundationHeimdallPlugin
api-version: 1.18
prefix: Foundation-Heimdall
load: STARTUP
depend:
- Foundation
authors:
- kubelet
commands:
export_all_chunks:
description: Export All Chunks
usage: /export_all_chunks
permission: foundation.heimdall.command.export_all_chunks

View File

@ -11,6 +11,4 @@ pluginManagement {
include(
":foundation-core",
":foundation-bifrost",
":foundation-heimdall",
":tool-gjallarhorn",
)

View File

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

View File

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

View File

@ -1,153 +0,0 @@
package gay.pizza.foundation.gjallarhorn.commands
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.requireObject
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import com.github.ajalt.clikt.parameters.types.enum
import com.github.ajalt.clikt.parameters.types.int
import gay.pizza.foundation.gjallarhorn.render.*
import gay.pizza.foundation.gjallarhorn.state.*
import gay.pizza.foundation.gjallarhorn.util.compose
import gay.pizza.foundation.gjallarhorn.util.savePngFile
import gay.pizza.foundation.heimdall.view.BlockChangeView
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greaterEq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq
import org.jetbrains.exposed.sql.and
import org.slf4j.LoggerFactory
import java.awt.Color
import java.awt.Font
import java.awt.font.TextLayout
import java.awt.image.BufferedImage
import java.time.Duration
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ScheduledThreadPoolExecutor
class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name = "block-change-timelapse") {
private val db by requireObject<Database>()
private val timelapseIntervalLimit by option("--timelapse-limit", help = "Timelapse Limit Intervals").int()
private val timelapseMode by option("--timelapse", help = "Timelapse Mode").enum<TimelapseMode> { it.id }.required()
private val timelapseSpeedChangeThreshold by option(
"--timelapse-change-speed-threshold",
help = "Timelapse Change Speed Threshold"
).int()
private val timelapseSpeedChangeMinimumIntervalSeconds by option(
"--timelapse-change-speed-minimum-interval-seconds",
help = "Timelapse Change Speed Minimum Interval Seconds"
).int()
private val render by option("--render", help = "Render Top Down Image").enum<ImageRenderType> { it.id }.required()
private val considerAirBlocks by option("--consider-air-blocks", help = "Enable Air Block Consideration").flag()
private val fromCoordinate by option("--trim-from", help = "Trim From Coordinate")
private val toCoordinate by option("--trim-to", help = "Trim To Coordinate")
private val parallelPoolSize by option("--pool-size", help = "Task Pool Size").int().default(8)
private val inMemoryRender by option("--in-memory-render", help = "Render Images to Memory").flag()
private val shouldRenderLoop by option("--loop-render", help = "Loop Render").flag()
private val quadPixelNoop by option("--quad-pixel-noop", help = "Disable Quad Pixel Render").flag()
private val logger = LoggerFactory.getLogger(BlockChangeTimelapseCommand::class.java)
override fun run() {
if (quadPixelNoop) {
BlockGridRenderer.globalQuadPixelNoop = true
}
val threadPoolExecutor = ScheduledThreadPoolExecutor(parallelPoolSize)
if (shouldRenderLoop) {
while (true) {
perform(threadPoolExecutor)
}
} else {
perform(threadPoolExecutor)
}
threadPoolExecutor.shutdown()
}
private fun perform(threadPoolExecutor: ScheduledThreadPoolExecutor) {
val trim = maybeBuildTrim()
val filter = compose(
combine = { a, b -> a and b },
{ trim?.first?.x != null } to { BlockChangeView.x greaterEq trim!!.first.x },
{ trim?.first?.z != null } to { BlockChangeView.z greaterEq trim!!.first.z },
{ trim?.second?.x != null } to { BlockChangeView.x lessEq trim!!.second.x },
{ trim?.second?.z != null } to { BlockChangeView.z lessEq trim!!.second.z }
)
val changelog = BlockChangelog.query(db, filter)
logger.info("Block Changelog: ${changelog.changes.size} changes")
val timelapse = BlockMapTimelapse<BufferedImage>()
var slices = changelog.calculateChangelogSlices(timelapseMode.interval, timelapseIntervalLimit)
if (timelapseSpeedChangeThreshold != null && timelapseSpeedChangeMinimumIntervalSeconds != null) {
val minimumInterval = Duration.ofSeconds(timelapseSpeedChangeMinimumIntervalSeconds!!.toLong())
val blockChangeThreshold = timelapseSpeedChangeThreshold!!
slices = changelog.splitChangelogSlicesWithThreshold(blockChangeThreshold, minimumInterval, slices)
}
logger.info("Timelapse Slices: ${slices.size} slices")
val imagePadCount = slices.size.toString().length
val inMemoryPool = if (inMemoryRender) {
ConcurrentHashMap<ChangelogSlice, BufferedImage>()
} else {
null
}
val pool = BlockMapRenderPool(
changelog = changelog,
blockTrackMode = if (considerAirBlocks) BlockTrackMode.AirOnDelete else BlockTrackMode.RemoveOnDelete,
delegate = timelapse,
createRendererFunction = { expanse -> render.createNewRenderer(expanse, db) },
threadPoolExecutor = threadPoolExecutor
) { slice, result ->
val speed = slice.sliceRelativeDuration.toSeconds().toDouble() / timelapseMode.interval.toSeconds().toDouble()
val graphics = result.createGraphics()
val font = Font.decode("Arial Black").deriveFont(24.0f)
graphics.color = Color.black
graphics.font = font
val context = graphics.fontRenderContext
val text = String.format("%s @ %.4f speed (1 frame = %s sec)", slice.sliceEndTime, speed, slice.sliceRelativeDuration.toSeconds())
val layout =
TextLayout(text, font, context)
layout.draw(graphics, 60f, 60f)
graphics.dispose()
val index = slices.indexOf(slice) + 1
if (inMemoryRender) {
inMemoryPool?.put(slice, result)
} else {
val suffix = "-${index.toString().padStart(imagePadCount, '0')}"
result.savePngFile("${render.id}${suffix}.png")
}
logger.info("Rendered Timelapse Slice $index")
}
pool.render(slices)
}
private fun maybeBuildTrim(): Pair<BlockCoordinate, BlockCoordinate>? {
if (fromCoordinate == null || toCoordinate == null) {
return null
}
val from = fromCoordinate!!.split(",").map { it.toLong() }
val to = toCoordinate!!.split(",").map { it.toLong() }
val fromBlock = BlockCoordinate(from[0], 0, from[1])
val toBlock = BlockCoordinate(to[0], 0, to[1])
return fromBlock to toBlock
}
@Suppress("unused")
enum class TimelapseMode(val id: String, val interval: Duration) {
ByHour("hours", Duration.ofHours(1)),
ByDay("days", Duration.ofDays(1)),
ByFifteenMinutes("fifteen-minutes", Duration.ofMinutes(15))
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,60 +0,0 @@
package gay.pizza.foundation.gjallarhorn.render
import gay.pizza.foundation.gjallarhorn.state.BlockCoordinate
import gay.pizza.foundation.gjallarhorn.state.BlockCoordinateSparseMap
import gay.pizza.foundation.gjallarhorn.state.BlockExpanse
import gay.pizza.foundation.gjallarhorn.state.BlockStateMap
import gay.pizza.foundation.gjallarhorn.state.ChangelogSlice
import gay.pizza.foundation.gjallarhorn.util.BlockColorKey
import gay.pizza.foundation.heimdall.table.PlayerPositionTable
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import java.awt.Color
import java.awt.image.BufferedImage
import java.util.UUID
class PlayerLocationShareRenderer(
val expanse: BlockExpanse,
val db: Database,
quadPixelSize: Int = defaultQuadPixelSize
) : BlockGridRenderer(quadPixelSize) {
private val colorKey = BlockColorKey(mapOf())
override fun render(slice: ChangelogSlice, map: BlockStateMap): BufferedImage {
val start = slice.sliceChangeRange.start
val end = slice.sliceChangeRange.endInclusive
val playerSparseMap = BlockCoordinateSparseMap<MutableList<UUID>>()
val allPlayerIds = HashSet<UUID>()
transaction(db) {
PlayerPositionTable.select {
(PlayerPositionTable.time greater start) and
(PlayerPositionTable.time lessEq end)
}.forEach {
val x = it[PlayerPositionTable.x].toLong()
val y = it[PlayerPositionTable.y].toLong()
val z = it[PlayerPositionTable.z].toLong()
val coordinate = expanse.offset.applyAsOffset(BlockCoordinate(x, y, z))
val player = it[PlayerPositionTable.player]
playerSparseMap.createOrModify(
coordinate,
create = { mutableListOf(player) },
modify = { players -> players.add(player) })
allPlayerIds.add(player)
}
}
val colorOfPlayers = allPlayerIds.associateWith { colorKey.map(it.toString()) }
return buildPixelQuadImage(expanse) { g, x, z ->
val players = playerSparseMap.getVerticalSection(x, z)?.flatMap { it.value }?.distinct()
if (players != null) {
setPixelQuad(g, x, z, colorOfPlayers[players.first()]!!)
} else {
setPixelQuad(g, x, z, Color.white)
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +0,0 @@
package gay.pizza.foundation.gjallarhorn.util
import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO
fun BufferedImage.savePngFile(path: String) {
if (!ImageIO.write(this, "png", File(path))) {
throw RuntimeException("Unable to write PNG.")
}
}

View File

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

View File

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

View File

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