mirror of
https://github.com/hykilpikonna/AquaDX.git
synced 2026-08-22 17:24:39 -05:00
fix: improve ongeki ux, improve ranking backend
This commit is contained in:
@@ -81,8 +81,7 @@ export interface GenericRanking {
|
||||
rating: number
|
||||
fullCombo: number
|
||||
allPerfect: number
|
||||
lastSeen: string,
|
||||
modernRating: number
|
||||
lastSeen: string
|
||||
}
|
||||
|
||||
export interface RankCount {
|
||||
@@ -97,7 +96,8 @@ export interface GenericGameSummary {
|
||||
serverRank: number
|
||||
accuracy: number
|
||||
rating: number
|
||||
ratingHighest: number
|
||||
ratingHighest: number,
|
||||
ratingNotGeneric: boolean,
|
||||
ranks: RankCount[]
|
||||
detailedRanks: { [key: number]: { [key: string]: number } }
|
||||
maxCombo: number
|
||||
|
||||
@@ -2,8 +2,8 @@ export const EN_REF_USER = {
|
||||
'UserHome.ServerRank': 'Server Rank',
|
||||
'UserHome.DXRating': 'DX Rating',
|
||||
'UserHome.Rating': 'Rating',
|
||||
'UserHome.LegacyRating': 'Legacy Rating',
|
||||
'UserHome.ModernRating': 'Modern Rating',
|
||||
'UserHome.HighestDXRating': 'Peak DX Rating',
|
||||
'UserHome.HighestRating': 'Peak Rating',
|
||||
'UserHome.Statistics': 'Statistics',
|
||||
'UserHome.Accuracy': 'Accuracy',
|
||||
'UserHome.MaxCombo': 'Max Combo',
|
||||
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
const zhUser: typeof EN_REF_USER = {
|
||||
'UserHome.ServerRank': '服务器排名',
|
||||
'UserHome.DXRating': 'DX B50',
|
||||
'UserHome.Rating': '评分',
|
||||
'UserHome.LegacyRating': '旧评分',
|
||||
'UserHome.ModernRating': '新评分',
|
||||
'UserHome.Rating': '最高评分',
|
||||
'UserHome.HighestDXRating': 'DX B50',
|
||||
'UserHome.HighestRating': '最高评分',
|
||||
'UserHome.Statistics': '统计数据',
|
||||
'UserHome.Accuracy': '准确率',
|
||||
'UserHome.MaxCombo': '最大连击',
|
||||
|
||||
@@ -22,8 +22,3 @@ export interface OngekiUserSummaryEntry {
|
||||
recent10: string
|
||||
recent: GenericGamePlaylog[]
|
||||
}
|
||||
|
||||
export interface OngekiRefreshData {
|
||||
playerRating: number,
|
||||
highestRating: number
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import type {
|
||||
Dict,
|
||||
GameUserOption
|
||||
} from './generalTypes'
|
||||
import type { OngekiRefreshData } from './ongekiTypes'
|
||||
import type { GameName } from './scoring'
|
||||
|
||||
interface ExtReqInit extends RequestInit {
|
||||
@@ -245,8 +244,6 @@ export const GAME = {
|
||||
post(`/api/v2/game/${game}/import-music-detail`, {}, { json: data }),
|
||||
setRival: (game: GameName, rivalUserName: string, isAdd: boolean) =>
|
||||
post(`/api/v2/game/${game}/set-rival`, { rivalUserName, isAdd }),
|
||||
refreshData: (username: string): Promise<OngekiRefreshData> =>
|
||||
post(`/api/v2/game/ongeki/refresh-data`, { username })
|
||||
}
|
||||
|
||||
export const DATA = {
|
||||
@@ -263,9 +260,9 @@ export const SETTING = {
|
||||
post('/api/v2/settings/set', { key, value: `${value}` }),
|
||||
detailSet: (game: string, field: string, value: any) =>
|
||||
post(`/api/v2/game/${game}/user-detail-set`, { field, value }),
|
||||
optionGet: (game: string): Promise<GameUserOption> =>
|
||||
optionGet: (game: string): Promise<GameUserOption> =>
|
||||
post(`/api/v2/game/${game}/user-option`),
|
||||
optionSet: (game: string, field: string, value: number): Promise<void> =>
|
||||
optionSet: (game: string, field: string, value: number): Promise<void> =>
|
||||
post(`/api/v2/game/${game}/user-option-set`, { field, value }),
|
||||
}
|
||||
|
||||
|
||||
@@ -52,12 +52,7 @@
|
||||
<div class="lb-user" on:mouseenter={() => hoveringUser = ""} role="heading" aria-level="2">
|
||||
<span class="rank">{t("Leaderboard.Rank")}</span>
|
||||
<span class="name"></span>
|
||||
{#if game == "ongeki"}
|
||||
<span class="rating modern-rating">{t("Leaderboard.ModernRating")}</span>
|
||||
<span class="rating legacy-rating">{t("Leaderboard.LegacyRating")}</span>
|
||||
{:else}
|
||||
<span class="rating">{t("Leaderboard.Rating")}</span>
|
||||
{/if}
|
||||
<span class="rating">{t("Leaderboard.Rating")}</span>
|
||||
<span class="accuracy">{t("Leaderboard.Accuracy")}</span>
|
||||
<span class="fc">{t("Leaderboard.FC")}</span>
|
||||
<span class="ap">{t("Leaderboard.AP")}</span>
|
||||
@@ -76,13 +71,8 @@
|
||||
{/if}
|
||||
</span>
|
||||
{#if game == 'ongeki'}
|
||||
{#if user.modernRating > 0}
|
||||
<span class="rating modern-rating">{
|
||||
(user.modernRating / 1000).toFixed(3)
|
||||
}</span>
|
||||
{/if}
|
||||
<span class="rating legacy-rating">{
|
||||
(user.rating / 100).toFixed(2)
|
||||
<span class="rating">{
|
||||
(user.rating / 1000).toFixed(3)
|
||||
}</span>
|
||||
{:else}
|
||||
<span class="rating">{
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
import useLocalStorage from "../libs/hooks/useLocalStorage.svelte";
|
||||
import Line from "../components/chart/Line.svelte";
|
||||
import ChuniUserboxDisplay from "../components/settings/userbox/ChuniUserboxDisplay.svelte";
|
||||
import type { OngekiRefreshData } from "../libs/ongekiTypes";
|
||||
|
||||
const TREND_DAYS = 60
|
||||
|
||||
@@ -35,7 +34,6 @@
|
||||
let calElement: HTMLElement
|
||||
let error: string;
|
||||
let me: AquaNetUser
|
||||
title(`User ${username}`)
|
||||
const rounding = useLocalStorage("rounding", true);
|
||||
|
||||
interface MusicAndPlay extends MusicMeta, GenericGamePlaylog {}
|
||||
@@ -52,7 +50,6 @@
|
||||
let isLoading = false
|
||||
let showMoreRecent = false
|
||||
|
||||
let ongekiData: OngekiRefreshData
|
||||
|
||||
function init() {
|
||||
USER.isLoggedIn() && USER.me().then(u => me = u)
|
||||
@@ -85,6 +82,8 @@
|
||||
console.log(trend)
|
||||
console.log(games)
|
||||
|
||||
title(` ${user.name} (@${username}) ・ ${GAME_TITLE[game]}`)
|
||||
|
||||
// If game is wacca, divide all ratings by 10
|
||||
if (game === 'wacca') {
|
||||
user.rating /= 10
|
||||
@@ -95,11 +94,6 @@
|
||||
})
|
||||
}
|
||||
|
||||
if (game == 'ongeki') {
|
||||
let ogkData = await GAME.refreshData(username).catch(err => {});
|
||||
if(ogkData) ongekiData = ogkData
|
||||
}
|
||||
|
||||
// Set beforeRating in recent to the last play's afterRating
|
||||
user.recent.forEach((it, i) => {
|
||||
if (i < user.recent.length - 1) {
|
||||
@@ -195,23 +189,19 @@
|
||||
<div class="scoring-info">
|
||||
<div class="chart">
|
||||
<div class="info-top">
|
||||
|
||||
|
||||
{#if game == "ongeki" && ongekiData}
|
||||
<!-- display modern (refresh) and legacy rating -->
|
||||
|
||||
{#if game == "ongeki" && d.user.ratingNotGeneric}
|
||||
<div class="rating">
|
||||
<span>{t("UserHome.ModernRating")}</span>
|
||||
<span>{(ongekiData.playerRating / 1000).toFixed(3)}</span>
|
||||
</div>
|
||||
<div class="rating">
|
||||
<span>{t("UserHome.LegacyRating")}</span>
|
||||
<span>{(d.user.rating / 100).toFixed(2)}</span>
|
||||
<span>{t("UserHome.Rating")}</span>
|
||||
<span>{(d.user.rating / 1000).toFixed(3)}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rating">
|
||||
<span>{game === 'mai2' ? t("UserHome.DXRating"): t("UserHome.Rating")}</span>
|
||||
<!-- NOTE: this is for legacy ongeki display, so 1.45 profiles are at least usable -->
|
||||
<span>{
|
||||
game === 'chu3' ?
|
||||
game === 'chu3' || game === 'ongeki' ?
|
||||
(d.user.rating / 100).toFixed(2) :
|
||||
d.user.rating.toLocaleString()
|
||||
}</span>
|
||||
@@ -220,7 +210,7 @@
|
||||
|
||||
<div class="rank">
|
||||
<span>{t('UserHome.ServerRank')}</span>
|
||||
<span>#{(d.user.serverRank + 1).toLocaleString()}</span>
|
||||
<span>#{(d.user.serverRank).toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -269,6 +259,23 @@
|
||||
<span>{(d.user.accuracy).toFixed(2)}%</span>
|
||||
</div>
|
||||
|
||||
{#if game == "ongeki" && d.user.ratingNotGeneric}
|
||||
<div class="rating">
|
||||
<span>{t("UserHome.HighestRating")}</span>
|
||||
<span>{(d.user.ratingHighest / 1000).toFixed(3)}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rating">
|
||||
<span>{game === 'mai2' ? t("UserHome.HighestDXRating"): t("UserHome.HighestRating")}</span>
|
||||
<!-- NOTE: this is for legacy ongeki display, so 1.45 profiles are at least usable -->
|
||||
<span>{
|
||||
game === 'chu3' || game === 'ongeki' ?
|
||||
(d.user.ratingHighest / 100).toFixed(2) :
|
||||
d.user.ratingHighest.toLocaleString()
|
||||
}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="max-combo">
|
||||
<span>{t("UserHome.MaxCombo")}</span>
|
||||
<span>{d.user.maxCombo}</span>
|
||||
@@ -335,7 +342,7 @@
|
||||
<RatingComposition title={t("UserHome.RatingComposition.New", {n : 15})} comp={d.user.ratingComposition.new15} {allMusics} {game}/>
|
||||
<RatingComposition title={t("UserHome.RatingComposition.New", {n: 10})} comp={d.user.ratingComposition.new10} {allMusics} {game}/>
|
||||
<RatingComposition title={t("UserHome.RatingComposition.Platinum", {n: 50})} comp={d.user.ratingComposition.pscore50} {allMusics} {game}/>
|
||||
|
||||
|
||||
{#if me && me.displayCandidates && d.user.aquaUser && me.username == d.user.aquaUser.username}
|
||||
<RatingComposition title={t("UserHome.RatingComposition.BestCandidates", {n: 20})} comp={d.user.ratingComposition.best20_candidates} {allMusics} {game}/>
|
||||
<RatingComposition title={t("UserHome.RatingComposition.NewCandidates", {n: 10})} comp={d.user.ratingComposition.new10_candidates} {allMusics} {game}/>
|
||||
@@ -523,7 +530,7 @@
|
||||
.scoring-info
|
||||
display: flex
|
||||
gap: vars.$gap
|
||||
max-height: 250px
|
||||
max-height: 300px
|
||||
|
||||
.chart
|
||||
flex: 0 1 790px
|
||||
@@ -604,7 +611,7 @@
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 20px
|
||||
|
||||
|
||||
background-color: rgba(white, 0.03)
|
||||
border-radius: vars.$border-radius
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -58,18 +58,7 @@ 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") }
|
||||
val v = rankingCache.also { logger.info("Ranking returned in ${millis() - time}ms") }
|
||||
|
||||
return page?.let {
|
||||
if (it < 0) (400 - "Invalid page number")
|
||||
@@ -78,7 +67,7 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
fun rakingCacheInit() = thread { rankingCacheRun() }
|
||||
fun rankingCacheInit() = thread { rankingCacheRun() }
|
||||
|
||||
// Every 20 minutes
|
||||
@Scheduled(fixedRate = 20, timeUnit = TimeUnit.MINUTES)
|
||||
@@ -92,34 +81,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 +148,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 +177,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")
|
||||
|
||||
Reference in New Issue
Block a user