mirror of https://github.com/hykilpikonna/AquaDX
[O] Optimize ranking
parent
2376e511ac
commit
e34f0587fe
|
@ -5,6 +5,7 @@ import io.ktor.client.*
|
|||
import io.ktor.client.engine.cio.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import jakarta.persistence.Query
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.apache.tika.Tika
|
||||
|
@ -138,7 +139,26 @@ fun Any.long() = when (this) {
|
|||
}
|
||||
fun Any.uint32() = long() and 0xFFFFFFFF
|
||||
fun Any.int() = long().toInt()
|
||||
val Any.long get() = long()
|
||||
val Any.int get() = int()
|
||||
val Any.double get() = when (this) {
|
||||
is Boolean -> if (this) 1.0 else 0.0
|
||||
is Number -> toDouble()
|
||||
is String -> toDouble()
|
||||
else -> 400 - "Invalid number: $this"
|
||||
}
|
||||
operator fun Bool.unaryPlus() = if (this) 1 else 0
|
||||
val Any?.truthy get() = when (this) {
|
||||
null -> false
|
||||
is Bool -> this
|
||||
is Float -> this != 0f && !isNaN()
|
||||
is Double -> this != 0.0 && !isNaN()
|
||||
is Number -> this != 0
|
||||
is String -> this.isNotBlank()
|
||||
is Collection<*> -> isNotEmpty()
|
||||
is Map<*, *> -> isNotEmpty()
|
||||
else -> true
|
||||
}
|
||||
|
||||
// Collections
|
||||
fun <T> ls(vararg args: T) = args.toList()
|
||||
|
@ -176,7 +196,7 @@ fun Str.ensureEndingSlash() = if (endsWith('/')) this else "$this/"
|
|||
|
||||
fun <T: Any> T.logger() = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
// I hate this ;-;
|
||||
// I hate this ;-; (list destructuring)
|
||||
operator fun <E> List<E>.component6(): E = get(5)
|
||||
operator fun <E> List<E>.component7(): E = get(6)
|
||||
operator fun <E> List<E>.component8(): E = get(7)
|
||||
|
@ -185,3 +205,11 @@ operator fun <E> List<E>.component10(): E = get(9)
|
|||
operator fun <E> List<E>.component11(): E = get(10)
|
||||
operator fun <E> List<E>.component12(): E = get(11)
|
||||
operator fun <E> List<E>.component13(): E = get(12)
|
||||
|
||||
inline operator fun <reified E> List<Any?>.invoke(i: Int) = get(i) as E
|
||||
|
||||
val <F> Pair<F, *>.l get() = component1()
|
||||
val <S> Pair<*, S>.r get() = component2()
|
||||
|
||||
// Database
|
||||
val Query.exec get() = resultList.map { (it as Array<*>).toList() }
|
||||
|
|
|
@ -8,7 +8,7 @@ import org.slf4j.LoggerFactory
|
|||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
abstract class GameApiController<T : IUserData>(name: String, userDataClass: KClass<T>) {
|
||||
abstract class GameApiController<T : IUserData>(val name: String, userDataClass: KClass<T>) {
|
||||
val musicMapping = resJson<Map<String, GenericMusicMeta>>("/meta/$name/music.json")
|
||||
?.mapKeys { it.key.toInt() } ?: emptyMap()
|
||||
val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
@ -31,41 +31,61 @@ abstract class GameApiController<T : IUserData>(name: String, userDataClass: KCl
|
|||
playlogRepo.findByUserCardExtId(card.extId)
|
||||
}
|
||||
|
||||
private var rankingCache: MutableMap<Long, Pair<Long, List<GenericRankingPlayer>>> = mutableMapOf()
|
||||
// Pair<time, List<Pair<should_hide, player>>>
|
||||
private var rankingCache: Pair<Long, List<Pair<Bool, GenericRankingPlayer>>> = 0L to emptyList()
|
||||
private val rankingCacheDuration = 240_000
|
||||
@API("ranking")
|
||||
fun ranking(@RP token: String?): List<GenericRankingPlayer> {
|
||||
val reqUser = token?.let { us.jwt.auth(it) { u ->
|
||||
val time = millis()
|
||||
val tableName = when (name) { "mai2" -> "maimai2"; "chu3" -> "chusan"; else -> name }
|
||||
|
||||
// Check if ranking cache needs to be updated
|
||||
// TODO: pagination
|
||||
if (time - rankingCache.first > rankingCacheDuration) {
|
||||
rankingCache = time to us.em.createNativeQuery(
|
||||
"""
|
||||
SELECT
|
||||
u.id,
|
||||
u.user_name,
|
||||
u.player_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 AS hide,
|
||||
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
|
||||
ORDER BY u.player_rating DESC;
|
||||
"""
|
||||
).exec.mapIndexed { i, it ->
|
||||
it[7].truthy to GenericRankingPlayer(
|
||||
rank = i + 1,
|
||||
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]}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 }) null
|
||||
else u
|
||||
} }
|
||||
val cacheKey = reqUser?.auId ?: -1
|
||||
|
||||
// Read from cache if we just computed it less than duration ago
|
||||
rankingCache[cacheKey]?.let { (t, r) ->
|
||||
if (millis() - t < rankingCacheDuration) return r
|
||||
}
|
||||
|
||||
// TODO: pagination
|
||||
// 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 players = userDataRepo.findAll().sortedByDescending { it.playerRating }
|
||||
.filter { (it.card?.rankingBanned != true && it.card?.aquaUser?.optOutOfLeaderboard != true) || it.card?.aquaUser?.let { it == reqUser } ?: false }
|
||||
return players.filter { it.card != null }.mapIndexed { i, user ->
|
||||
val card = user.card!!
|
||||
val plays = playlogRepo.findByUserCardExtId(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 = (if (card.isGhost) user.card!!.aquaUser?.username else null) ?: "user${user.card!!.id}"
|
||||
)
|
||||
}.also { rankingCache[cacheKey] = millis() to it } // Update cache
|
||||
return rankingCache.r.filter { !it.l || it.r.username == reqUser?.username }.map { it.r }.also {
|
||||
logger.info("Ranking computed in ${millis() - time}ms")
|
||||
}
|
||||
}
|
||||
|
||||
@API("playlog")
|
||||
|
|
Loading…
Reference in New Issue