fix: improve ongeki ux, improve ranking backend (#227)

This commit is contained in:
Raymond
2026-06-14 11:12:37 +08:00
committed by GitHub
parent 08bf69c723
commit e62115e6ac
11 changed files with 90 additions and 97 deletions
@@ -46,10 +46,10 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
}
// List<Pair<should_hide, player>>>
private var rankingCache: List<Pair<Bool, GenericRankingPlayer>> = emptyList()
private var rankingCache: List<GenericRankingPlayer> = emptyList()
private var rankingCacheLock = ReentrantLock()
// Sorted index List<Rating> = Rank
private var rankingSortedIndex: List<Int> = emptyList()
private var rankingLookupCache: Map<Long, GenericRankingPlayer> = emptyMap()
private val pageSize = 100
@API("ranking")
@@ -59,26 +59,14 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
// Check cache validity
if (rankingCache.isEmpty()) (500 - "Rank is empty or is currently computing.")
val reqUser = token?.let { us.jwt.auth(it) }?.let { u ->
// Optimization: If the user is not banned, we don't need to process user information
if (!u.ghostCard.rankingBanned && !u.cards.any { it.rankingBanned } && u.ghostCard.status.isNormal) null
else u
}
// Read from cache if we just computed it less than duration ago
// Shadow-ban: Do not show banned cards in the ranking except for the user who owns the card
val v = rankingCache.filter { !it.l || it.r.username == reqUser?.username }
.mapIndexed { i, it -> it.r.apply { rank = i + 1 } }
.also { logger.info("Ranking returned in ${millis() - time}ms") }
return page?.let {
if (it < 0) (400 - "Invalid page number")
v.drop(it * pageSize).take(pageSize)
} ?: v
rankingCache.drop(it * pageSize).take(pageSize)
} ?: rankingCache
}
@PostConstruct
fun rakingCacheInit() = thread { rankingCacheRun() }
fun rankingCacheInit() = thread { rankingCacheRun() }
// Every 20 minutes
@Scheduled(fixedRate = 20, timeUnit = TimeUnit.MINUTES)
@@ -92,34 +80,35 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
SELECT
c.id,
u.user_name,
u.player_rating,
${if (name == "ongeki") "u.new_player_rating" else "u.player_rating"} AS rating,
u.last_play_date,
AVG(p.achievement) / 10000.0 AS acc,
SUM(p.is_full_combo) AS fc,
SUM(p.is_all_perfect) AS ap,
c.ranking_banned or a.opt_out_of_leaderboard or c.status = 12 AS hide,
a.username ${if (name == "ongeki") ", u.new_player_rating" else ""}
a.username
FROM ${tableName}_user_playlog_view p
JOIN ${tableName}_user_data_view u ON p.user_id = u.id
JOIN sega_card c ON u.aime_card_id = c.id
LEFT JOIN aqua_net_user a ON c.net_user_id = a.au_id
GROUP BY p.user_id, u.player_rating
HAVING NOT hide ${if (name == "ongeki") "AND rating > 0" else "" /* Hide users on Ongeki 1.45 and below */}
ORDER BY u.player_rating DESC;
"""
).exec.mapIndexed { i, it ->
it[7].truthy to GenericRankingPlayer(
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]}",
modernRating = if (name == "ongeki") it[9]?.int() ?: 0 else 0
username = it[8]?.toString() ?: "user${it[0]}"
)
}
rankingSortedIndex = rankingCache.filter { !it.l }.map { it.r.rating }.reversed()
rankingLookupCache = rankingCache.associateBy { it.id }
logger.info("Ranking for $name computed in ${millis() - time}ms")
}
@@ -158,6 +147,10 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
userMusicRepo.findByUser_Card_ExtIdAndMusicIdIn(card.extId, musicList)
}
open fun getRating(user: T, isHighest: Bool): Int {
return if (isHighest) user.highestRating else user.playerRating;
}
fun genericUserSummary(card: Card, ratingComp: Map<String, String>, rival: Boolean? = null, favorites: List<Int>? = null): GenericGameSummary {
// Summary values: total plays, player rating, server-wide ranking
// number of each rank, max combo, number of full combo, number of all perfect
@@ -183,18 +176,14 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
}
}
// Find serverRank by binary-searching in the rankingSortedIndex to find the minimal index that
// is greater than or equal to the user's rating
var serverRank = rankingSortedIndex.binarySearch(user.playerRating).let { if (it < 0) -it - 1 else it + 1 }
serverRank = rankingSortedIndex.size - serverRank
return GenericGameSummary(
name = user.userName,
aquaUser = card.aquaUser?.publicFields,
serverRank = serverRank.long,
serverRank = rankingLookupCache[user.card!!.id]?.rank?.str ?: "-",
accuracy = plays.acc(),
rating = user.playerRating,
ratingHighest = user.highestRating,
rating = getRating(user, false),
ratingHighest = getRating(user, true),
ratingNotGeneric = getRating(user, false) != user.playerRating,
ranks = ranks.map { (k, v) -> RankCount(k, v) },
detailedRanks = detailedRanks,
maxCombo = plays.maxOfOrNull { it.maxCombo } ?: 0,
@@ -1,6 +1,7 @@
package icu.samnyan.aqua.net.games
import com.fasterxml.jackson.annotation.JsonIgnore
import ext.Bool
import ext.JACKSON
import ext.JavaSerializable
import icu.samnyan.aqua.sega.general.model.Card
@@ -25,10 +26,11 @@ data class GenericGameSummary(
val aquaUser: Map<String, Any?>?,
val serverRank: Long,
val serverRank: String,
val accuracy: Double,
val rating: Int,
val ratingHighest: Int,
val ratingNotGeneric: Bool,
val ranks: List<RankCount>,
val detailedRanks: Map<Int, Map<String, Int>>,
val maxCombo: Int,
@@ -60,7 +62,9 @@ data class GenericRankingPlayer(
val allPerfect: Int,
val fullCombo: Int,
val lastSeen: String,
val modernRating: Int = 0
@JsonIgnore
val id: Long
)
@Serializable
@@ -14,6 +14,7 @@ import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserDetail
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserGeneralData
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserLoginBonus
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserOption
import jakarta.annotation.PostConstruct
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RestController
@@ -214,6 +215,21 @@ class Maimai2(
// creating a ton of SHA256 hashes every launch *probably* isn't ideal but it's better than exposing token AND extid...
@OptIn(ExperimentalStdlibApi::class)
fun myPhotoGetHash(value: Str): Str {
return MessageDigest.getInstance("SHA-256")
.digest(value.toByteArray(UTF_8)).toHexString()
}
@PostConstruct
fun myPhotoInit() = thread {
photoDir.listFiles()
?.map { it.name }
?.map {
// generate hash of photo filename as to not expose details
photoHashMap[it] = myPhotoGetHash(it)
}
}
@API("my-photo")
suspend fun myPhoto(@RP token: Str) = us.jwt.auth(token) { u ->
val find = "${u.ghostCard.extId}-"
@@ -224,8 +240,7 @@ class Maimai2(
?.map {
// generate hash of photo filename as to not expose details
if (!photoHashMap.containsKey(it))
photoHashMap[it] = MessageDigest.getInstance("SHA-256")
.digest(it.toByteArray(UTF_8)).toHexString()
photoHashMap[it] = myPhotoGetHash(it)
photoHashMap[it]
}
?: emptyList()
@@ -1,6 +1,7 @@
package icu.samnyan.aqua.net.games.ongeki
import ext.API
import ext.Bool
import ext.RP
import ext.minus
import icu.samnyan.aqua.net.db.AquaUserServices
@@ -64,15 +65,10 @@ class Ongeki(
))
}
@API("refresh-data")
suspend fun refreshData(@RP username: String) = us.cardByName(username) { card ->
val user = userDataRepo.findByCard_ExtId(card.extId) ?: (404 - "User not found")
if (user.newHighestRating > 0)
mapOf(
"playerRating" to user.newPlayerRating,
"highestRating" to user.newHighestRating
)
else (400 - "User has not played Refresh")
override fun getRating(user: UserData, isHighest: Bool): Int {
return if (user.newHighestRating > 0) {
if (isHighest) user.newHighestRating else user.newPlayerRating
} else if (isHighest) user.highestRating else user.playerRating
}
@API("user-option")