From 32c0a8745cf64872d35f0d968112a4bee10cfde2 Mon Sep 17 00:00:00 2001 From: Raymond <101374892+raymonable@users.noreply.github.com> Date: Sat, 13 Jun 2026 23:04:22 -0400 Subject: [PATCH] fix: improve ongeki ux, improve ranking backend --- AquaNet/src/libs/generalTypes.ts | 6 +-- AquaNet/src/libs/i18n/en_ref.ts | 4 +- AquaNet/src/libs/i18n/zh.ts | 6 +-- AquaNet/src/libs/ongekiTypes.ts | 5 -- AquaNet/src/libs/sdk.ts | 7 +-- AquaNet/src/pages/Ranking.svelte | 16 ++---- AquaNet/src/pages/UserHome.svelte | 51 +++++++++++-------- .../aqua/net/games/GameApiController.kt | 48 +++++++---------- .../java/icu/samnyan/aqua/net/games/Models.kt | 8 ++- .../samnyan/aqua/net/games/mai2/Maimai2.kt | 19 ++++++- .../samnyan/aqua/net/games/ongeki/Ongeki.kt | 14 ++--- 11 files changed, 89 insertions(+), 95 deletions(-) diff --git a/AquaNet/src/libs/generalTypes.ts b/AquaNet/src/libs/generalTypes.ts index bd79701b..877d897b 100644 --- a/AquaNet/src/libs/generalTypes.ts +++ b/AquaNet/src/libs/generalTypes.ts @@ -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 diff --git a/AquaNet/src/libs/i18n/en_ref.ts b/AquaNet/src/libs/i18n/en_ref.ts index c4be6629..b110008a 100644 --- a/AquaNet/src/libs/i18n/en_ref.ts +++ b/AquaNet/src/libs/i18n/en_ref.ts @@ -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', diff --git a/AquaNet/src/libs/i18n/zh.ts b/AquaNet/src/libs/i18n/zh.ts index a5e7e380..ca416d36 100644 --- a/AquaNet/src/libs/i18n/zh.ts +++ b/AquaNet/src/libs/i18n/zh.ts @@ -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': '最大连击', diff --git a/AquaNet/src/libs/ongekiTypes.ts b/AquaNet/src/libs/ongekiTypes.ts index a5981dbd..5f107fe2 100644 --- a/AquaNet/src/libs/ongekiTypes.ts +++ b/AquaNet/src/libs/ongekiTypes.ts @@ -22,8 +22,3 @@ export interface OngekiUserSummaryEntry { recent10: string recent: GenericGamePlaylog[] } - -export interface OngekiRefreshData { - playerRating: number, - highestRating: number -} \ No newline at end of file diff --git a/AquaNet/src/libs/sdk.ts b/AquaNet/src/libs/sdk.ts index 2ad3ac98..7b1e41da 100644 --- a/AquaNet/src/libs/sdk.ts +++ b/AquaNet/src/libs/sdk.ts @@ -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 => - 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 => + optionGet: (game: string): Promise => post(`/api/v2/game/${game}/user-option`), - optionSet: (game: string, field: string, value: number): Promise => + optionSet: (game: string, field: string, value: number): Promise => post(`/api/v2/game/${game}/user-option-set`, { field, value }), } diff --git a/AquaNet/src/pages/Ranking.svelte b/AquaNet/src/pages/Ranking.svelte index e9500a6b..b80c3bda 100644 --- a/AquaNet/src/pages/Ranking.svelte +++ b/AquaNet/src/pages/Ranking.svelte @@ -52,12 +52,7 @@
hoveringUser = ""} role="heading" aria-level="2"> {t("Leaderboard.Rank")} - {#if game == "ongeki"} - {t("Leaderboard.ModernRating")} - {t("Leaderboard.LegacyRating")} - {:else} - {t("Leaderboard.Rating")} - {/if} + {t("Leaderboard.Rating")} {t("Leaderboard.Accuracy")} {t("Leaderboard.FC")} {t("Leaderboard.AP")} @@ -76,13 +71,8 @@ {/if} {#if game == 'ongeki'} - {#if user.modernRating > 0} - { - (user.modernRating / 1000).toFixed(3) - } - {/if} - { - (user.rating / 100).toFixed(2) + { + (user.rating / 1000).toFixed(3) } {:else} { diff --git a/AquaNet/src/pages/UserHome.svelte b/AquaNet/src/pages/UserHome.svelte index 2521fb94..7b105f11 100644 --- a/AquaNet/src/pages/UserHome.svelte +++ b/AquaNet/src/pages/UserHome.svelte @@ -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 @@
- - {#if game == "ongeki" && ongekiData} - + + {#if game == "ongeki" && d.user.ratingNotGeneric}
- {t("UserHome.ModernRating")} - {(ongekiData.playerRating / 1000).toFixed(3)} -
-
- {t("UserHome.LegacyRating")} - {(d.user.rating / 100).toFixed(2)} + {t("UserHome.Rating")} + {(d.user.rating / 1000).toFixed(3)}
{:else}
{game === 'mai2' ? t("UserHome.DXRating"): t("UserHome.Rating")} + { - game === 'chu3' ? + game === 'chu3' || game === 'ongeki' ? (d.user.rating / 100).toFixed(2) : d.user.rating.toLocaleString() } @@ -220,7 +210,7 @@
{t('UserHome.ServerRank')} - #{(d.user.serverRank + 1).toLocaleString()} + #{(d.user.serverRank).toLocaleString()}
@@ -269,6 +259,23 @@ {(d.user.accuracy).toFixed(2)}%
+ {#if game == "ongeki" && d.user.ratingNotGeneric} +
+ {t("UserHome.HighestRating")} + {(d.user.ratingHighest / 1000).toFixed(3)} +
+ {:else} +
+ {game === 'mai2' ? t("UserHome.HighestDXRating"): t("UserHome.HighestRating")} + + { + game === 'chu3' || game === 'ongeki' ? + (d.user.ratingHighest / 100).toFixed(2) : + d.user.ratingHighest.toLocaleString() + } +
+ {/if} +
{t("UserHome.MaxCombo")} {d.user.maxCombo} @@ -335,7 +342,7 @@ - + {#if me && me.displayCandidates && d.user.aquaUser && me.username == d.user.aquaUser.username} @@ -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 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 1a28cb37..d09f4d32 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/GameApiController.kt @@ -46,10 +46,10 @@ abstract class GameApiController(val name: String, userDataClass: } // List>> - private var rankingCache: List> = emptyList() + private var rankingCache: List = emptyList() private var rankingCacheLock = ReentrantLock() // Sorted index List = Rank - private var rankingSortedIndex: List = emptyList() + private var rankingLookupCache: Map = emptyMap() private val pageSize = 100 @API("ranking") @@ -58,18 +58,7 @@ abstract class GameApiController(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(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(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(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, rival: Boolean? = null, favorites: List? = 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(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, diff --git a/src/main/java/icu/samnyan/aqua/net/games/Models.kt b/src/main/java/icu/samnyan/aqua/net/games/Models.kt index a4bcc14e..01f5809e 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/Models.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/Models.kt @@ -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?, - val serverRank: Long, + val serverRank: String, val accuracy: Double, val rating: Int, val ratingHighest: Int, + val ratingNotGeneric: Bool, val ranks: List, val detailedRanks: Map>, 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 diff --git a/src/main/java/icu/samnyan/aqua/net/games/mai2/Maimai2.kt b/src/main/java/icu/samnyan/aqua/net/games/mai2/Maimai2.kt index 92b45676..32469942 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/mai2/Maimai2.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/mai2/Maimai2.kt @@ -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() diff --git a/src/main/java/icu/samnyan/aqua/net/games/ongeki/Ongeki.kt b/src/main/java/icu/samnyan/aqua/net/games/ongeki/Ongeki.kt index 4f6577d2..ab74907e 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/ongeki/Ongeki.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/ongeki/Ongeki.kt @@ -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")