mirror of
				https://github.com/GayPizzaSpecifications/foundation.git
				synced 2025-11-04 11:39:39 +00:00 
			
		
		
		
	Heimdall: It's back!
This commit is contained in:
		@ -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
 | 
			
		||||
		Reference in New Issue
	
	Block a user