245 lines
11 KiB
Kotlin
245 lines
11 KiB
Kotlin
package xyz.etztech.stonks.statisticsimporter
|
|
|
|
import com.beust.klaxon.*
|
|
import com.beust.klaxon.Klaxon
|
|
import io.ktor.client.*
|
|
import io.ktor.client.engine.java.*
|
|
import io.ktor.client.request.*
|
|
import io.ktor.client.statement.*
|
|
import io.ktor.http.*
|
|
import java.io.File
|
|
import java.time.Duration
|
|
import java.time.Instant
|
|
import kotlinx.coroutines.*
|
|
import org.jetbrains.exposed.sql.*
|
|
import org.jetbrains.exposed.sql.`java-time`.timestamp
|
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
|
import org.jetbrains.exposed.sql.transactions.transaction
|
|
import xyz.etztech.stonks.dsl.LiveStatistics
|
|
import xyz.etztech.stonks.dsl.Players
|
|
import xyz.etztech.stonks.dsl.Statistics
|
|
|
|
object StatisticsImporter {
|
|
init {}
|
|
|
|
private val httpClient = HttpClient(Java)
|
|
private val klaxon = Klaxon()
|
|
|
|
fun importStatistics(folder: String, database: Database) {
|
|
println("Importing new statistics...")
|
|
File(folder).listFiles().forEach { readFile(it, database) }
|
|
println("Updating live statistics table...")
|
|
updateLiveStatistics(database)
|
|
println("Updating aggregate statistics table...")
|
|
updateAggregateStatistics(database)
|
|
println("Refreshing player names...")
|
|
refreshPlayerNames(database)
|
|
}
|
|
|
|
fun readFile(file: File, database: Database) {
|
|
val statsFile = StatsFile.fromJson(file.readText())
|
|
val playerId = file.nameWithoutExtension
|
|
|
|
val playerStats = emptyMap<String, MutableMap<String, Long>>().toMutableMap()
|
|
|
|
transaction(database) {
|
|
LiveStatistics.slice(LiveStatistics.type, LiveStatistics.name, LiveStatistics.value)
|
|
.select { LiveStatistics.playerId eq playerId }
|
|
.forEach {
|
|
if (playerStats.containsKey(it[LiveStatistics.type])) {
|
|
playerStats[it[LiveStatistics.type]]?.put(
|
|
it[LiveStatistics.name],
|
|
it[LiveStatistics.value]
|
|
)
|
|
} else {
|
|
playerStats.put(
|
|
it[LiveStatistics.type],
|
|
mapOf<String, Long>(
|
|
it[LiveStatistics.name] to
|
|
it[LiveStatistics.value]
|
|
)
|
|
.toMutableMap()
|
|
)
|
|
}
|
|
}
|
|
}
|
|
transaction(database) {
|
|
addLogger(StdOutSqlLogger)
|
|
|
|
statsFile?.stats?.forEach { type, stats ->
|
|
stats.forEach { name, value ->
|
|
if (playerStats.get(type)?.get(name) != value) {
|
|
Statistics.insert {
|
|
it[Statistics.playerId] = playerId
|
|
it[Statistics.type] = type
|
|
it[Statistics.name] = name
|
|
it[Statistics.timestamp] = Instant.now() as Instant
|
|
it[Statistics.value] = value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun updateLiveStatistics(database: Database) {
|
|
transaction(database) {
|
|
TransactionManager.current()
|
|
.exec(
|
|
"""
|
|
MERGE INTO LIVESTATISTICS AS T USING (
|
|
SELECT
|
|
STATISTICS."Type",
|
|
STATISTICS."Name",
|
|
STATISTICS."PlayerId",
|
|
STATISTICS."Value",
|
|
(RANK () OVER (PARTITION BY STATISTICS."Type", STATISTICS."Name" ORDER BY STATISTICS."Value" DESC)) AS "Rank"
|
|
FROM
|
|
STATISTICS
|
|
JOIN (
|
|
SELECT
|
|
"Type",
|
|
"Name",
|
|
"PlayerId",
|
|
MAX("Timestamp") AS "MaxTimestamp",
|
|
FROM STATISTICS
|
|
GROUP BY
|
|
"Type",
|
|
"Name",
|
|
"PlayerId"
|
|
) MAX_TIMESTAMPS
|
|
ON
|
|
STATISTICS."Type" = MAX_TIMESTAMPS."Type"
|
|
AND STATISTICS."Name" = MAX_TIMESTAMPS."Name"
|
|
AND STATISTICS."PlayerId" = MAX_TIMESTAMPS."PlayerId"
|
|
AND STATISTICS."Timestamp" = MAX_TIMESTAMPS."MaxTimestamp"
|
|
) AS S
|
|
ON (T."Type" = S."Type" AND T."Name" = S."Name" AND T."PlayerId" = S."PlayerId")
|
|
|
|
WHEN MATCHED AND (S."Value" <> T."Value" OR S."Rank" <> T."Rank")THEN
|
|
UPDATE SET T."Value" = S."Value", T."Rank" = S."Rank"
|
|
WHEN NOT MATCHED THEN
|
|
INSERT VALUES (S."PlayerId", S."Type", S."Name", S."Value", S."Rank")
|
|
""".trimIndent()
|
|
)
|
|
}
|
|
}
|
|
|
|
fun updateAggregateStatistics(database: Database) {
|
|
transaction(database) {
|
|
TransactionManager.current()
|
|
.exec(
|
|
"""
|
|
SET @Timestamp = CURRENT_TIMESTAMP;
|
|
|
|
INSERT INTO AGGREGATESTATISTICS ("Type", "Name", "Timestamp", "Value")
|
|
SELECT Live."Type",
|
|
'',
|
|
@Timestamp AS "Timestamp",
|
|
sum(Live."Value")
|
|
FROM LIVESTATISTICS as Live
|
|
LEFT JOIN AGGREGATESTATISTICS as Agg
|
|
ON Live."Type" = Agg."Type"
|
|
WHERE array_contains(array['minecraft:mined'], Live."Type")
|
|
GROUP BY Live."Type"
|
|
HAVING sum(Live."Value") <> max(Agg."Value") OR max(Agg."Value") IS NULL;
|
|
|
|
INSERT INTO AGGREGATESTATISTICS ("Type", "Name", "Timestamp", "Value")
|
|
SELECT Live."Type",
|
|
Live."Name",
|
|
@Timestamp AS "Timestamp",
|
|
Sum(Live."Value")
|
|
FROM livestatistics as Live
|
|
LEFT JOIN AGGREGATESTATISTICS as Agg
|
|
ON Live."Type" = Agg."Type" AND Live."Name" = Agg."Name"
|
|
WHERE array_contains(array['minecraft:animals_bred', 'minecraft:play_one_minute', 'minecraft:deaths', 'minecraft:player_kills', 'minecraft:aviate_one_cm', 'minecraft:boat_one_cm', 'minecraft:crouch_one_cm', 'minecraft:horse_one_cm', 'minecraft:minecart_one_cm', 'minecraft:sprint_one_cm', 'minecraft:strider_one_cm', 'minecraft:swim_one_cm', 'minecraft:walk_on_water_one_cm', 'minecraft:walk_one_cm', 'minecraft:walk_under_water_one_cm' ], Live."Name")
|
|
OR array_contains(array['minecraft:killed', 'minecraft:killed_by'], Live."Type")
|
|
GROUP BY Live."Type", Live."Name"
|
|
HAVING sum(Live."Value") <> max(Agg."Value") OR max(Agg."Value") IS NULL;
|
|
""".trimIndent()
|
|
)
|
|
}
|
|
}
|
|
|
|
fun refreshPlayerNames(database: Database) {
|
|
transaction(database) {
|
|
addLogger(StdOutSqlLogger)
|
|
val savedPlayers =
|
|
Players.selectAll().map {
|
|
object {
|
|
val id = it[Players.id]
|
|
val timestamp = it[Players.timestamp]
|
|
}
|
|
}
|
|
|
|
val players =
|
|
LiveStatistics.slice(LiveStatistics.playerId)
|
|
.selectAll()
|
|
.groupBy(LiveStatistics.playerId)
|
|
.map { it[LiveStatistics.playerId] }
|
|
|
|
savedPlayers
|
|
.filter { Duration.between(Instant.now(), it.timestamp) > Duration.ofDays(1) }
|
|
.forEach { player ->
|
|
runBlocking {
|
|
val name = getName(player.id)
|
|
if (name != null) {
|
|
println("Updating ${player.id} -> $name")
|
|
Players.update({ Players.id eq player.id }) {
|
|
it[Players.name] = name
|
|
it[Players.timestamp] = Instant.now()
|
|
}
|
|
} else {
|
|
println("Error updating ${player.id}")
|
|
}
|
|
}
|
|
}
|
|
|
|
players
|
|
.filterNot { player ->
|
|
savedPlayers.any { savedPlayer -> savedPlayer.id == player }
|
|
}
|
|
.forEach { playerId ->
|
|
runBlocking {
|
|
val name = getName(playerId)
|
|
if (name != null) {
|
|
println("Updating $playerId -> $name")
|
|
Players.insert {
|
|
it[Players.id] = playerId
|
|
it[Players.name] = name
|
|
it[Players.timestamp] = Instant.now()
|
|
}
|
|
} else {
|
|
println("Error updating $playerId")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
suspend fun getName(id: String): String? {
|
|
val response: HttpResponse =
|
|
httpClient.request(
|
|
"https://sessionserver.mojang.com/session/minecraft/profile/${id.replace("-", "")}"
|
|
) { method = HttpMethod.Get }
|
|
if (response.status.isSuccess()) {
|
|
try {
|
|
return MojangProfileResponse.fromJson(response.readText())?.name
|
|
} catch (e: Exception) {}
|
|
}
|
|
return null
|
|
}
|
|
|
|
data class StatsFile(val stats: Map<String, Map<String, Long>>) {
|
|
companion object {
|
|
public fun fromJson(json: String) = klaxon.parse<StatsFile>(json)
|
|
}
|
|
}
|
|
|
|
data class MojangProfileResponse(val name: String) {
|
|
companion object {
|
|
public fun fromJson(json: String) = klaxon.parse<MojangProfileResponse>(json)
|
|
}
|
|
}
|
|
}
|