refactor(mods): move models from app.models.score to app.models.mods
This commit is contained in:
@@ -1,47 +1,91 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TypedDict
|
import json
|
||||||
|
from typing import Literal, NotRequired, TypedDict
|
||||||
|
|
||||||
|
from app.path import STATIC_DIR
|
||||||
|
|
||||||
|
|
||||||
class APIMod(TypedDict):
|
class APIMod(TypedDict):
|
||||||
acronym: str
|
acronym: str
|
||||||
settings: dict[str, bool | float | str]
|
settings: NotRequired[dict[str, bool | float | str]]
|
||||||
|
|
||||||
|
|
||||||
# https://github.com/ppy/osu-api/wiki#mods
|
# https://github.com/ppy/osu-api/wiki#mods
|
||||||
LEGACY_MOD_TO_API_MOD = {
|
API_MOD_TO_LEGACY: dict[str, int] = {
|
||||||
(1 << 0): APIMod(acronym="NF", settings={}), # No Fail
|
"NF": 1 << 0, # No Fail
|
||||||
(1 << 1): APIMod(acronym="EZ", settings={}),
|
"EZ": 1 << 1, # Easy
|
||||||
(1 << 2): APIMod(acronym="TD", settings={}), # Touch Device
|
"TD": 1 << 2, # Touch Device
|
||||||
(1 << 3): APIMod(acronym="HD", settings={}), # Hidden
|
"HD": 1 << 3, # Hidden
|
||||||
(1 << 4): APIMod(acronym="HR", settings={}), # Hard Rock
|
"HR": 1 << 4, # Hard Rock
|
||||||
(1 << 5): APIMod(acronym="SD", settings={}), # Sudden Death
|
"SD": 1 << 5, # Sudden Death
|
||||||
(1 << 6): APIMod(acronym="DT", settings={}), # Double Time
|
"DT": 1 << 6, # Double Time
|
||||||
(1 << 7): APIMod(acronym="RX", settings={}), # Relax
|
"RX": 1 << 7, # Relax
|
||||||
(1 << 8): APIMod(acronym="HT", settings={}), # Half Time
|
"HT": 1 << 8, # Half Time
|
||||||
(1 << 9): APIMod(acronym="NC", settings={}), # Nightcore
|
"NC": 1 << 9, # Nightcore
|
||||||
(1 << 10): APIMod(acronym="FL", settings={}), # Flashlight
|
"FL": 1 << 10, # Flashlight
|
||||||
(1 << 11): APIMod(acronym="AT", settings={}), # Auto Play
|
"AT": 1 << 11, # Autoplay
|
||||||
(1 << 12): APIMod(acronym="SO", settings={}), # Spun Out
|
"SO": 1 << 12, # Spun Out
|
||||||
(1 << 13): APIMod(acronym="AP", settings={}), # Autopilot
|
"AP": 1 << 13, # Auto Pilot
|
||||||
(1 << 14): APIMod(acronym="PF", settings={}), # Perfect
|
"PF": 1 << 14, # Perfect
|
||||||
(1 << 15): APIMod(acronym="4K", settings={}), # 4K
|
"4K": 1 << 15, # 4K
|
||||||
(1 << 16): APIMod(acronym="5K", settings={}), # 5K
|
"5K": 1 << 16, # 5K
|
||||||
(1 << 17): APIMod(acronym="6K", settings={}), # 6K
|
"6K": 1 << 17, # 6K
|
||||||
(1 << 18): APIMod(acronym="7K", settings={}), # 7K
|
"7K": 1 << 18, # 7K
|
||||||
(1 << 19): APIMod(acronym="8K", settings={}), # 8K
|
"8K": 1 << 19, # 8K
|
||||||
(1 << 20): APIMod(acronym="FI", settings={}), # Fade In
|
"FI": 1 << 20, # Fade In
|
||||||
(1 << 21): APIMod(acronym="RD", settings={}), # Random
|
"RD": 1 << 21, # Random
|
||||||
(1 << 22): APIMod(acronym="CN", settings={}), # Cinema
|
"CN": 1 << 22, # Cinema
|
||||||
(1 << 23): APIMod(acronym="TP", settings={}), # Target Practice
|
"TP": 1 << 23, # Target Practice
|
||||||
(1 << 24): APIMod(acronym="9K", settings={}), # 9K
|
"9K": 1 << 24, # 9K
|
||||||
(1 << 25): APIMod(acronym="CO", settings={}), # Key Co-op
|
"CO": 1 << 25, # Key Co-op
|
||||||
(1 << 26): APIMod(acronym="1K", settings={}), # 1K
|
"1K": 1 << 26, # 1K
|
||||||
(1 << 27): APIMod(acronym="2K", settings={}), # 2K
|
"3K": 1 << 27, # 3K
|
||||||
(1 << 28): APIMod(acronym="3K", settings={}), # 3K
|
"2K": 1 << 28, # 2K
|
||||||
(1 << 29): APIMod(acronym="SV2", settings={}), # Score V2
|
"SV2": 1 << 29, # ScoreV2
|
||||||
(1 << 30): APIMod(acronym="MR", settings={}), # Mirror
|
"MR": 1 << 30, # Mirror
|
||||||
}
|
}
|
||||||
|
LEGACY_MOD_TO_API_MOD = {}
|
||||||
|
for k, v in API_MOD_TO_LEGACY.items():
|
||||||
|
LEGACY_MOD_TO_API_MOD[v] = APIMod(acronym=k, settings={})
|
||||||
|
API_MOD_TO_LEGACY["NC"] |= API_MOD_TO_LEGACY["DT"]
|
||||||
|
API_MOD_TO_LEGACY["PF"] |= API_MOD_TO_LEGACY["SD"]
|
||||||
|
|
||||||
|
|
||||||
|
# see static/mods.json
|
||||||
|
class Settings(TypedDict):
|
||||||
|
Name: str
|
||||||
|
Type: str
|
||||||
|
Label: str
|
||||||
|
Description: str
|
||||||
|
|
||||||
|
|
||||||
|
class Mod(TypedDict):
|
||||||
|
Acronym: str
|
||||||
|
Name: str
|
||||||
|
Description: str
|
||||||
|
Type: str
|
||||||
|
Settings: list[Settings]
|
||||||
|
IncompatibleMods: list[str]
|
||||||
|
RequiresConfiguration: bool
|
||||||
|
UserPlayable: bool
|
||||||
|
ValidForMultiplayer: bool
|
||||||
|
ValidForFreestyleAsRequiredMod: bool
|
||||||
|
ValidForMultiplayerAsFreeMod: bool
|
||||||
|
AlwaysValidForSubmission: bool
|
||||||
|
|
||||||
|
|
||||||
|
API_MODS: dict[Literal[0, 1, 2, 3], dict[str, Mod]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def init_mods():
|
||||||
|
mods_file = STATIC_DIR / "mods.json"
|
||||||
|
raw_mods = json.loads(mods_file.read_text())
|
||||||
|
for ruleset in raw_mods:
|
||||||
|
ruleset_mods = {}
|
||||||
|
for mod in ruleset["Mods"]:
|
||||||
|
ruleset_mods[mod["Acronym"]] = mod
|
||||||
|
API_MODS[ruleset["RulesetID"]] = ruleset_mods
|
||||||
|
|
||||||
|
|
||||||
def int_to_mods(mods: int) -> list[APIMod]:
|
def int_to_mods(mods: int) -> list[APIMod]:
|
||||||
@@ -54,3 +98,10 @@ def int_to_mods(mods: int) -> list[APIMod]:
|
|||||||
if mods & (1 << 9):
|
if mods & (1 << 9):
|
||||||
mod_list.remove(LEGACY_MOD_TO_API_MOD[(1 << 6)])
|
mod_list.remove(LEGACY_MOD_TO_API_MOD[(1 << 6)])
|
||||||
return mod_list
|
return mod_list
|
||||||
|
|
||||||
|
|
||||||
|
def mods_to_int(mods: list[APIMod]) -> int:
|
||||||
|
sum_ = 0
|
||||||
|
for mod in mods:
|
||||||
|
sum_ |= API_MOD_TO_LEGACY.get(mod["acronym"], 0)
|
||||||
|
return sum_
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
import json
|
from typing import Literal, TypedDict
|
||||||
from typing import Any, Literal, TypedDict
|
|
||||||
|
|
||||||
from app.path import STATIC_DIR
|
from .mods import API_MOD_TO_LEGACY, API_MODS, APIMod, init_mods
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, ValidationInfo, field_validator
|
from pydantic import BaseModel, Field, ValidationInfo, field_validator
|
||||||
import rosu_pp_py as rosu
|
import rosu_pp_py as rosu
|
||||||
@@ -46,55 +45,6 @@ class Rank(str, Enum):
|
|||||||
F = "F"
|
F = "F"
|
||||||
|
|
||||||
|
|
||||||
class APIMod(TypedDict, total=False):
|
|
||||||
acronym: str
|
|
||||||
settings: dict[str, Any]
|
|
||||||
|
|
||||||
|
|
||||||
legacy_mod: dict[str, int] = {
|
|
||||||
"NF": 1 << 0, # No Fail
|
|
||||||
"EZ": 1 << 1, # Easy
|
|
||||||
"TD": 1 << 2, # Touch Device
|
|
||||||
"HD": 1 << 3, # Hidden
|
|
||||||
"HR": 1 << 4, # Hard Rock
|
|
||||||
"SD": 1 << 5, # Sudden Death
|
|
||||||
"DT": 1 << 6, # Double Time
|
|
||||||
"RX": 1 << 7, # Relax
|
|
||||||
"HT": 1 << 8, # Half Time
|
|
||||||
"NC": 1 << 9, # Nightcore
|
|
||||||
"FL": 1 << 10, # Flashlight
|
|
||||||
"AT": 1 << 11, # Autoplay
|
|
||||||
"SO": 1 << 12, # Spun Out
|
|
||||||
"AP": 1 << 13, # Auto Pilot
|
|
||||||
"PF": 1 << 14, # Perfect
|
|
||||||
"4K": 1 << 15, # 4K
|
|
||||||
"5K": 1 << 16, # 5K
|
|
||||||
"6K": 1 << 17, # 6K
|
|
||||||
"7K": 1 << 18, # 7K
|
|
||||||
"8K": 1 << 19, # 8K
|
|
||||||
"FI": 1 << 20, # Fade In
|
|
||||||
"RD": 1 << 21, # Random
|
|
||||||
"CN": 1 << 22, # Cinema
|
|
||||||
"TP": 1 << 23, # Target Practice
|
|
||||||
"9K": 1 << 24, # 9K
|
|
||||||
"CO": 1 << 25, # Key Co-op
|
|
||||||
"1K": 1 << 26, # 1K
|
|
||||||
"3K": 1 << 27, # 3K
|
|
||||||
"2K": 1 << 28, # 2K
|
|
||||||
"SV2": 1 << 29, # ScoreV2
|
|
||||||
"MR": 1 << 30, # Mirror
|
|
||||||
}
|
|
||||||
legacy_mod["NC"] |= legacy_mod["DT"]
|
|
||||||
legacy_mod["PF"] |= legacy_mod["SD"]
|
|
||||||
|
|
||||||
|
|
||||||
def api_mod_to_int(mods: list[APIMod]) -> int:
|
|
||||||
sum_ = 0
|
|
||||||
for mod in mods:
|
|
||||||
sum_ |= legacy_mod.get(mod["acronym"], 0)
|
|
||||||
return sum_
|
|
||||||
|
|
||||||
|
|
||||||
# https://github.com/ppy/osu/blob/master/osu.Game/Rulesets/Scoring/HitResult.cs
|
# https://github.com/ppy/osu/blob/master/osu.Game/Rulesets/Scoring/HitResult.cs
|
||||||
class HitResult(str, Enum):
|
class HitResult(str, Enum):
|
||||||
PERFECT = "perfect" # [Order(0)]
|
PERFECT = "perfect" # [Order(0)]
|
||||||
@@ -140,44 +90,9 @@ class LeaderboardType(Enum):
|
|||||||
TEAM = "team"
|
TEAM = "team"
|
||||||
|
|
||||||
|
|
||||||
# see static/mods.json
|
|
||||||
class Settings(TypedDict):
|
|
||||||
Name: str
|
|
||||||
Type: str
|
|
||||||
Label: str
|
|
||||||
Description: str
|
|
||||||
|
|
||||||
|
|
||||||
class Mod(TypedDict):
|
|
||||||
Acronym: str
|
|
||||||
Name: str
|
|
||||||
Description: str
|
|
||||||
Type: str
|
|
||||||
Settings: list[Settings]
|
|
||||||
IncompatibleMods: list[str]
|
|
||||||
RequiresConfiguration: bool
|
|
||||||
UserPlayable: bool
|
|
||||||
ValidForMultiplayer: bool
|
|
||||||
ValidForFreestyleAsRequiredMod: bool
|
|
||||||
ValidForMultiplayerAsFreeMod: bool
|
|
||||||
AlwaysValidForSubmission: bool
|
|
||||||
|
|
||||||
|
|
||||||
MODS: dict[int, dict[str, Mod]] = {}
|
|
||||||
|
|
||||||
ScoreStatistics = dict[HitResult, int]
|
ScoreStatistics = dict[HitResult, int]
|
||||||
|
|
||||||
|
|
||||||
def _init_mods():
|
|
||||||
mods_file = STATIC_DIR / "mods.json"
|
|
||||||
raw_mods = json.loads(mods_file.read_text())
|
|
||||||
for ruleset in raw_mods:
|
|
||||||
ruleset_mods = {}
|
|
||||||
for mod in ruleset["Mods"]:
|
|
||||||
ruleset_mods[mod["Acronym"]] = mod
|
|
||||||
MODS[ruleset["RulesetID"]] = ruleset_mods
|
|
||||||
|
|
||||||
|
|
||||||
class SoloScoreSubmissionInfo(BaseModel):
|
class SoloScoreSubmissionInfo(BaseModel):
|
||||||
rank: Rank
|
rank: Rank
|
||||||
total_score: int = Field(ge=0, le=2**31 - 1)
|
total_score: int = Field(ge=0, le=2**31 - 1)
|
||||||
@@ -194,8 +109,8 @@ class SoloScoreSubmissionInfo(BaseModel):
|
|||||||
@field_validator("mods", mode="after")
|
@field_validator("mods", mode="after")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_mods(cls, mods: list[APIMod], info: ValidationInfo):
|
def validate_mods(cls, mods: list[APIMod], info: ValidationInfo):
|
||||||
if not MODS:
|
if not API_MOD_TO_LEGACY:
|
||||||
_init_mods()
|
init_mods()
|
||||||
incompatible_mods = set()
|
incompatible_mods = set()
|
||||||
# check incompatible mods
|
# check incompatible mods
|
||||||
for mod in mods:
|
for mod in mods:
|
||||||
@@ -203,7 +118,7 @@ class SoloScoreSubmissionInfo(BaseModel):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Mod {mod['acronym']} is incompatible with other mods"
|
f"Mod {mod['acronym']} is incompatible with other mods"
|
||||||
)
|
)
|
||||||
setting_mods = MODS[info.data["ruleset_id"]].get(mod["acronym"])
|
setting_mods = API_MODS[info.data["ruleset_id"]].get(mod["acronym"])
|
||||||
if not setting_mods:
|
if not setting_mods:
|
||||||
raise ValueError(f"Invalid mod: {mod['acronym']}")
|
raise ValueError(f"Invalid mod: {mod['acronym']}")
|
||||||
incompatible_mods.update(setting_mods["IncompatibleMods"])
|
incompatible_mods.update(setting_mods["IncompatibleMods"])
|
||||||
|
|||||||
Reference in New Issue
Block a user