from enum import Enum from typing import Literal, TypedDict, cast from app.config import settings from .mods import API_MODS, APIMod from pydantic import BaseModel, Field, ValidationInfo, field_serializer, field_validator class GameMode(str, Enum): OSU = "osu" TAIKO = "taiko" FRUITS = "fruits" MANIA = "mania" OSURX = "osurx" OSUAP = "osuap" TAIKORX = "taikorx" FRUITSRX = "fruitsrx" def __int__(self) -> int: return { GameMode.OSU: 0, GameMode.TAIKO: 1, GameMode.FRUITS: 2, GameMode.MANIA: 3, GameMode.OSURX: 0, GameMode.OSUAP: 0, GameMode.TAIKORX: 1, GameMode.FRUITSRX: 2, }[self] def __str__(self) -> str: return self.value @classmethod def from_int(cls, v: int) -> "GameMode": return { 0: GameMode.OSU, 1: GameMode.TAIKO, 2: GameMode.FRUITS, 3: GameMode.MANIA, }[v] @classmethod def from_int_extra(cls, v: int) -> "GameMode": return { 0: GameMode.OSU, 1: GameMode.TAIKO, 2: GameMode.FRUITS, 3: GameMode.MANIA, 4: GameMode.OSURX, 5: GameMode.OSUAP, 6: GameMode.TAIKORX, 7: GameMode.FRUITSRX, }[v] def readable(self) -> str: return { GameMode.OSU: "osu!", GameMode.TAIKO: "osu!taiko", GameMode.FRUITS: "osu!catch", GameMode.MANIA: "osu!mania", GameMode.OSURX: "osu!relax", GameMode.OSUAP: "osu!autopilot", GameMode.TAIKORX: "taiko relax", GameMode.FRUITSRX: "catch relax", }[self] def to_base_ruleset(self) -> "GameMode": gamemode = { GameMode.OSURX: GameMode.OSU, GameMode.OSUAP: GameMode.OSU, GameMode.TAIKORX: GameMode.TAIKO, GameMode.FRUITSRX: GameMode.FRUITS, }.get(self) return gamemode if gamemode else self def to_special_mode(self, mods: list[APIMod] | list[str]) -> "GameMode": if self not in (GameMode.OSU, GameMode.TAIKO, GameMode.FRUITS): return self if not settings.enable_rx and not settings.enable_ap: return self if len(mods) > 0 and isinstance(mods[0], dict): mods = [mod["acronym"] for mod in cast(list[APIMod], mods)] if "AP" in mods and settings.enable_ap: return GameMode.OSUAP if "RX" in mods and settings.enable_rx: return { GameMode.OSU: GameMode.OSURX, GameMode.TAIKO: GameMode.TAIKORX, GameMode.FRUITS: GameMode.FRUITSRX, }[self] return self @classmethod def parse(cls, v: str | int) -> "GameMode | None": if isinstance(v, int) or v.isdigit(): return cls.from_int_extra(int(v)) v = v.upper() try: return cls[v] except ValueError: return None class Rank(str, Enum): X = "X" XH = "XH" S = "S" SH = "SH" A = "A" B = "B" C = "C" D = "D" F = "F" @property def in_statisctics(self): return self in { Rank.X, Rank.XH, Rank.S, Rank.SH, Rank.A, } # https://github.com/ppy/osu/blob/master/osu.Game/Rulesets/Scoring/HitResult.cs class HitResult(str, Enum): NONE = "none" # [Order(15)] MISS = "miss" # [Order(5)] MEH = "meh" # [Order(4)] OK = "ok" # [Order(3)] GOOD = "good" # [Order(2)] GREAT = "great" # [Order(1)] PERFECT = "perfect" # [Order(0)] SMALL_TICK_MISS = "small_tick_miss" # [Order(12)] SMALL_TICK_HIT = "small_tick_hit" # [Order(7)] LARGE_TICK_MISS = "large_tick_miss" # [Order(11)] LARGE_TICK_HIT = "large_tick_hit" # [Order(6)] SMALL_BONUS = "small_bonus" # [Order(10)] LARGE_BONUS = "large_bonus" # [Order(9)] IGNORE_MISS = "ignore_miss" # [Order(14)] IGNORE_HIT = "ignore_hit" # [Order(13)] COMBO_BREAK = "combo_break" # [Order(16)] SLIDER_TAIL_HIT = "slider_tail_hit" # [Order(8)] LEGACY_COMBO_INCREASE = "legacy_combo_increase" # [Order(99)] @deprecated def is_hit(self) -> bool: return self not in ( HitResult.NONE, HitResult.IGNORE_MISS, HitResult.COMBO_BREAK, HitResult.LARGE_TICK_MISS, HitResult.SMALL_TICK_MISS, HitResult.MISS, ) def is_scorable(self) -> bool: return self not in ( HitResult.NONE, HitResult.IGNORE_HIT, HitResult.IGNORE_MISS, ) class LeaderboardType(Enum): GLOBAL = "global" FRIENDS = "friend" COUNTRY = "country" TEAM = "team" ScoreStatistics = dict[HitResult, int] class SoloScoreSubmissionInfo(BaseModel): rank: Rank total_score: int = Field(ge=0, le=2**31 - 1) total_score_without_mods: int = Field(ge=0, le=2**31 - 1) accuracy: float = Field(ge=0, le=1) pp: float = Field(default=0, ge=0, le=2**31 - 1) max_combo: int = 0 ruleset_id: Literal[0, 1, 2, 3] passed: bool = False mods: list[APIMod] = Field(default_factory=list) statistics: ScoreStatistics = Field(default_factory=dict) maximum_statistics: ScoreStatistics = Field(default_factory=dict) @field_validator("mods", mode="after") @classmethod def validate_mods(cls, mods: list[APIMod], info: ValidationInfo): incompatible_mods = set() # check incompatible mods for mod in mods: if mod["acronym"] in incompatible_mods: raise ValueError(f"Mod {mod['acronym']} is incompatible with other mods") setting_mods = API_MODS[info.data["ruleset_id"]].get(mod["acronym"]) if not setting_mods: raise ValueError(f"Invalid mod: {mod['acronym']}") incompatible_mods.update(setting_mods["IncompatibleMods"]) return mods @field_serializer("statistics", "maximum_statistics", when_used="json") def serialize_statistics(self, v): """序列化统计字段,确保枚举值正确转换为字符串""" if isinstance(v, dict): serialized = {} for key, value in v.items(): if hasattr(key, "value"): # 如果是枚举,使用其值 serialized[key.value] = value else: # 否则直接使用键 serialized[str(key)] = value return serialized return v @field_serializer("rank", when_used="json") def serialize_rank(self, v): """序列化等级,确保枚举值正确转换为字符串""" if hasattr(v, "value"): return v.value return str(v) class LegacyReplaySoloScoreInfo(TypedDict): online_id: int mods: list[APIMod] statistics: ScoreStatistics maximum_statistics: ScoreStatistics client_version: str rank: Rank user_id: int total_score_without_mods: int