From 85dd19509c7e96f1beb9147f90cadf3f4ac2c8a7 Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Tue, 19 Mar 2024 21:04:07 -0400 Subject: [PATCH] [M] Move class --- .../aqua/net/games/GameApiController.kt | 131 ++++++++++++++++++ .../java/icu/samnyan/aqua/net/games/Models.kt | 123 ---------------- 2 files changed, 131 insertions(+), 123 deletions(-) create mode 100644 src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt diff --git a/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt b/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt new file mode 100644 index 00000000..2b1160a4 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt @@ -0,0 +1,131 @@ +package icu.samnyan.aqua.net.games + +import ext.* +import icu.samnyan.aqua.net.db.AquaUserServices +import icu.samnyan.aqua.net.utils.SUCCESS +import icu.samnyan.aqua.sega.general.model.Card +import kotlin.jvm.optionals.getOrNull +import kotlin.reflect.KClass + +abstract class GameApiController(name: String, userDataClass: KClass) { + val musicMapping = resJson>("/meta/$name/music.json") + ?.mapKeys { it.key.toInt() } ?: emptyMap() + + val itemMapping = resJson>>("/meta/$name/items.json") ?: emptyMap() + + abstract val us: AquaUserServices + abstract val userDataRepo: GenericUserDataRepo + abstract val playlogRepo: GenericPlaylogRepo<*> + abstract val shownRanks: List> + abstract val settableFields: Map Unit> + + @API("trend") + abstract suspend fun trend(@RP username: String): List + @API("user-summary") + abstract suspend fun userSummary(@RP username: String): GenericGameSummary + + @API("recent") + suspend fun recent(@RP username: String): List = us.cardByName(username) { card -> + playlogRepo.findByUserCardExtId(card.extId) + } + + private var rankingCache: Pair>? = null + @API("ranking") + fun ranking(): List { + // Read from cache if we just computed it less than 2 minutes ago + rankingCache?.let { (t, r) -> + if (millis() - t < 120_000) return r + } + + // TODO: pagination + val players = userDataRepo.findAll().sortedByDescending { it.playerRating } + return players.filter { it.card != null }.mapIndexed { i, user -> + val plays = playlogRepo.findByUserCardExtId(user.card!!.extId) + + GenericRankingPlayer( + rank = i + 1, + name = user.userName, + accuracy = plays.acc(), + rating = user.playerRating, + allPerfect = plays.count { it.isAllPerfect }, + fullCombo = plays.count { it.isFullCombo }, + lastSeen = user.lastPlayDate.toString(), + username = user.card!!.aquaUser?.username ?: "user${user.card!!.id}" + ) + }.also { rankingCache = millis() to it } // Update the cache + } + + @API("playlog") + fun playlog(@RP id: Long): IGenericGamePlaylog = playlogRepo.findById(id).getOrNull() ?: (404 - "Playlog not found") + + val userDetailFields by lazy { userDataClass.gettersMap().let { vm -> + settableFields.map { (k, _) -> k to (vm[k] ?: error("Field $k not found")) }.toMap() + } } + + @API("user-detail") + suspend fun userDetail(@RP username: String) = us.cardByName(username) { card -> + val u = userDataRepo.findByCard(card) ?: (404 - "User not found") + userDetailFields.toList().associate { (k, f) -> k to f.invoke(u) } + } + + @API("user-detail-set") + suspend fun userDetailSet(@RP token: String, @RP field: String, @RP value: String): Any { + val prop = settableFields[field] ?: (400 - "Invalid field $field") + + return us.jwt.auth(token) { u -> + val user = async { userDataRepo.findByCard(u.ghostCard) } ?: (404 - "User not found") + prop(user, value) + async { userDataRepo.save(user) } + SUCCESS + } + } + + fun genericUserSummary(card: Card, ratingComp: Map): GenericGameSummary { + // 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 = userDataRepo.findByCard(card) ?: (404 - "Game data not found") + val plays = playlogRepo.findByUserCardExtId(card.extId) + + // Detailed ranks: Find the number of each rank in each level category + // map> + val rankMap = shownRanks.associate { (_, v) -> v to 0 } + val detailedRanks = HashMap>() + plays.forEach { play -> + val lvl = musicMapping[play.musicId]?.notes?.getOrNull(if (play.level == 10) 0 else play.level)?.lv ?: return@forEach + shownRanks.find { (s, _) -> play.achievement > s }?.let { (_, v) -> + val ranks = detailedRanks.getOrPut(lvl.toInt()) { rankMap.toMutableMap() } + ranks[v] = ranks[v]!! + 1 + } + } + + // Collapse detailed ranks to get non-detailed ranks map + val ranks = shownRanks.associate { (_, v) -> v to 0 }.toMutableMap().also { ranks -> + plays.forEach { play -> + shownRanks.find { (s, _) -> play.achievement > s }?.let { (_, v) -> ranks[v] = ranks[v]!! + 1 } + } + } + + return GenericGameSummary( + name = user.userName, + iconId = user.iconId, + aquaUser = card.aquaUser?.publicFields, + serverRank = userDataRepo.getRanking(user.playerRating), + accuracy = plays.acc(), + rating = user.playerRating, + ratingHighest = user.highestRating, + ranks = ranks.map { (k, v) -> RankCount(k, v) }, + detailedRanks = detailedRanks, + maxCombo = plays.maxOfOrNull { it.maxCombo } ?: 0, + fullCombo = plays.count { it.isFullCombo }, + allPerfect = plays.count { it.isAllPerfect }, + totalScore = user.totalScore, + plays = plays.size, + totalPlayTime = plays.count() * 3L, // TODO: Give a better estimate + joined = user.firstPlayDate.toString(), + lastSeen = user.lastPlayDate.toString(), + lastVersion = user.lastRomVersion, + ratingComposition = ratingComp, + recent = plays.sortedBy { it.userPlayDate.toString() }.takeLast(15).reversed() + ) + } +} \ 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 index 7fec0027..2bd72672 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/Models.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/Models.kt @@ -114,127 +114,4 @@ interface GenericUserDataRepo : JpaRepository { interface GenericPlaylogRepo : JpaRepository { fun findByUserCardExtId(extId: Long): List fun findByUserCardExtId(extId: Long, page: Pageable): Page -} - -abstract class GameApiController(name: String, userDataClass: KClass) { - val musicMapping = resJson>("/meta/$name/music.json") - ?.mapKeys { it.key.toInt() } ?: emptyMap() - - val itemMapping = resJson>>("/meta/$name/items.json") ?: emptyMap() - - abstract val us: AquaUserServices - abstract val userDataRepo: GenericUserDataRepo - abstract val playlogRepo: GenericPlaylogRepo<*> - abstract val shownRanks: List> - abstract val settableFields: Map Unit> - - @API("trend") - abstract suspend fun trend(@RP username: String): List - @API("user-summary") - abstract suspend fun userSummary(@RP username: String): GenericGameSummary - - @API("recent") - suspend fun recent(@RP username: String): List = us.cardByName(username) { card -> - playlogRepo.findByUserCardExtId(card.extId) - } - - private var rankingCache: Pair>? = null - @API("ranking") - fun ranking(): List { - // Read from cache if we just computed it less than 2 minutes ago - rankingCache?.let { (t, r) -> - if (millis() - t < 120_000) return r - } - - // TODO: pagination - val players = userDataRepo.findAll().sortedByDescending { it.playerRating } - return players.filter { it.card != null }.mapIndexed { i, user -> - val plays = playlogRepo.findByUserCardExtId(user.card!!.extId) - - GenericRankingPlayer( - rank = i + 1, - name = user.userName, - accuracy = plays.acc(), - rating = user.playerRating, - allPerfect = plays.count { it.isAllPerfect }, - fullCombo = plays.count { it.isFullCombo }, - lastSeen = user.lastPlayDate.toString(), - username = user.card!!.aquaUser?.username ?: "user${user.card!!.id}" - ) - }.also { rankingCache = millis() to it } // Update the cache - } - - @API("playlog") - fun playlog(@RP id: Long): IGenericGamePlaylog = playlogRepo.findById(id).getOrNull() ?: (404 - "Playlog not found") - - val userDetailFields by lazy { userDataClass.gettersMap().let { vm -> - settableFields.map { (k, _) -> k to (vm[k] ?: error("Field $k not found")) }.toMap() - } } - - @API("user-detail") - suspend fun userDetail(@RP username: String) = us.cardByName(username) { card -> - val u = userDataRepo.findByCard(card) ?: (404 - "User not found") - userDetailFields.toList().associate { (k, f) -> k to f.invoke(u) } - } - - @API("user-detail-set") - suspend fun userDetailSet(@RP token: String, @RP field: String, @RP value: String): Any { - val prop = settableFields[field] ?: (400 - "Invalid field $field") - - return us.jwt.auth(token) { u -> - val user = async { userDataRepo.findByCard(u.ghostCard) } ?: (404 - "User not found") - prop(user, value) - async { userDataRepo.save(user) } - SUCCESS - } - } - - fun genericUserSummary(card: Card, ratingComp: Map): GenericGameSummary { - // 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 = userDataRepo.findByCard(card) ?: (404 - "Game data not found") - val plays = playlogRepo.findByUserCardExtId(card.extId) - - // Detailed ranks: Find the number of each rank in each level category - // map> - val rankMap = shownRanks.associate { (_, v) -> v to 0 } - val detailedRanks = HashMap>() - plays.forEach { play -> - val lvl = musicMapping[play.musicId]?.notes?.getOrNull(if (play.level == 10) 0 else play.level)?.lv ?: return@forEach - shownRanks.find { (s, _) -> play.achievement > s }?.let { (_, v) -> - val ranks = detailedRanks.getOrPut(lvl.toInt()) { rankMap.toMutableMap() } - ranks[v] = ranks[v]!! + 1 - } - } - - // Collapse detailed ranks to get non-detailed ranks map - val ranks = shownRanks.associate { (_, v) -> v to 0 }.toMutableMap().also { ranks -> - plays.forEach { play -> - shownRanks.find { (s, _) -> play.achievement > s }?.let { (_, v) -> ranks[v] = ranks[v]!! + 1 } - } - } - - return GenericGameSummary( - name = user.userName, - iconId = user.iconId, - aquaUser = card.aquaUser?.publicFields, - serverRank = userDataRepo.getRanking(user.playerRating), - accuracy = plays.acc(), - rating = user.playerRating, - ratingHighest = user.highestRating, - ranks = ranks.map { (k, v) -> RankCount(k, v) }, - detailedRanks = detailedRanks, - maxCombo = plays.maxOfOrNull { it.maxCombo } ?: 0, - fullCombo = plays.count { it.isFullCombo }, - allPerfect = plays.count { it.isAllPerfect }, - totalScore = user.totalScore, - plays = plays.size, - totalPlayTime = plays.count() * 3L, // TODO: Give a better estimate - joined = user.firstPlayDate.toString(), - lastSeen = user.lastPlayDate.toString(), - lastVersion = user.lastRomVersion, - ratingComposition = ratingComp, - recent = plays.sortedBy { it.userPlayDate.toString() }.takeLast(15).reversed() - ) - } } \ No newline at end of file