Refactored user scores

This commit is contained in:
nise.moe 2024-03-06 15:07:12 +01:00
parent 825ac523c2
commit f3d85b47b0
15 changed files with 86 additions and 616 deletions

View File

@ -112,64 +112,11 @@ data class ReplayPairViewerData(
val judgements2: List<CircleguardService.ScoreJudgement> val judgements2: List<CircleguardService.ScoreJudgement>
) )
data class UserReplayData(
val replay_id: UUID,
val osu_replay_id: Long?,
val username: String,
val beatmap_id: Int,
val beatmap_beatmapset_id: Int,
val beatmap_artist: String,
val beatmap_title: String,
val beatmap_star_rating: Double,
val beatmap_creator: String,
val beatmap_version: String,
val score: Int,
val mods: List<String>,
val ur: Double?,
val adjusted_ur: Double?,
val frametime: Int,
val snaps: Int,
val hits: Int,
var mean_error: Double?,
var error_variance: Double?,
var error_standard_deviation: Double?,
var minimum_error: Double?,
var maximum_error: Double?,
var error_range: Double?,
var error_coefficient_of_variation: Double?,
var error_kurtosis: Double?,
var error_skewness: Double?,
var comparable_samples: Int? = null,
var comparable_mean_error: Double? = null,
var comparable_error_variance: Double? = null,
var comparable_error_standard_deviation: Double? = null,
var comparable_minimum_error: Double? = null,
var comparable_maximum_error: Double? = null,
var comparable_error_range: Double? = null,
var comparable_error_coefficient_of_variation: Double? = null,
var comparable_error_kurtosis: Double? = null,
var comparable_error_skewness: Double? = null,
val perfect: Boolean,
val max_combo: Int,
val count_300: Int,
val count_100: Int,
val count_50: Int,
val count_miss: Int,
val similar_scores: List<ReplayDataSimilarScore>,
val error_distribution: Map<Int, DistributionEntry>,
val charts: List<ReplayDataChart>
)
data class ReplayData( data class ReplayData(
val replay_id: Long, val replay_id: Long,
val user_id: Int, val user_id: Int?,
val username: String, val username: String,
val date: String, val date: String?,
val beatmap_id: Int, val beatmap_id: Int,
val beatmap_beatmapset_id: Int, val beatmap_beatmapset_id: Int,
val beatmap_artist: String, val beatmap_artist: String,
@ -189,10 +136,9 @@ data class ReplayData(
val beatmap_count_spinners: Int?, val beatmap_count_spinners: Int?,
val score: Int, val score: Int,
val mods: List<String>, val mods: List<String>,
val rank: String, val rank: String?,
val ur: Double?, val ur: Double?,
val adjusted_ur: Double?, val adjusted_ur: Double?,
val average_ur: Double?,
val frametime: Int, val frametime: Int,
val snaps: Int, val snaps: Int,
val hits: Int, val hits: Int,
@ -217,8 +163,9 @@ data class ReplayData(
var comparable_error_coefficient_of_variation: Double? = null, var comparable_error_coefficient_of_variation: Double? = null,
var comparable_error_kurtosis: Double? = null, var comparable_error_kurtosis: Double? = null,
var comparable_error_skewness: Double? = null, var comparable_error_skewness: Double? = null,
var comparable_adjusted_ur: Double? = null,
val pp: Double, val pp: Double?,
val perfect: Boolean, val perfect: Boolean,
val max_combo: Int, val max_combo: Int,

View File

@ -49,7 +49,7 @@ class ScoreController(
// Sort replays by date (the first replay will always be the oldest) // Sort replays by date (the first replay will always be the oldest)
val replays = listOf(replay1Data, replay2Data) val replays = listOf(replay1Data, replay2Data)
.sortedBy { Format.parseStringToDate(it.date) } .sortedBy { Format.parseStringToDate(it.date!!) }
val replayPair = ReplayPair(replays, replayPairStatistics) val replayPair = ReplayPair(replays, replayPairStatistics)

View File

@ -1,12 +1,12 @@
package com.nisemoe.nise.controller package com.nisemoe.nise.controller
import com.nisemoe.nise.UserReplayData import com.nisemoe.nise.ReplayData
import com.nisemoe.nise.database.UserScoreService import com.nisemoe.nise.database.UserScoreService
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import java.util.UUID import java.util.*
@RestController @RestController
class UserScoresController( class UserScoresController(
@ -14,7 +14,7 @@ class UserScoresController(
) { ) {
@GetMapping("user-score/{replayId}") @GetMapping("user-score/{replayId}")
fun getScoreDetails(@PathVariable replayId: UUID): ResponseEntity<UserReplayData> { fun getScoreDetails(@PathVariable replayId: UUID): ResponseEntity<ReplayData> {
val replayData = this.userScoreService.getReplayData(replayId) val replayData = this.userScoreService.getReplayData(replayId)
?: return ResponseEntity.notFound().build() ?: return ResponseEntity.notFound().build()

View File

@ -44,23 +44,4 @@ class BeatmapService(
} }
} }
fun getAverageUR(beatmapId: Int, excludeReplayId: Long): Double? {
val condition = SCORES.BEATMAP_ID.eq(beatmapId)
.and(SCORES.UR.isNotNull)
.and(SCORES.REPLAY_ID.notEqual(excludeReplayId))
val totalScores = dslContext.fetchCount(
SCORES, condition
)
if(totalScores < 50)
return null
return dslContext
.select(avg(SCORES.UR))
.from(SCORES)
.where(condition)
.fetchOneInto(Double::class.java)
}
} }

View File

@ -174,7 +174,6 @@ class ScoreService(
.fetchOne() ?: return null .fetchOne() ?: return null
val beatmapId = result.get(BEATMAPS.BEATMAP_ID, Int::class.java) val beatmapId = result.get(BEATMAPS.BEATMAP_ID, Int::class.java)
val averageUR = beatmapService.getAverageUR(beatmapId = beatmapId, excludeReplayId = replayId)
val hitDistribution = this.getHitDistribution(scoreId = result.get(SCORES.ID, Int::class.java)) val hitDistribution = this.getHitDistribution(scoreId = result.get(SCORES.ID, Int::class.java))
val charts = this.getCharts(result) val charts = this.getCharts(result)
@ -207,7 +206,6 @@ class ScoreService(
score = result.get(SCORES.SCORE, Int::class.java), score = result.get(SCORES.SCORE, Int::class.java),
mods = Mod.parseModCombination(result.get(SCORES.MODS, Int::class.java)), mods = Mod.parseModCombination(result.get(SCORES.MODS, Int::class.java)),
rank = result.get(SCORES.RANK, String::class.java), rank = result.get(SCORES.RANK, String::class.java),
average_ur = averageUR,
snaps = result.get(SCORES.SNAPS, Int::class.java), snaps = result.get(SCORES.SNAPS, Int::class.java),
hits = result.get(SCORES.EDGE_HITS, Int::class.java), hits = result.get(SCORES.EDGE_HITS, Int::class.java),
perfect = result.get(SCORES.PERFECT, Boolean::class.java), perfect = result.get(SCORES.PERFECT, Boolean::class.java),
@ -482,7 +480,8 @@ class ScoreService(
avg(SCORES.ERROR_RANGE).`as`("avg_error_range"), avg(SCORES.ERROR_RANGE).`as`("avg_error_range"),
avg(SCORES.ERROR_COEFFICIENT_OF_VARIATION).`as`("avg_error_coefficient_of_variation"), avg(SCORES.ERROR_COEFFICIENT_OF_VARIATION).`as`("avg_error_coefficient_of_variation"),
avg(SCORES.ERROR_KURTOSIS).`as`("avg_error_kurtosis"), avg(SCORES.ERROR_KURTOSIS).`as`("avg_error_kurtosis"),
avg(SCORES.ERROR_SKEWNESS).`as`("avg_error_skewness") avg(SCORES.ERROR_SKEWNESS).`as`("avg_error_skewness"),
avg(SCORES.ADJUSTED_UR).`as`("avg_adjusted_ur")
) )
.from(SCORES) .from(SCORES)
.where(SCORES.BEATMAP_ID.eq(replayData.beatmap_id)) .where(SCORES.BEATMAP_ID.eq(replayData.beatmap_id))
@ -500,6 +499,7 @@ class ScoreService(
replayData.comparable_error_coefficient_of_variation = otherScores.get("avg_error_coefficient_of_variation", Double::class.java) replayData.comparable_error_coefficient_of_variation = otherScores.get("avg_error_coefficient_of_variation", Double::class.java)
replayData.comparable_error_kurtosis = otherScores.get("avg_error_kurtosis", Double::class.java) replayData.comparable_error_kurtosis = otherScores.get("avg_error_kurtosis", Double::class.java)
replayData.comparable_error_skewness = otherScores.get("avg_error_skewness", Double::class.java) replayData.comparable_error_skewness = otherScores.get("avg_error_skewness", Double::class.java)
replayData.comparable_adjusted_ur = otherScores.get("avg_adjusted_ur", Double::class.java)
} }
fun mapLegacyJudgement(judgementType: JudgementType): CircleguardService.JudgementType { fun mapLegacyJudgement(judgementType: JudgementType): CircleguardService.JudgementType {

View File

@ -1,13 +1,13 @@
package com.nisemoe.nise.database package com.nisemoe.nise.database
import com.nisemoe.generated.tables.references.* import com.nisemoe.generated.tables.references.*
import com.nisemoe.nise.* import com.nisemoe.nise.DistributionEntry
import com.nisemoe.nise.Format
import com.nisemoe.nise.ReplayData
import com.nisemoe.nise.ReplayDataSimilarScore
import com.nisemoe.nise.osu.Mod import com.nisemoe.nise.osu.Mod
import com.nisemoe.nise.service.AuthService
import com.nisemoe.nise.service.CompressJudgements import com.nisemoe.nise.service.CompressJudgements
import org.jooq.DSLContext import org.jooq.DSLContext
import org.jooq.Record
import org.jooq.impl.DSL
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.* import java.util.*
@ -16,18 +16,11 @@ import kotlin.math.roundToInt
@Service @Service
class UserScoreService( class UserScoreService(
private val dslContext: DSLContext, private val dslContext: DSLContext,
private val authService: AuthService, private val scoreService: ScoreService,
private val compressJudgements: CompressJudgements private val compressJudgements: CompressJudgements
) { ) {
fun getCharts(db: Record): List<ReplayDataChart> { fun getReplayData(replayId: UUID): ReplayData? {
if (!authService.isAdmin()) return emptyList()
return listOf(USER_SCORES.SLIDEREND_RELEASE_TIMES to "slider end release times", USER_SCORES.KEYPRESSES_TIMES to "keypress times")
.mapNotNull { (scoreType, title) -> db.get(scoreType)?.let { ReplayDataChart(title, it.filterNotNull()) } }
}
fun getReplayData(replayId: UUID): UserReplayData? {
val result = dslContext.select( val result = dslContext.select(
USER_SCORES.ID, USER_SCORES.ID,
USER_SCORES.PLAYER_NAME, USER_SCORES.PLAYER_NAME,
@ -38,6 +31,16 @@ class UserScoreService(
BEATMAPS.STAR_RATING, BEATMAPS.STAR_RATING,
BEATMAPS.CREATOR, BEATMAPS.CREATOR,
BEATMAPS.VERSION, BEATMAPS.VERSION,
BEATMAPS.TOTAL_LENGTH,
BEATMAPS.MAX_COMBO,
BEATMAPS.BPM,
BEATMAPS.ACCURACY,
BEATMAPS.AR,
BEATMAPS.CS,
BEATMAPS.DRAIN,
BEATMAPS.COUNT_CIRCLES,
BEATMAPS.COUNT_SPINNERS,
BEATMAPS.COUNT_SLIDERS,
USER_SCORES.ONLINE_SCORE_ID, USER_SCORES.ONLINE_SCORE_ID,
USER_SCORES.TOTAL_SCORE, USER_SCORES.TOTAL_SCORE,
USER_SCORES.FRAMETIME, USER_SCORES.FRAMETIME,
@ -72,11 +75,10 @@ class UserScoreService(
val beatmapId = result.get(BEATMAPS.BEATMAP_ID, Int::class.java) val beatmapId = result.get(BEATMAPS.BEATMAP_ID, Int::class.java)
val hitDistribution = this.getHitDistribution(result.get(USER_SCORES.JUDGEMENTS, ByteArray::class.java)) val hitDistribution = this.getHitDistribution(result.get(USER_SCORES.JUDGEMENTS, ByteArray::class.java))
val charts = this.getCharts(result) val charts = this.scoreService.getCharts(result)
val replayData = UserReplayData( val replayData = ReplayData(
replay_id = replayId, replay_id = result.get(USER_SCORES.ONLINE_SCORE_ID, Long::class.java),
osu_replay_id = result.get(USER_SCORES.ONLINE_SCORE_ID, Long::class.java),
username = result.get(USER_SCORES.PLAYER_NAME, String::class.java), username = result.get(USER_SCORES.PLAYER_NAME, String::class.java),
beatmap_id = beatmapId, beatmap_id = beatmapId,
beatmap_beatmapset_id = result.get(BEATMAPS.BEATMAPSET_ID, Int::class.java), beatmap_beatmapset_id = result.get(BEATMAPS.BEATMAPSET_ID, Int::class.java),
@ -85,6 +87,16 @@ class UserScoreService(
beatmap_star_rating = result.get(BEATMAPS.STAR_RATING, Double::class.java), beatmap_star_rating = result.get(BEATMAPS.STAR_RATING, Double::class.java),
beatmap_creator = result.get(BEATMAPS.CREATOR, String::class.java), beatmap_creator = result.get(BEATMAPS.CREATOR, String::class.java),
beatmap_version = result.get(BEATMAPS.VERSION, String::class.java), beatmap_version = result.get(BEATMAPS.VERSION, String::class.java),
beatmap_bpm = result.get(BEATMAPS.BPM, Double::class.java),
beatmap_max_combo = result.get(BEATMAPS.MAX_COMBO, Int::class.java),
beatmap_total_length = result.get(BEATMAPS.TOTAL_LENGTH, Int::class.java),
beatmap_accuracy = result.get(BEATMAPS.ACCURACY, Double::class.java),
beatmap_ar = result.get(BEATMAPS.AR, Double::class.java),
beatmap_cs = result.get(BEATMAPS.CS, Double::class.java),
beatmap_drain = result.get(BEATMAPS.DRAIN, Double::class.java),
beatmap_count_circles = result.get(BEATMAPS.COUNT_CIRCLES, Int::class.java),
beatmap_count_spinners = result.get(BEATMAPS.COUNT_SPINNERS, Int::class.java),
beatmap_count_sliders = result.get(BEATMAPS.COUNT_SLIDERS, Int::class.java),
frametime = result.get(USER_SCORES.FRAMETIME, Double::class.java).toInt(), frametime = result.get(USER_SCORES.FRAMETIME, Double::class.java).toInt(),
ur = result.get(USER_SCORES.UR, Double::class.java), ur = result.get(USER_SCORES.UR, Double::class.java),
adjusted_ur = result.get(USER_SCORES.ADJUSTED_UR, Double::class.java), adjusted_ur = result.get(USER_SCORES.ADJUSTED_UR, Double::class.java),
@ -109,9 +121,13 @@ class UserScoreService(
error_kurtosis = result.get(USER_SCORES.ERROR_KURTOSIS, Double::class.java), error_kurtosis = result.get(USER_SCORES.ERROR_KURTOSIS, Double::class.java),
error_skewness = result.get(USER_SCORES.ERROR_SKEWNESS, Double::class.java), error_skewness = result.get(USER_SCORES.ERROR_SKEWNESS, Double::class.java),
charts = charts, charts = charts,
similar_scores = this.getSimilarScores(replayId) similar_scores = this.getSimilarScores(replayId),
date = null,
pp = null,
rank = null,
user_id = null
) )
this.loadComparableReplayData(replayData) this.scoreService.loadComparableReplayData(replayData)
return replayData return replayData
} }
@ -148,46 +164,6 @@ class UserScoreService(
} }
} }
fun loadComparableReplayData(replayData: UserReplayData) {
// Total samples
val totalSamples = dslContext.fetchCount(
SCORES, SCORES.BEATMAP_ID.eq(replayData.beatmap_id)
)
if(totalSamples <= 0) {
return
}
// We will select same beatmap_id and same mods
val otherScores = dslContext.select(
DSL.avg(SCORES.MEAN_ERROR).`as`("avg_mean_error"),
DSL.avg(SCORES.ERROR_VARIANCE).`as`("avg_error_variance"),
DSL.avg(SCORES.ERROR_STANDARD_DEVIATION).`as`("avg_error_standard_deviation"),
DSL.avg(SCORES.MINIMUM_ERROR).`as`("avg_minimum_error"),
DSL.avg(SCORES.MAXIMUM_ERROR).`as`("avg_maximum_error"),
DSL.avg(SCORES.ERROR_RANGE).`as`("avg_error_range"),
DSL.avg(SCORES.ERROR_COEFFICIENT_OF_VARIATION).`as`("avg_error_coefficient_of_variation"),
DSL.avg(SCORES.ERROR_KURTOSIS).`as`("avg_error_kurtosis"),
DSL.avg(SCORES.ERROR_SKEWNESS).`as`("avg_error_skewness")
)
.from(SCORES)
.where(SCORES.BEATMAP_ID.eq(replayData.beatmap_id))
.fetchOne() ?: return
replayData.comparable_samples = totalSamples
replayData.comparable_mean_error = otherScores.get("avg_mean_error", Double::class.java)
replayData.comparable_error_variance = otherScores.get("avg_error_variance", Double::class.java)
replayData.comparable_error_standard_deviation = otherScores.get("avg_error_standard_deviation", Double::class.java)
replayData.comparable_minimum_error = otherScores.get("avg_minimum_error", Double::class.java)
replayData.comparable_maximum_error = otherScores.get("avg_maximum_error", Double::class.java)
replayData.comparable_error_range = otherScores.get("avg_error_range", Double::class.java)
replayData.comparable_error_coefficient_of_variation = otherScores.get("avg_error_coefficient_of_variation", Double::class.java)
replayData.comparable_error_kurtosis = otherScores.get("avg_error_kurtosis", Double::class.java)
replayData.comparable_error_skewness = otherScores.get("avg_error_skewness", Double::class.java)
}
fun getHitDistribution(compressedJudgements: ByteArray): Map<Int, DistributionEntry> { fun getHitDistribution(compressedJudgements: ByteArray): Map<Int, DistributionEntry> {
val judgements = compressJudgements.deserialize(compressedJudgements) val judgements = compressJudgements.deserialize(compressedJudgements)

View File

@ -194,8 +194,8 @@ class SendScoresToDiscord(
) )
statisticsEmbed.addEmbed(name = "Played by", value = "[${replayData.username}](https://osu.ppy.sh/users/${replayData.user_id})") statisticsEmbed.addEmbed(name = "Played by", value = "[${replayData.username}](https://osu.ppy.sh/users/${replayData.user_id})")
statisticsEmbed.addEmbed(name = "Played at", value = replayData.date) statisticsEmbed.addEmbed(name = "Played at", value = replayData.date!!)
statisticsEmbed.addEmbed(name = "PP", value = replayData.pp.roundToInt().toString()) statisticsEmbed.addEmbed(name = "PP", value = replayData.pp?.roundToInt().toString())
if(replayData.mods.isNotEmpty()) if(replayData.mods.isNotEmpty())
statisticsEmbed.addEmbed(name = "Mods", value = replayData.mods.joinToString("")) statisticsEmbed.addEmbed(name = "Mods", value = replayData.mods.joinToString(""))
statisticsEmbed.addEmbed(name = "Max Combo", value = "${replayData.max_combo}x") statisticsEmbed.addEmbed(name = "Max Combo", value = "${replayData.max_combo}x")

View File

@ -7,7 +7,6 @@ import {ViewScoreComponent} from "./view-score/view-score.component";
import {ViewUserComponent} from "./view-user/view-user.component"; import {ViewUserComponent} from "./view-user/view-user.component";
import {ViewReplayPairComponent} from "./view-replay-pair/view-replay-pair.component"; import {ViewReplayPairComponent} from "./view-replay-pair/view-replay-pair.component";
import {SearchComponent} from "./search/search.component"; import {SearchComponent} from "./search/search.component";
import {ViewUserScoreComponent} from "./view-user-score/view-user-score.component";
const routes: Routes = [ const routes: Routes = [
{path: 'sus/:f', component: ViewSuspiciousScoresComponent, title: '/sus/'}, {path: 'sus/:f', component: ViewSuspiciousScoresComponent, title: '/sus/'},
@ -18,10 +17,10 @@ const routes: Routes = [
{path: 'u/:userId', component: ViewUserComponent}, {path: 'u/:userId', component: ViewUserComponent},
{path: 's/:replayId', component: ViewScoreComponent}, {path: 's/:replayId', component: ViewScoreComponent},
{path: 'c/:userReplayId', component: ViewScoreComponent},
{path: 'search', component: SearchComponent}, {path: 'search', component: SearchComponent},
{path: 'p/:replay1Id/:replay2Id', component: ViewReplayPairComponent},
{path: 'c/:replayId', component: ViewUserScoreComponent}, {path: 'p/:replay1Id/:replay2Id', component: ViewReplayPairComponent},
{path: '**', component: HomeComponent, title: '/nise.moe/'}, {path: '**', component: HomeComponent, title: '/nise.moe/'},
]; ];

View File

@ -1,4 +1,4 @@
import {ReplayData, UserReplayData} from "./replays"; import {ReplayData} from "./replays";
export function formatDuration(seconds: number): string | null { export function formatDuration(seconds: number): string | null {
if(!seconds) { if(!seconds) {
@ -31,7 +31,7 @@ export function countryCodeToFlag(countryCode: string): string {
return String.fromCodePoint(...codePoints); return String.fromCodePoint(...codePoints);
} }
export function calculateAccuracy(replayData: ReplayData | UserReplayData): number { export function calculateAccuracy(replayData: ReplayData): number {
if(!replayData) { if(!replayData) {
return 0; return 0;
} }

View File

@ -13,59 +13,6 @@ export interface ReplayDataSimilarScore {
correlation: number; correlation: number;
} }
export interface UserReplayData {
replay_id: string;
osu_replay_id?: string;
username: string;
beatmap_id: number;
beatmap_beatmapset_id: number;
beatmap_artist: string;
beatmap_title: string;
beatmap_star_rating: number;
beatmap_creator: string;
beatmap_version: string;
score: number;
mods: string[];
ur?: number;
adjusted_ur?: number;
frametime: number;
snaps: number;
hits: number;
mean_error?: number;
error_variance?: number;
error_standard_deviation?: number;
minimum_error?: number;
maximum_error?: number;
error_range?: number;
error_coefficient_of_variation?: number;
error_kurtosis?: number;
error_skewness?: number;
perfect: boolean;
max_combo: number;
count_300: number;
count_100: number;
count_50: number;
count_miss: number;
comparable_samples?: number;
comparable_mean_error?: number,
comparable_error_variance?: number,
comparable_error_standard_deviation?: number,
comparable_minimum_error?: number,
comparable_maximum_error?: number,
comparable_error_range?: number,
comparable_error_coefficient_of_variation?: number,
comparable_error_kurtosis?: number,
comparable_error_skewness?: number,
similar_scores: ReplayDataSimilarScore[];
error_distribution: ErrorDistribution;
charts: ReplayDataChart[];
}
export function getMockReplayData(): ReplayData { export function getMockReplayData(): ReplayData {
return { return {
'replay_id': 123, 'replay_id': 123,
@ -93,7 +40,6 @@ export function getMockReplayData(): ReplayData {
'mods': ['mods'], 'mods': ['mods'],
'rank': 'rank', 'rank': 'rank',
'ur': 123, 'ur': 123,
'average_ur': 123,
'adjusted_ur': 123, 'adjusted_ur': 123,
'frametime': 123, 'frametime': 123,
'snaps': 123, 'snaps': 123,
@ -146,7 +92,6 @@ export interface ReplayData {
mods: string[]; mods: string[];
rank: string; rank: string;
ur: number; ur: number;
average_ur: number | null;
adjusted_ur?: number; adjusted_ur?: number;
frametime: number; frametime: number;
snaps: number; snaps: number;
@ -176,6 +121,7 @@ export interface ReplayData {
comparable_error_coefficient_of_variation?: number, comparable_error_coefficient_of_variation?: number,
comparable_error_kurtosis?: number, comparable_error_kurtosis?: number,
comparable_error_skewness?: number, comparable_error_skewness?: number,
comparable_adjusted_ur?: number;
count_300: number, count_300: number,
count_100: number, count_100: number,

View File

@ -33,9 +33,11 @@
</div> </div>
</div> </div>
<div class="score-player__row score-player__row--player mt-2"> <div class="score-player__row score-player__row--player mt-2">
Played by <a [routerLink]="['/u/' + this.replayData.username]">{{ this.replayData.username }}</a> <a class="btn" style="margin-left: 5px" href="https://osu.ppy.sh/users/{{ this.replayData.user_id }}" target="_blank">osu!web</a> Played by <a [routerLink]="['/u/' + this.replayData.username]">{{ this.replayData.username }}</a> <a *ngIf="this.replayData.user_id" class="btn" style="margin-left: 5px" href="https://osu.ppy.sh/users/{{ this.replayData.user_id }}" target="_blank">osu!web</a>
<ng-container *ngIf="!this.isUserScore">
<br> <br>
Submitted on <strong>{{ this.replayData.date }}</strong> Submitted on <strong>{{ this.replayData.date }}</strong>
</ng-container>
</div> </div>
</div> </div>
</div> </div>
@ -130,7 +132,7 @@
<span class="stat-label">Max Combo</span> <span class="stat-label">Max Combo</span>
<span class="stat-value">{{ this.replayData.max_combo }}x <span *ngIf="this.replayData.perfect" class="badge badge-green">perfect</span></span> <span class="stat-value">{{ this.replayData.max_combo }}x <span *ngIf="this.replayData.perfect" class="badge badge-green">perfect</span></span>
</div> </div>
<div class="stat"> <div class="stat" *ngIf="this.replayData.pp">
<span class="stat-label">PP</span> <span class="stat-label">PP</span>
<span class="stat-value">{{ this.replayData.pp | number: '1.0-0' }}</span> <span class="stat-value">{{ this.replayData.pp | number: '1.0-0' }}</span>
</div> </div>
@ -218,10 +220,6 @@
<div class="main term mb-2" *ngIf="this.replayData.mean_error"> <div class="main term mb-2" *ngIf="this.replayData.mean_error">
<h1># nerd stats</h1> <h1># nerd stats</h1>
<div class="alert text-center mb-2" *ngIf="this.replayData.average_ur">
<p class="bold">Heads up!</p>
<p>The average cvUR for this beatmap is <span class="bold">{{ this.replayData.average_ur | number: '1.0-2' }}</span></p>
</div>
<table> <table>
<thead> <thead>
<th></th> <th></th>
@ -269,6 +267,11 @@
<td class="text-center">{{ this.replayData.error_skewness | number: '1.2-2'}}</td> <td class="text-center">{{ this.replayData.error_skewness | number: '1.2-2'}}</td>
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_error_skewness | number: '1.2-2' }}</td> <td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_error_skewness | number: '1.2-2' }}</td>
</tr> </tr>
<tr>
<td>Adjusted cvUR</td>
<td class="text-center">{{ this.replayData.adjusted_ur | number: '1.2-2'}}</td>
<td *ngIf="this.replayData.comparable_adjusted_ur" class="text-center">{{ this.replayData.comparable_adjusted_ur | number: '1.2-2' }}</td>
</tr>
</table> </table>
</div> </div>
@ -276,7 +279,7 @@
<app-chart [title]="chart.title" [data]="chart.data"></app-chart> <app-chart [title]="chart.title" [data]="chart.data"></app-chart>
</ng-container> </ng-container>
<ng-container *ngIf="hasReplay()"> <ng-container *ngIf="hasErrorDistribution()">
<app-chart-hit-distribution [errorDistribution]="replayData!!.error_distribution" [mods]="replayData!!.mods"></app-chart-hit-distribution> <app-chart-hit-distribution [errorDistribution]="replayData!!.error_distribution" [mods]="replayData!!.mods"></app-chart-hit-distribution>
</ng-container> </ng-container>
</ng-container> </ng-container>

View File

@ -1,12 +1,11 @@
import {Component, OnInit, ViewChild} from '@angular/core'; import {Component, OnInit, ViewChild} from '@angular/core';
import {ChartConfiguration} from 'chart.js';
import {BaseChartDirective, NgChartsModule} from 'ng2-charts'; import {BaseChartDirective, NgChartsModule} from 'ng2-charts';
import {HttpClient} from "@angular/common/http"; import {HttpClient} from "@angular/common/http";
import {environment} from "../../environments/environment"; import {environment} from "../../environments/environment";
import {DecimalPipe, JsonPipe, NgForOf, NgIf, NgOptimizedImage} from "@angular/common"; import {DecimalPipe, JsonPipe, NgForOf, NgIf, NgOptimizedImage} from "@angular/common";
import {ActivatedRoute, RouterLink} from "@angular/router"; import {ActivatedRoute, RouterLink} from "@angular/router";
import {catchError, throwError} from "rxjs"; import {catchError, throwError} from "rxjs";
import {DistributionEntry, ReplayData} from "../replays"; import {ReplayData} from "../replays";
import {calculateAccuracy} from "../format"; import {calculateAccuracy} from "../format";
import {Title} from "@angular/platform-browser"; import {Title} from "@angular/platform-browser";
import {OsuGradeComponent} from "../../corelib/components/osu-grade/osu-grade.component"; import {OsuGradeComponent} from "../../corelib/components/osu-grade/osu-grade.component";
@ -43,6 +42,9 @@ export class ViewScoreComponent implements OnInit {
isLoading = false; isLoading = false;
error: string | null = null; error: string | null = null;
replayData: ReplayData | null = null; replayData: ReplayData | null = null;
isUserScore: boolean = false;
userReplayId: string | null = null;
replayId: number | null = null; replayId: number | null = null;
constructor(private http: HttpClient, constructor(private http: HttpClient,
@ -50,15 +52,27 @@ export class ViewScoreComponent implements OnInit {
private title: Title private title: Title
) {} ) {}
hasReplay(): boolean { hasErrorDistribution(): boolean {
return !!this.replayData?.error_distribution && Object.keys(this.replayData.error_distribution).length > 0; return !!this.replayData?.error_distribution && Object.keys(this.replayData.error_distribution).length > 0;
} }
hasReplay(): boolean {
if(this.isUserScore) {
return false;
}
return this.hasErrorDistribution();
}
ngOnInit(): void { ngOnInit(): void {
this.activatedRoute.params.subscribe(params => { this.activatedRoute.params.subscribe(params => {
this.replayId = params['replayId']; this.replayId = params['replayId'];
this.userReplayId = params['userReplayId'];
if (this.replayId) { if (this.replayId) {
this.loadScoreData(); this.loadScoreData(`${environment.apiUrl}/score/${this.replayId}`);
}
if(this.userReplayId) {
this.loadScoreData(`${environment.apiUrl}/user-score/${this.userReplayId}`);
this.isUserScore = true;
} }
}); });
} }
@ -87,9 +101,9 @@ export class ViewScoreComponent implements OnInit {
return url; return url;
} }
private loadScoreData(): void { private loadScoreData(url: string): void {
this.isLoading = true; this.isLoading = true;
this.http.get<ReplayData>(`${environment.apiUrl}/score/${this.replayId}`).pipe( this.http.get<ReplayData>(url).pipe(
catchError(error => { catchError(error => {
this.isLoading = false; this.isLoading = false;
return throwError(() => new Error('An error occurred with the request: ' + error.message)); return throwError(() => new Error('An error occurred with the request: ' + error.message));

View File

@ -1,21 +0,0 @@
/* Flex container */
.flex-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 20px; /* Adjust the gap between items as needed */
}
/* Flex items - default to full width to stack on smaller screens */
.flex-container > div {
flex: 0 0 100%;
box-sizing: border-box; /* To include padding and border in the element's total width and height */
}
/* Responsive columns */
@media (min-width: 768px) { /* Adjust the breakpoint as needed */
.flex-container > div {
flex: 0 0 15%;
max-width: 20%;
}
}

View File

@ -1,203 +0,0 @@
<ng-container *ngIf="this.isLoading">
<div class="main term">
<div class="text-center">
Loading, please wait...
</div>
</div>
</ng-container>
<ng-container *ngIf="this.error">
<div class="main term">
<div class="text-center">
An error occured. Maybe try again in a bit?
</div>
</div>
</ng-container>
<ng-container *ngIf="this.replayData && !this.isLoading && !this.error">
<div class="main term mb-2 text-center">
<p>
This replay is user-submitted. As such, it might have been edited.
</p>
<p>
Please take the displayed data with a grain of salt.
</p>
</div>
<div class="main term mb-2">
<div class="fade-stuff">
<div class="image-container">
<a href="https://osu.ppy.sh/beatmaps/{{ this.replayData.beatmap_id }}?mode=osu" target="_blank">
<img ngSrc="https://assets.ppy.sh/beatmaps/{{ this.replayData.beatmap_beatmapset_id }}/covers/cover.jpg" width="260" height="72"
alt="Beatmap Cover">
<div class="overlay">
<h4>
{{ this.replayData.beatmap_title }} <span class="text-muted">by</span> {{ this.replayData.beatmap_artist }}
</h4>
★{{ this.replayData.beatmap_star_rating | number: '1.0-2' }} {{ this.replayData.beatmap_version }}
</div>
</a>
</div>
<hr class="mt-2 mb-2">
<div class="badge-list">
<span class="badge" *ngFor="let mod of this.replayData.mods">
{{ mod }}
</span>
</div>
<h1>
{{ this.replayData.score | number }}
</h1>
<div style="display: flex; justify-content: space-between;">
<div style="flex: 1; padding-right: 10px;">
<ul style="line-height: 2.2">
<li>Played by: {{ this.replayData.username }}</li>
<li *ngIf="this.replayData.osu_replay_id">Link to score: <a class="btn" href="https://osu.ppy.sh/scores/osu/{{ this.replayData.osu_replay_id }}" target="_blank">osu!web</a></li>
</ul>
</div>
<div style="flex: 1; padding-left: 10px;">
<ul>
<li>
Max combo: {{ this.replayData.max_combo }}x
<span *ngIf="this.replayData.perfect" class="badge badge-green">perfect</span>
</li>
<li>Accuracy: {{ calculateAccuracy(this.replayData) | number: '1.2-2' }}%</li>
<li>300x: {{ this.replayData.count_300 }}</li>
<li>100x: {{ this.replayData.count_100 }}</li>
<li>50x: {{ this.replayData.count_50 }}</li>
<li>Misses: {{ this.replayData.count_miss }}</li>
</ul>
</div>
</div>
<div class="text-center mb-4 flex-container">
<div *ngIf="this.replayData.ur">
<h2 title="Converted Unstable Rate">cvUR</h2>
<div>{{ this.replayData.ur | number: '1.2-2' }}</div>
</div>
<div *ngIf="this.replayData.adjusted_ur">
<h2 title="Adjusted cvUR - filters outlier hits">Adj. cvUR</h2>
<div>{{ this.replayData.adjusted_ur | number: '1.2-2' }}</div>
</div>
<div *ngIf="this.replayData.frametime">
<h2 title="Median time between frames">Frametime</h2>
<div>{{ this.replayData.frametime | number: '1.0-2' }}ms</div>
</div>
<div *ngIf="this.replayData.hits">
<h2 title="Hits within <1px of the edge">Edge Hits</h2>
<div>{{ this.replayData.hits }}</div>
</div>
<div *ngIf="this.replayData.snaps">
<h2 title="Unusual snaps in the cursor movement">Snaps</h2>
<div>{{ this.replayData.snaps }}</div>
</div>
</div>
</div>
</div>
<div class="main term mb-2" *ngIf="this.replayData.similar_scores && this.replayData.similar_scores.length > 0">
<h1># similar replays</h1>
<table>
<thead>
<th class="text-center">Played by</th>
<th class="text-center">PP</th>
<th class="text-center">Date</th>
<th class="text-center">Similarity</th>
<th class="text-center">Correlation</th>
<th class="text-center"></th>
</thead>
<tbody>
<tr *ngFor="let score of this.replayData.similar_scores">
<td class="text-center">
<a [routerLink]="['/u/' + score.username]">{{ score.username }}</a>
</td>
<td class="text-center">
{{ score.pp | number: '1.2-2' }}
</td>
<td class="text-center">
{{ score.date }}
</td>
<td class="text-center">
{{ score.similarity | number: '1.2-3' }}
</td>
<td class="text-center">
{{ score.correlation | number: '1.2-4' }}
</td>
<td class="text-center">
<a class="btn" [routerLink]="['/s/' + score.replay_id]">details</a>
</td>
</tbody>
</table>
</div>
<div class="main term mb-2" *ngIf="this.replayData.mean_error">
<h1># nerd stats</h1>
<table>
<thead>
<th></th>
<th>
this replay
</th>
<th *ngIf="this.replayData.comparable_samples">
<span title="average values for this beatmap">
avg. (n={{ this.replayData.comparable_samples }})
</span>
</th>
</thead>
<tr>
<td>Mean error</td>
<td class="text-center">{{ this.replayData.mean_error | number: '1.2-2' }}</td>
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_mean_error | number: '1.2-2' }}</td>
</tr>
<tr>
<td>Error variance</td>
<td class="text-center">{{ this.replayData.error_variance | number: '1.2-2'}}</td>
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_error_variance | number: '1.2-2' }}</td>
</tr>
<tr>
<td>Error Std. deviation</td>
<td class="text-center">{{ this.replayData.error_standard_deviation | number: '1.2-2'}}</td>
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_error_standard_deviation | number: '1.2-2' }}</td>
</tr>
<tr>
<td>Min/max error</td>
<td class="text-center">[{{ this.replayData.minimum_error | number: '1.0-0' }}, {{ this.replayData.maximum_error | number: '1.0-0' }}]</td>
<td *ngIf="this.replayData.comparable_samples" class="text-center">[{{ this.replayData.comparable_minimum_error | number: '1.0-0' }}, {{ this.replayData.comparable_maximum_error | number: '1.0-0' }}]</td>
</tr>
<tr>
<td>Coefficient of variation</td>
<td class="text-center">{{ this.replayData.error_coefficient_of_variation | number: '1.2-2'}}</td>
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_error_coefficient_of_variation | number: '1.2-2' }}</td>
</tr>
<tr>
<td>Kurtosis</td>
<td class="text-center">{{ this.replayData.error_kurtosis | number: '1.2-2'}}</td>
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_error_kurtosis | number: '1.2-2' }}</td>
</tr>
<tr>
<td>Error skewness</td>
<td class="text-center">{{ this.replayData.error_skewness | number: '1.2-2'}}</td>
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_error_skewness | number: '1.2-2' }}</td>
</tr>
</table>
</div>
<ng-container *ngFor="let chart of this.replayData.charts">
<app-chart [title]="chart.title" [data]="chart.data"></app-chart>
</ng-container>
<div class="main term mb-2" *ngIf="this.replayData.error_distribution && Object.keys(this.replayData.error_distribution).length > 0">
<h1># hit distribution</h1>
<canvas baseChart
[data]="barChartData"
[options]="barChartOptions"
[plugins]="barChartPlugins"
[legend]="barChartLegend"
[type]="'bar'"
class="chart">
</canvas>
</div>
</ng-container>

View File

@ -1,172 +0,0 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {ActivatedRoute, RouterLink} from "@angular/router";
import {Title} from "@angular/platform-browser";
import {DistributionEntry, UserReplayData} from "../replays";
import {environment} from "../../environments/environment";
import {catchError, throwError} from "rxjs";
import {ChartComponent} from "../../corelib/components/chart/chart.component";
import {BaseChartDirective, NgChartsModule} from "ng2-charts";
import {DecimalPipe, NgForOf, NgIf, NgOptimizedImage} from "@angular/common";
import {calculateAccuracy} from "../format";
import {ChartConfiguration} from "chart.js";
@Component({
selector: 'app-view-user-score',
standalone: true,
imports: [
ChartComponent,
NgChartsModule,
NgIf,
NgForOf,
RouterLink,
DecimalPipe,
NgOptimizedImage
],
templateUrl: './view-user-score.component.html',
styleUrl: './view-user-score.component.css'
})
export class ViewUserScoreComponent implements OnInit {
@ViewChild(BaseChartDirective)
public chart!: BaseChartDirective;
isLoading = false;
error: string | null = null;
replayData: UserReplayData | null = null;
replayId: number | null = null;
public barChartLegend = true;
public barChartPlugins = [];
public barChartData: ChartConfiguration<'bar'>['data'] = {
labels: [],
datasets: [
{ data: [], label: 'Miss (%)', backgroundColor: 'rgba(255,0,0,0.66)', borderRadius: 5 },
{ data: [], label: '50 (%)', backgroundColor: 'rgba(187,129,33,0.66)', borderRadius: 5 },
{ data: [], label: '100 (%)', backgroundColor: 'rgba(219,255,0,0.8)', borderRadius: 5 },
{ data: [], label: '300 (%)', backgroundColor: 'rgba(0,255,41,0.66)', borderRadius: 5 }
],
};
public barChartOptions: ChartConfiguration<'bar'>['options'] = {
responsive: true,
scales: {
x: {
stacked: true,
},
y: {
stacked: true
}
}
};
constructor(private http: HttpClient,
private activatedRoute: ActivatedRoute,
private title: Title
) {}
ngOnInit(): void {
this.activatedRoute.params.subscribe(params => {
this.replayId = params['replayId'];
if (this.replayId) {
this.loadScoreData();
}
});
}
private loadScoreData(): void {
this.isLoading = true;
this.http.get<UserReplayData>(`${environment.apiUrl}/user-score/${this.replayId}`).pipe(
catchError(error => {
this.isLoading = false;
return throwError(() => new Error('An error occurred with the request: ' + error.message));
})
).subscribe({
next: (response) => {
this.isLoading = false;
this.replayData = response;
this.title.setTitle(
`${this.replayData.username} on ${this.replayData.beatmap_title} (${this.replayData.beatmap_version})`
)
let errorDistribution = Object.entries(this.replayData.error_distribution);
if (errorDistribution.length >= 1) {
const sortedEntries = errorDistribution
.sort((a, b) => parseInt(a[0]) - parseInt(b[0]));
const chartData = this.generateChartData(sortedEntries);
this.barChartData.labels = chartData.labels;
for (let i = 0; i < 4; i++) {
this.barChartData.datasets[i].data = chartData.datasets[i];
}
}
},
error: (error) => {
this.error = error;
}
});
}
private getChartRange(entries: [string, DistributionEntry][]): [number, number] {
const keys = entries.map(([key, _]) => parseInt(key));
const minKey = Math.min(...keys);
const maxKey = Math.max(...keys);
return [minKey, maxKey];
}
private generateChartData(entries: [string, DistributionEntry][]): { labels: string[], datasets: number[][] } {
const range = this.getChartRange(entries);
const labels: string[] = [];
const datasets: number[][] = Array(4).fill(0).map(() => []);
const defaultPercentageValues: DistributionEntry = {
countMiss: 0,
count50: 0,
count100: 0,
count300: 0,
};
const entriesMap = new Map<number, DistributionEntry>(entries.map(([key, value]) => [parseInt(key), value]));
for (let key = range[0]; key <= range[1]; key += 2) {
const endKey = key + 2 <= range[1] ? key + 2 : key + 1;
labels.push(`${key}ms to ${endKey}ms`);
const currentEntry = entriesMap.get(key) || { ...defaultPercentageValues };
const nextEntry = key + 1 <= range[1] ? (entriesMap.get(key + 1) || { ...defaultPercentageValues }) : defaultPercentageValues;
const sumEntry: DistributionEntry = {
countMiss: currentEntry.countMiss + nextEntry.countMiss,
count50: currentEntry.count50 + nextEntry.count50,
count100: currentEntry.count100 + nextEntry.count100,
count300: currentEntry.count300 + nextEntry.count300,
};
datasets[0].push(sumEntry.countMiss);
datasets[1].push(sumEntry.count50);
datasets[2].push(sumEntry.count100);
datasets[3].push(sumEntry.count300);
}
// Handling the case for an odd last key if needed
if (range[1] % 2 !== range[0] % 2) {
const lastEntry = entriesMap.get(range[1]) || { ...defaultPercentageValues };
labels.push(`${range[1]}ms to ${range[1] + 1}ms`);
datasets[0].push(lastEntry.countMiss);
datasets[1].push(lastEntry.count50);
datasets[2].push(lastEntry.count100);
datasets[3].push(lastEntry.count300);
}
return { labels, datasets };
}
protected readonly Object = Object;
protected readonly calculateAccuracy = calculateAccuracy;
}