From e0c71006d5b1859d8ec858aacd75073d8c25764c Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Sun, 25 Feb 2024 17:22:12 -0500 Subject: [PATCH] [O] Remake maimai2 user summary api --- .../sega/game/maimai2/Maimai2New.kt | 91 ------------------- .../icu/samnyan/aqua/net/db/AquaNetUser.kt | 3 + .../icu/samnyan/aqua/net/games/Maimai2.kt | 90 ++++++++++++++++++ .../java/icu/samnyan/aqua/net/games/Models.kt | 37 ++++++++ 4 files changed, 130 insertions(+), 91 deletions(-) delete mode 100644 src/main/java/icu/samnyan/aqua/api/controller/sega/game/maimai2/Maimai2New.kt create mode 100644 src/main/java/icu/samnyan/aqua/net/games/Maimai2.kt create mode 100644 src/main/java/icu/samnyan/aqua/net/games/Models.kt diff --git a/src/main/java/icu/samnyan/aqua/api/controller/sega/game/maimai2/Maimai2New.kt b/src/main/java/icu/samnyan/aqua/api/controller/sega/game/maimai2/Maimai2New.kt deleted file mode 100644 index f7ae185c..00000000 --- a/src/main/java/icu/samnyan/aqua/api/controller/sega/game/maimai2/Maimai2New.kt +++ /dev/null @@ -1,91 +0,0 @@ -package icu.samnyan.aqua.api.controller.sega.game.maimai2 - -import ext.API -import ext.RP -import ext.minus -import icu.samnyan.aqua.sega.maimai2.dao.userdata.UserDataRepository -import icu.samnyan.aqua.sega.maimai2.dao.userdata.UserGeneralDataRepository -import icu.samnyan.aqua.sega.maimai2.dao.userdata.UserPlaylogRepository -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RestController -import kotlin.jvm.optionals.getOrNull - -@RestController -@API("api/game/maimai2new") -class Maimai2New( - private val userPlaylogRepository: UserPlaylogRepository, - private val userDataRepository: UserDataRepository, - private val userGeneralDataRepository: UserGeneralDataRepository -) -{ - data class TrendOut(val date: String, val rating: Int, val plays: Int) - - @API("trend") - fun trend(@RP userId: Long): List { - // O(n log n) sort - val d = userPlaylogRepository.findByUser_Card_ExtId(userId).sortedBy { it.playDate }.toList() - - // Precompute the play counts for each date in O(n) - val playCounts = d.groupingBy { it.playDate }.eachCount() - - // Find the max afterRating on each date - val maxRating = d.groupingBy { it.playDate }.fold(0) { acc, e -> maxOf(acc, e.afterRating) } - - // Use the precomputed play counts - return d.distinctBy { it.playDate } - .map { TrendOut(it.playDate, maxRating[it.playDate] ?: 0, - playCounts[it.playDate] ?: 0) } - .sortedBy { it.date } - } - - private val shownRanks = listOf( - 100.5 to "SSS+", - 100.0 to "SSS", - 99.5 to "SS+", - 99.0 to "SS", - 98.0 to "S+", - 97.0 to "S").map { (k, v) -> (k * 10000).toInt() to v } - - @API("user-summary") - fun userSummary(@RP userId: Long): Map { - // Summary values: total plays, player rating, server-wide ranking - // number of each rank, max combo, number of full combo, number of all perfect - val user = userDataRepository.findByCard_ExtId(userId).getOrNull() ?: (404 - "User not found") - val plays = userPlaylogRepository.findByUser_Card_ExtId(userId) - val extra = userGeneralDataRepository.findByUser_Card_ExtId(userId) - .associate { it.propertyKey to it.propertyValue } - - // O(6n) ranks algorithm: Loop through the entire list of plays, - // count the number of each rank - val ranks = shownRanks.associate { (_, v) -> v to 0 }.toMutableMap() - plays.forEach { - shownRanks.find { (s, _) -> it.achievement > s }?.let { (_, v) -> ranks[v] = ranks[v]!! + 1 } - } - - return mapOf( - "name" to user.userName, - "iconId" to user.iconId, - - "serverRank" to userDataRepository.getRanking(user.playerRating), - "accuracy" to plays.sumOf { it.achievement } / plays.size, - "rating" to user.playerRating, - "ratingHighest" to user.highestRating, - "ranks" to ranks.map { (k, v) -> mapOf("name" to k, "count" to v) }, - "maxCombo" to plays.maxOf { it.maxCombo }, - "fullCombo" to plays.count { it.totalCombo == it.maxCombo }, - "allPerfect" to plays.count { it.achievement == 1010000 }, - "totalDxScore" to user.totalDeluxscore, - - "plays" to plays.size, - "totalPlayTime" to plays.count() * 3, // TODO: Make a more accurate estimate - "joined" to user.firstPlayDate, - "lastSeen" to user.lastPlayDate, - "lastVersion" to user.lastRomVersion, - - "best35" to (extra["recent_rating"] ?: ""), - "best15" to (extra["recent_rating_new"] ?: ""), - - "recent" to plays.sortedBy { it.playDate }.takeLast(10) - ) - } -} \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt index ddd13de1..53c552ac 100644 --- a/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt +++ b/src/main/java/icu/samnyan/aqua/net/db/AquaNetUser.kt @@ -69,6 +69,9 @@ interface AquaNetUserRepo : JpaRepository { fun findByAuId(auId: Long): AquaNetUser? fun findByEmailIgnoreCase(email: String): AquaNetUser? fun findByUsernameIgnoreCase(username: String): AquaNetUser? + + fun byName(username: Str, callback: (AquaNetUser) -> T) = + findByUsernameIgnoreCase(username)?.let(callback) ?: (404 - "User not found") } data class SettingField( diff --git a/src/main/java/icu/samnyan/aqua/net/games/Maimai2.kt b/src/main/java/icu/samnyan/aqua/net/games/Maimai2.kt new file mode 100644 index 00000000..1e333a43 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/net/games/Maimai2.kt @@ -0,0 +1,90 @@ +package icu.samnyan.aqua.net.games + +import ext.API +import ext.RP +import ext.Str +import ext.minus +import icu.samnyan.aqua.net.db.AquaNetUserRepo +import icu.samnyan.aqua.sega.maimai2.dao.userdata.UserDataRepository +import icu.samnyan.aqua.sega.maimai2.dao.userdata.UserGeneralDataRepository +import icu.samnyan.aqua.sega.maimai2.dao.userdata.UserPlaylogRepository +import org.springframework.web.bind.annotation.RestController + +@RestController +@API("api/v2/game/maimai2") +class Maimai2( + val user: AquaNetUserRepo, + val userPlaylogRepository: UserPlaylogRepository, + val userDataRepository: UserDataRepository, + val userGeneralDataRepository: UserGeneralDataRepository +) +{ + @API("trend") + fun trend(@RP username: Str): List = user.byName(username) { u -> + // O(n log n) sort + val d = userPlaylogRepository.findByUser_Card_ExtId(u.ghostCard.extId).sortedBy { it.playDate }.toList() + + // Precompute the play counts for each date in O(n) + val playCounts = d.groupingBy { it.playDate }.eachCount() + + // Find the max afterRating on each date + val maxRating = d.groupingBy { it.playDate }.fold(0) { acc, e -> maxOf(acc, e.afterRating) } + + // Use the precomputed play counts + d.distinctBy { it.playDate } + .map { TrendOut(it.playDate, maxRating[it.playDate] ?: 0, + playCounts[it.playDate] ?: 0) } + .sortedBy { it.date } + } + + private val shownRanks = listOf( + 100.5 to "SSS+", + 100.0 to "SSS", + 99.5 to "SS+", + 99.0 to "SS", + 98.0 to "S+", + 97.0 to "S").map { (k, v) -> (k * 10000).toInt() to v } + + @API("user-summary") + fun userSummary(@RP username: Str) = user.byName(username) { u -> + // Summary values: total plays, player rating, server-wide ranking + // number of each rank, max combo, number of full combo, number of all perfect + val user = userDataRepository.findByCard(u.ghostCard) ?: (404 - "User not found") + val plays = userPlaylogRepository.findByUser_Card_ExtId(u.ghostCard.extId) + val extra = userGeneralDataRepository.findByUser_Card_ExtId(u.ghostCard.extId) + .associate { it.propertyKey to it.propertyValue } + + // O(6n) ranks algorithm: Loop through the entire list of plays, + // count the number of each rank + val ranks = shownRanks.associate { (_, v) -> v to 0 }.toMutableMap() + plays.forEach { + shownRanks.find { (s, _) -> it.achievement > s }?.let { (_, v) -> ranks[v] = ranks[v]!! + 1 } + } + + GenericGameSummary( + name = user.userName, + iconId = user.iconId, + serverRank = userDataRepository.getRanking(user.playerRating), + accuracy = plays.sumOf { it.achievement }.toDouble() / plays.size, + rating = user.playerRating, + ratingHighest = user.highestRating, + ranks = ranks.map { (k, v) -> RankCount(k, v) }, + maxCombo = plays.maxOf { it.maxCombo }, + fullCombo = plays.count { it.totalCombo == it.maxCombo }, + allPerfect = plays.count { it.achievement == 1010000 }, + totalScore = user.totalDeluxscore, + plays = plays.size, + totalPlayTime = plays.count() * 3L, // TODO: Give a better estimate + joined = user.firstPlayDate, + lastSeen = user.lastPlayDate, + lastVersion = user.lastRomVersion, + ratingComposition = mapOf( + "best35" to (extra["recent_rating"] ?: ""), + "best15" to (extra["recent_rating_new"] ?: "") + ), + recent = plays.sortedBy { it.playDate }.takeLast(15).map { + GenericGamePlaylog(it.playDate, it.achievement, it.maxCombo, it.totalCombo) + } + ) + } +} \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/net/games/Models.kt b/src/main/java/icu/samnyan/aqua/net/games/Models.kt new file mode 100644 index 00000000..e385b2db --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/net/games/Models.kt @@ -0,0 +1,37 @@ +package icu.samnyan.aqua.net.games + +data class TrendOut(val date: String, val rating: Int, val plays: Int) + +data class GenericGamePlaylog( + val playDate: String, + val achievement: Int, + val maxCombo: Int, + val totalCombo: Int +) + +data class RankCount(val name: String, val count: Int) + +data class GenericGameSummary( + val name: String, + val iconId: Int, + + val serverRank: Long, + val accuracy: Double, + val rating: Int, + val ratingHighest: Int, + val ranks: List, + val maxCombo: Int, + val fullCombo: Int, + val allPerfect: Int, + val totalScore: Long, + + val plays: Int, + val totalPlayTime: Long, + val joined: String, + val lastSeen: String, + val lastVersion: String, + + val ratingComposition: Map, + + val recent: List +) \ No newline at end of file