diff --git a/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt b/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt index 156dd69f..cd6bb086 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt @@ -7,12 +7,8 @@ import icu.samnyan.aqua.net.utils.SUCCESS import icu.samnyan.aqua.sega.allnet.UserKeychipRepo import icu.samnyan.aqua.sega.general.model.Card import icu.samnyan.aqua.sega.general.service.CardService -import jakarta.annotation.PostConstruct import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired -import org.springframework.scheduling.annotation.Scheduled -import java.util.concurrent.TimeUnit -import java.util.concurrent.locks.ReentrantLock import kotlin.jvm.optionals.getOrNull import kotlin.reflect.KClass @@ -45,33 +41,8 @@ abstract class GameApiController(val name: String, userDataClass: playlogRepo.findByUserCardExtId(card.extId) } - // List>> - private var rankingCache: List = emptyList() - private var rankingCacheLock = ReentrantLock() - // Sorted index List = Rank - private var rankingLookupCache: Map = emptyMap() private val pageSize = 100 - @API("ranking") - fun ranking(@RP token: String?, @RP page: Int?): List { - val time = millis() - - // Check cache validity - if (rankingCache.isEmpty()) (500 - "Rank is empty or is currently computing.") - - return page?.let { - if (it < 0) (400 - "Invalid page number") - rankingCache.drop(it * pageSize).take(pageSize) - } ?: rankingCache - } - - @PostConstruct - fun rankingCacheInit() = thread { rankingCacheRun() } - - // Every 20 minutes - @Scheduled(fixedRate = 20, initialDelay = 20, timeUnit = TimeUnit.MINUTES) - fun rankingCacheRun() = rankingCacheLock.maybeLock { rankingCacheCompute() } - private val tableName = when (name) { "mai2" -> "maimai2"; "chu3" -> "chusan"; else -> name } private val userDataTable = when (name) { "mai2" -> "maimai2_user_detail" @@ -80,47 +51,66 @@ abstract class GameApiController(val name: String, userDataClass: else -> "wacca_user" } private val userCardColumn = if (name == "chu3") "card_id" else "aime_card_id" + private val ratingColumn = if (name == "ongeki") "u.new_player_rating" else "u.player_rating" - fun rankingCacheCompute() { - val time = millis() - rankingCache = us.em.createNativeQuery( - """ - SELECT - c.id, - u.user_name, - ${if (name == "ongeki") "u.new_player_rating" else "u.player_rating"} AS rating, - u.last_play_date, - r.achievement_sum / NULLIF(r.play_count, 0) / 10000.0 AS acc, - r.full_combo_count AS fc, - r.all_perfect_count AS ap, - c.ranking_banned or COALESCE(a.opt_out_of_leaderboard, 0) or c.status = 12 AS hide, - a.username - FROM ${tableName}_user_ranking_cache r - JOIN $userDataTable u ON r.user_id = u.id - JOIN sega_card c ON u.$userCardColumn = c.id - LEFT JOIN aqua_net_user a ON c.net_user_id = a.au_id - WHERE r.play_count > 0 - AND NOT (c.ranking_banned or COALESCE(a.opt_out_of_leaderboard, 0) or c.status = 12) - ${if (name == "ongeki") "AND u.new_player_rating > 0" else "" /* Hide users on Ongeki 1.45 and below */} - ORDER BY rating DESC; - """ - ).setHint("jakarta.persistence.query.timeout", 60_000).exec.mapIndexed { i, it -> + private val rankingSelect = """ + SELECT + ROW_NUMBER() OVER (ORDER BY $ratingColumn DESC, c.id) AS player_rank, + c.id AS card_id, + u.user_name, + $ratingColumn AS rating, + u.last_play_date, + r.achievement_sum / NULLIF(r.play_count, 0) / 10000.0 AS acc, + r.full_combo_count AS fc, + r.all_perfect_count AS ap, + a.username + FROM ${tableName}_user_ranking_cache r + JOIN $userDataTable u ON r.user_id = u.id + JOIN sega_card c ON u.$userCardColumn = c.id + LEFT JOIN aqua_net_user a ON c.net_user_id = a.au_id + WHERE r.play_count > 0 + AND NOT (c.ranking_banned or COALESCE(a.opt_out_of_leaderboard, 0) or c.status = 12) + ${if (name == "ongeki") "AND u.new_player_rating > 0" else "" /* Hide users on Ongeki 1.45 and below */} + """.trimIndent() + + @API("ranking") + fun ranking(@RP token: String?, @RP page: Int?): List { + if (page != null && page < 0) (400 - "Invalid page number") + + val offset = page?.toLong()?.times(pageSize) + if (offset != null && offset > Int.MAX_VALUE) return emptyList() + + val query = us.em.createNativeQuery("$rankingSelect ORDER BY player_rank") + .setHint("jakarta.persistence.query.timeout", 60_000) + if (offset != null) { + query.setFirstResult(offset.toInt()).setMaxResults(pageSize) + } + + return query.exec.map { row -> GenericRankingPlayer( - rank = i + 1, - id = it[0]!!.long, - name = it[1].toString(), - rating = it[2]!!.int, - lastSeen = it[3].toString(), - accuracy = it[4]!!.double, - fullCombo = it[5]!!.int, - allPerfect = it[6]!!.int, - username = it[8]?.toString() ?: "user${it[0]}" + rank = row[0]!!.int, + id = row[1]!!.long, + name = row[2].toString(), + rating = row[3]!!.int, + lastSeen = row[4].toString(), + accuracy = row[5]!!.double, + fullCombo = row[6]!!.int, + allPerfect = row[7]!!.int, + username = row[8]?.toString() ?: "user${row[1]}" ) } - rankingLookupCache = rankingCache.associateBy { it.id } - logger.info("Ranking for $name computed in ${millis() - time}ms") } + private fun rankingPosition(cardId: Long): String = us.em.createNativeQuery( + """ + SELECT player_rank + FROM ($rankingSelect) ranking + WHERE card_id = :cardId + """.trimIndent() + ).setParameter("cardId", cardId) + .setHint("jakarta.persistence.query.timeout", 60_000) + .resultList.singleOrNull()?.let { (it as Number).toInt().str } ?: "-" + @API("playlog") fun playlog(@RP id: Long): IGenericGamePlaylog = playlogRepo.findById(id)() ?: (404 - "Playlog not found") @@ -188,7 +178,7 @@ abstract class GameApiController(val name: String, userDataClass: return GenericGameSummary( name = user.userName, aquaUser = card.aquaUser?.publicFields, - serverRank = rankingLookupCache[user.card!!.id]?.rank?.str ?: "-", + serverRank = rankingPosition(user.card!!.id), accuracy = plays.acc(), rating = getRating(user, false), ratingHighest = getRating(user, true),