Files
g0v0-server/app/models/mods.py
2025-08-01 11:08:59 +00:00

182 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import json
from typing import Literal, NotRequired, TypedDict
from app.path import STATIC_DIR
from msgpack_lazer_api import APIMod as MsgpackAPIMod
class APIMod(TypedDict):
acronym: str
settings: NotRequired[dict[str, bool | float | str | int]]
# https://github.com/ppy/osu-api/wiki#mods
API_MOD_TO_LEGACY: 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_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]:
mod_list = []
for mod in range(31):
if mods & (1 << mod):
mod_list.append(LEGACY_MOD_TO_API_MOD[(1 << mod)])
if mods & (1 << 14):
mod_list.remove(LEGACY_MOD_TO_API_MOD[(1 << 5)])
if mods & (1 << 9):
mod_list.remove(LEGACY_MOD_TO_API_MOD[(1 << 6)])
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_
NO_CHECK = "DO_NO_CHECK"
# FIXME: 这里为空表示了两种情况mod 没有配置项;任何时候都可以获得 pp
# 如果是后者,则 mod 更新的时候可能会误判。
COMMON_CONFIG: dict[str, dict] = {
"EZ": {"retries": 2},
"NF": {},
"HT": {"speed_change": 0.75, "adjust_pitch": NO_CHECK},
"DC": {"speed_change": 0.75},
"HR": {},
"SD": {},
"PF": {},
"HD": {},
"DT": {"speed_change": 1.5, "adjust_pitch": NO_CHECK},
"NC": {"speed_change": 1.5},
"FL": {"size_multiplier": 1.0, "combo_based_size": True},
"AC": {},
"MU": {},
"TD": {},
}
RANKED_MODS: dict[int, dict[str, dict]] = {
0: COMMON_CONFIG,
1: COMMON_CONFIG,
2: COMMON_CONFIG,
3: COMMON_CONFIG,
}
# osu
RANKED_MODS[0]["HD"]["only_fade_approach_circles"] = False
RANKED_MODS[0]["FL"]["follow_delay"] = 1.0
RANKED_MODS[0]["BL"] = {}
RANKED_MODS[0]["NS"] = {}
RANKED_MODS[0]["SO"] = {}
RANKED_MODS[0]["TC"] = {}
# taiko
del RANKED_MODS[1]["EZ"]["retries"]
# catch
RANKED_MODS[2]["NS"] = {}
# mania
del RANKED_MODS[3]["HR"]
RANKED_MODS[3]["FL"]["combo_based_size"] = False
RANKED_MODS[3]["MR"] = {}
for i in range(4, 10):
RANKED_MODS[3][f"{i}K"] = {}
def mods_can_get_pp(ruleset_id: int, mods: list[APIMod]) -> bool:
ranked_mods = RANKED_MODS[ruleset_id]
for mod in mods:
mod["settings"] = mod.get("settings", {})
if (settings := ranked_mods.get(mod["acronym"])) is None:
return False
if settings == {}:
continue
for setting, value in mod["settings"].items():
if (expected_value := settings.get(setting)) is None:
return False
if expected_value != NO_CHECK and value != expected_value:
return False
return True
def msgpack_to_apimod(mod: MsgpackAPIMod) -> APIMod:
"""
Convert a MsgpackAPIMod to an APIMod.
"""
return APIMod(
acronym=mod.acronym,
settings=mod.settings,
)