from __future__ import annotations import math from collections.abc import Iterable from dataclasses import dataclass from typing import TypedDict from akatsuki_pp_py import Beatmap from akatsuki_pp_py import Calculator from app.constants.mods import Mods @dataclass class ScoreParams: mode: int mods: int | None = None combo: int | None = None # caller may pass either acc OR 300/100/50/geki/katu/miss # passing both will result in a value error being raised acc: float | None = None n300: int | None = None n100: int | None = None n50: int | None = None ngeki: int | None = None nkatu: int | None = None nmiss: int | None = None class PerformanceRating(TypedDict): pp: float pp_acc: float | None pp_aim: float | None pp_speed: float | None pp_flashlight: float | None effective_miss_count: float | None pp_difficulty: float | None class DifficultyRating(TypedDict): stars: float aim: float | None speed: float | None flashlight: float | None slider_factor: float | None speed_note_count: float | None stamina: float | None color: float | None rhythm: float | None peak: float | None class PerformanceResult(TypedDict): performance: PerformanceRating difficulty: DifficultyRating def calculate_performances( osu_file_path: str, scores: Iterable[ScoreParams], ) -> list[PerformanceResult]: """\ Calculate performance for multiple scores on a single beatmap. Typically most useful for mass-recalculation situations. TODO: Some level of error handling & returning to caller should be implemented here to handle cases where e.g. the beatmap file is invalid or there an issue during calculation. """ calc_bmap = Beatmap(path=osu_file_path) results: list[PerformanceResult] = [] for score in scores: if score.acc and ( score.n300 or score.n100 or score.n50 or score.ngeki or score.nkatu ): raise ValueError( "Must not specify accuracy AND 300/100/50/geki/katu. Only one or the other.", ) # rosupp ignores NC and requires DT if score.mods is not None: if score.mods & Mods.NIGHTCORE: score.mods |= Mods.DOUBLETIME calculator = Calculator( mode=score.mode, mods=score.mods or 0, combo=score.combo, acc=score.acc, n300=score.n300, n100=score.n100, n50=score.n50, n_geki=score.ngeki, n_katu=score.nkatu, n_misses=score.nmiss, ) result = calculator.performance(calc_bmap) pp = result.pp if math.isnan(pp) or math.isinf(pp): # TODO: report to logserver pp = 0.0 else: pp = round(pp, 3) results.append( { "performance": { "pp": pp, "pp_acc": result.pp_acc, "pp_aim": result.pp_aim, "pp_speed": result.pp_speed, "pp_flashlight": result.pp_flashlight, "effective_miss_count": result.effective_miss_count, "pp_difficulty": result.pp_difficulty, }, "difficulty": { "stars": result.difficulty.stars, "aim": result.difficulty.aim, "speed": result.difficulty.speed, "flashlight": result.difficulty.flashlight, "slider_factor": result.difficulty.slider_factor, "speed_note_count": result.difficulty.speed_note_count, "stamina": result.difficulty.stamina, "color": result.difficulty.color, "rhythm": result.difficulty.rhythm, "peak": result.difficulty.peak, }, }, ) return results