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.engine.cio.*
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import jakarta.persistence.Query
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.apache.tika.Tika
|
import org.apache.tika.Tika
|
||||||
|
@ -138,7 +139,26 @@ fun Any.long() = when (this) {
|
||||||
}
|
}
|
||||||
fun Any.uint32() = long() and 0xFFFFFFFF
|
fun Any.uint32() = long() and 0xFFFFFFFF
|
||||||
fun Any.int() = long().toInt()
|
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
|
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
|
// Collections
|
||||||
fun <T> ls(vararg args: T) = args.toList()
|
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)
|
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>.component6(): E = get(5)
|
||||||
operator fun <E> List<E>.component7(): E = get(6)
|
operator fun <E> List<E>.component7(): E = get(6)
|
||||||
operator fun <E> List<E>.component8(): E = get(7)
|
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>.component11(): E = get(10)
|
||||||
operator fun <E> List<E>.component12(): E = get(11)
|
operator fun <E> List<E>.component12(): E = get(11)
|
||||||
operator fun <E> List<E>.component13(): E = get(12)
|
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.jvm.optionals.getOrNull
|
||||||
import kotlin.reflect.KClass
|
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")
|
val musicMapping = resJson<Map<String, GenericMusicMeta>>("/meta/$name/music.json")
|
||||||
?.mapKeys { it.key.toInt() } ?: emptyMap()
|
?.mapKeys { it.key.toInt() } ?: emptyMap()
|
||||||
val logger = LoggerFactory.getLogger(javaClass)
|
val logger = LoggerFactory.getLogger(javaClass)
|
||||||
|
@ -31,41 +31,61 @@ abstract class GameApiController<T : IUserData>(name: String, userDataClass: KCl
|
||||||
playlogRepo.findByUserCardExtId(card.extId)
|
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
|
private val rankingCacheDuration = 240_000
|
||||||
@API("ranking")
|
@API("ranking")
|
||||||
fun ranking(@RP token: String?): List<GenericRankingPlayer> {
|
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
|
// 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
|
if (!u.ghostCard.rankingBanned && !u.cards.any { it.rankingBanned }) null
|
||||||
else u
|
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
|
// 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 }
|
return rankingCache.r.filter { !it.l || it.r.username == reqUser?.username }.map { it.r }.also {
|
||||||
.filter { (it.card?.rankingBanned != true && it.card?.aquaUser?.optOutOfLeaderboard != true) || it.card?.aquaUser?.let { it == reqUser } ?: false }
|
logger.info("Ranking computed in ${millis() - time}ms")
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@API("playlog")
|
@API("playlog")
|
||||||
|
|
Loading…
Reference in New Issue