feat(mods): configure ranked mods by file (#49)
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -227,3 +227,6 @@ osu-web-master/*
|
|||||||
osu-web-master/.env.dusk.local.example
|
osu-web-master/.env.dusk.local.example
|
||||||
osu-web-master/.env.example
|
osu-web-master/.env.example
|
||||||
osu-web-master/.env.testing.example
|
osu-web-master/.env.testing.example
|
||||||
|
config/*
|
||||||
|
!config/
|
||||||
|
!config/.gitkeep
|
||||||
|
|||||||
@@ -433,11 +433,6 @@ STORAGE_SETTINGS='{
|
|||||||
),
|
),
|
||||||
"游戏设置",
|
"游戏设置",
|
||||||
]
|
]
|
||||||
enable_all_mods_pp: Annotated[
|
|
||||||
bool,
|
|
||||||
Field(default=False, description="启用所有 Mod 的 PP 计算"),
|
|
||||||
"游戏设置",
|
|
||||||
]
|
|
||||||
enable_supporter_for_all_users: Annotated[
|
enable_supporter_for_all_users: Annotated[
|
||||||
bool,
|
bool,
|
||||||
Field(default=False, description="启用所有新注册用户的支持者状态"),
|
Field(default=False, description="启用所有新注册用户的支持者状态"),
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from copy import deepcopy
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from typing import Literal, NotRequired, TypedDict
|
from typing import Any, Literal, NotRequired, TypedDict
|
||||||
|
|
||||||
from app.config import settings as app_settings
|
from app.config import settings as app_settings
|
||||||
from app.path import STATIC_DIR
|
from app.log import logger
|
||||||
|
from app.path import CONFIG_DIR, STATIC_DIR
|
||||||
|
|
||||||
|
from pydantic import ConfigDict, Field, create_model
|
||||||
|
from pydantic.main import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class APIMod(TypedDict):
|
class APIMod(TypedDict):
|
||||||
@@ -79,13 +83,15 @@ class Mod(TypedDict):
|
|||||||
|
|
||||||
API_MODS: dict[Literal[0, 1, 2, 3], dict[str, Mod]] = {}
|
API_MODS: dict[Literal[0, 1, 2, 3], dict[str, Mod]] = {}
|
||||||
|
|
||||||
mods_file = STATIC_DIR / "mods.json"
|
|
||||||
raw_mods = json.loads(mods_file.read_text(encoding="utf-8"))
|
def init_mods():
|
||||||
for ruleset in raw_mods:
|
mods_file = STATIC_DIR / "mods.json"
|
||||||
ruleset_mods = {}
|
raw_mods = json.loads(mods_file.read_text(encoding="utf-8"))
|
||||||
for mod in ruleset["Mods"]:
|
for ruleset in raw_mods:
|
||||||
ruleset_mods[mod["Acronym"]] = mod
|
ruleset_mods = {}
|
||||||
API_MODS[ruleset["RulesetID"]] = 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]:
|
||||||
@@ -107,91 +113,244 @@ def mods_to_int(mods: list[APIMod]) -> int:
|
|||||||
return sum_
|
return sum_
|
||||||
|
|
||||||
|
|
||||||
NO_CHECK = "DO_NO_CHECK"
|
DEFAULT_RANKED_MODS = {
|
||||||
|
0: {
|
||||||
# FIXME: 这里为空表示了两种情况:mod 没有配置项;任何时候都可以获得 pp
|
"EZ": {"retries": {"type": "number", "eq": 2}},
|
||||||
# 如果是后者,则 mod 更新的时候可能会误判。
|
"NF": {},
|
||||||
COMMON_CONFIG: dict[str, dict] = {
|
"HT": {"speed_change": {"type": "number", "eq": 0.75}, "adjust_pitch": {"check": False, "type": "boolean"}},
|
||||||
"EZ": {"retries": 2},
|
"DC": {"speed_change": {"type": "number", "eq": 0.75}},
|
||||||
"NF": {},
|
"HR": {},
|
||||||
"HT": {"speed_change": 0.75, "adjust_pitch": NO_CHECK},
|
"SD": {
|
||||||
"DC": {"speed_change": 0.75},
|
"fail_on_slider_tail": {"check": False, "type": "boolean"},
|
||||||
"HR": {},
|
"restart": {"check": False, "type": "boolean"},
|
||||||
"SD": {},
|
},
|
||||||
"PF": {},
|
"PF": {"restart": {"check": False, "type": "boolean"}},
|
||||||
"HD": {},
|
"HD": {"only_fade_approach_circles": {"type": "boolean", "eq": False}},
|
||||||
"DT": {"speed_change": 1.5, "adjust_pitch": NO_CHECK},
|
"DT": {"speed_change": {"type": "number", "eq": 1.5}, "adjust_pitch": {"check": False, "type": "boolean"}},
|
||||||
"NC": {"speed_change": 1.5},
|
"NC": {"speed_change": {"type": "number", "eq": 1.5}},
|
||||||
"FL": {"size_multiplier": 1.0, "combo_based_size": True},
|
"FL": {
|
||||||
"AC": {},
|
"follow_delay": {"type": "number", "eq": 1.0},
|
||||||
"MU": {},
|
"size_multiplier": {"type": "number", "eq": 1.0},
|
||||||
"TD": {},
|
"combo_based_size": {"type": "boolean", "eq": True},
|
||||||
|
},
|
||||||
|
"AC": {
|
||||||
|
"minimum_accuracy": {"check": False, "type": "number"},
|
||||||
|
"accuracy_judge_mode": {"check": False, "type": "string"},
|
||||||
|
"restart": {"check": False, "type": "boolean"},
|
||||||
|
},
|
||||||
|
"MU": {
|
||||||
|
"inverse_muting": {"check": False, "type": "boolean"},
|
||||||
|
"enable_metronome": {"check": False, "type": "boolean"},
|
||||||
|
"mute_combo_count": {"check": False, "type": "number"},
|
||||||
|
"affects_hit_sounds": {"check": False, "type": "boolean"},
|
||||||
|
},
|
||||||
|
"TD": {},
|
||||||
|
"BL": {},
|
||||||
|
"NS": {"hidden_combo_count": {"check": False, "type": "number"}},
|
||||||
|
"SO": {},
|
||||||
|
"TC": {},
|
||||||
|
},
|
||||||
|
1: {
|
||||||
|
"EZ": {},
|
||||||
|
"NF": {},
|
||||||
|
"HT": {"speed_change": {"type": "number", "eq": 0.75}, "adjust_pitch": {"check": False, "type": "boolean"}},
|
||||||
|
"DC": {"speed_change": {"type": "number", "eq": 0.75}},
|
||||||
|
"HR": {},
|
||||||
|
"SD": {"restart": {"check": False, "type": "boolean"}},
|
||||||
|
"PF": {"restart": {"check": False, "type": "boolean"}},
|
||||||
|
"HD": {},
|
||||||
|
"DT": {"speed_change": {"type": "number", "eq": 1.5}, "adjust_pitch": {"check": False, "type": "boolean"}},
|
||||||
|
"NC": {"speed_change": {"type": "number", "eq": 1.5}},
|
||||||
|
"FL": {"size_multiplier": {"type": "number", "eq": 1.0}, "combo_based_size": {"type": "boolean", "eq": True}},
|
||||||
|
"AC": {
|
||||||
|
"minimum_accuracy": {"check": False, "type": "number"},
|
||||||
|
"accuracy_judge_mode": {"check": False, "type": "string"},
|
||||||
|
"restart": {"check": False, "type": "boolean"},
|
||||||
|
},
|
||||||
|
"MU": {
|
||||||
|
"inverse_muting": {"check": False, "type": "boolean"},
|
||||||
|
"enable_metronome": {"check": False, "type": "boolean"},
|
||||||
|
"mute_combo_count": {"check": False, "type": "number"},
|
||||||
|
"affects_hit_sounds": {"check": False, "type": "boolean"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
"EZ": {"retries": {"type": "number", "eq": 2}},
|
||||||
|
"NF": {},
|
||||||
|
"HT": {"speed_change": {"type": "number", "eq": 0.75}, "adjust_pitch": {"check": False, "type": "boolean"}},
|
||||||
|
"DC": {"speed_change": {"type": "number", "eq": 0.75}},
|
||||||
|
"HR": {},
|
||||||
|
"SD": {"restart": {"check": False, "type": "boolean"}},
|
||||||
|
"PF": {"restart": {"check": False, "type": "boolean"}},
|
||||||
|
"HD": {},
|
||||||
|
"DT": {"speed_change": {"type": "number", "eq": 1.5}, "adjust_pitch": {"check": False, "type": "boolean"}},
|
||||||
|
"NC": {"speed_change": {"type": "number", "eq": 1.5}},
|
||||||
|
"FL": {"size_multiplier": {"type": "number", "eq": 1.0}, "combo_based_size": {"type": "boolean", "eq": True}},
|
||||||
|
"AC": {
|
||||||
|
"minimum_accuracy": {"check": False, "type": "number"},
|
||||||
|
"accuracy_judge_mode": {"check": False, "type": "string"},
|
||||||
|
"restart": {"check": False, "type": "boolean"},
|
||||||
|
},
|
||||||
|
"MU": {
|
||||||
|
"inverse_muting": {"check": False, "type": "boolean"},
|
||||||
|
"enable_metronome": {"check": False, "type": "boolean"},
|
||||||
|
"mute_combo_count": {"check": False, "type": "number"},
|
||||||
|
"affects_hit_sounds": {"check": False, "type": "boolean"},
|
||||||
|
},
|
||||||
|
"NS": {"hidden_combo_count": {"check": False, "type": "number"}},
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
"EZ": {"retries": {"type": "number", "eq": 2}},
|
||||||
|
"NF": {},
|
||||||
|
"HT": {"speed_change": {"type": "number", "eq": 0.75}, "adjust_pitch": {"check": False, "type": "boolean"}},
|
||||||
|
"DC": {"speed_change": {"type": "number", "eq": 0.75}},
|
||||||
|
"SD": {"restart": {"check": False, "type": "boolean"}},
|
||||||
|
"PF": {
|
||||||
|
"require_perfect_hits": {"check": False, "type": "boolean"},
|
||||||
|
"restart": {"check": False, "type": "boolean"},
|
||||||
|
},
|
||||||
|
"HD": {},
|
||||||
|
"DT": {"speed_change": {"type": "number", "eq": 1.5}, "adjust_pitch": {"check": False, "type": "boolean"}},
|
||||||
|
"NC": {"speed_change": {"type": "number", "eq": 1.5}},
|
||||||
|
"FL": {"size_multiplier": {"type": "number", "eq": 1.0}, "combo_based_size": {"type": "boolean", "eq": False}},
|
||||||
|
"AC": {
|
||||||
|
"minimum_accuracy": {"check": False, "type": "number"},
|
||||||
|
"accuracy_judge_mode": {"check": False, "type": "string"},
|
||||||
|
"restart": {"check": False, "type": "boolean"},
|
||||||
|
},
|
||||||
|
"MU": {
|
||||||
|
"inverse_muting": {"check": False, "type": "boolean"},
|
||||||
|
"enable_metronome": {"check": False, "type": "boolean"},
|
||||||
|
"mute_combo_count": {"check": False, "type": "number"},
|
||||||
|
"affects_hit_sounds": {"check": False, "type": "boolean"},
|
||||||
|
},
|
||||||
|
"MR": {},
|
||||||
|
"4K": {},
|
||||||
|
"5K": {},
|
||||||
|
"6K": {},
|
||||||
|
"7K": {},
|
||||||
|
"8K": {},
|
||||||
|
"9K": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
TYPE_TO_PY = {
|
||||||
|
"number": int | float,
|
||||||
|
"boolean": bool,
|
||||||
|
"string": str,
|
||||||
}
|
}
|
||||||
|
|
||||||
RANKED_MODS: dict[int, dict[str, dict]] = {
|
RulesetRankedMods = dict[str, dict[str, Any]]
|
||||||
0: deepcopy(COMMON_CONFIG),
|
RankedMods = dict[int, RulesetRankedMods]
|
||||||
1: deepcopy(COMMON_CONFIG),
|
RANKED_MODS: RankedMods = {}
|
||||||
2: deepcopy(COMMON_CONFIG),
|
|
||||||
3: deepcopy(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_vanilla(ruleset_id: int, mods: list[APIMod]) -> bool:
|
class _LegacyModSettings(BaseModel):
|
||||||
ranked_mods = RANKED_MODS[ruleset_id]
|
enable_all_mods_pp: bool = False
|
||||||
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 mods_can_get_pp(ruleset_id: int, mods: list[APIMod]) -> bool:
|
def _get_mods_file_checksum() -> str:
|
||||||
if app_settings.enable_all_mods_pp:
|
current_mods_file = STATIC_DIR / "mods.json"
|
||||||
|
if not current_mods_file.exists():
|
||||||
|
return ""
|
||||||
|
return hashlib.md5(current_mods_file.read_bytes(), usedforsecurity=False).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def generate_ranked_mod_settings(enable_all: bool = False):
|
||||||
|
ranked_mods_file = CONFIG_DIR / "ranked_mods.json"
|
||||||
|
checksum = _get_mods_file_checksum()
|
||||||
|
legacy_setting = _LegacyModSettings.model_validate(app_settings.model_dump())
|
||||||
|
if not legacy_setting.enable_all_mods_pp and not enable_all:
|
||||||
|
result = DEFAULT_RANKED_MODS
|
||||||
|
else:
|
||||||
|
result = {}
|
||||||
|
for ruleset_id, ruleset_mods in API_MODS.items():
|
||||||
|
result[ruleset_id] = {}
|
||||||
|
for mod_acronym in ruleset_mods:
|
||||||
|
result[ruleset_id][mod_acronym] = {}
|
||||||
|
if not enable_all:
|
||||||
|
logger.info("ENABLE_ALL_MODS_PP is deprecated, transformed to config/ranked_mods.json")
|
||||||
|
result["$mods_checksum"] = checksum # pyright: ignore[reportArgumentType]
|
||||||
|
ranked_mods_file.write_text(json.dumps(result, indent=4))
|
||||||
|
|
||||||
|
|
||||||
|
def init_ranked_mods():
|
||||||
|
ranked_mods_file = CONFIG_DIR / "ranked_mods.json"
|
||||||
|
if ranked_mods_file.exists():
|
||||||
|
raw_ranked_mods = json.loads(ranked_mods_file.read_text(encoding="utf-8"))
|
||||||
|
mods_file_checksum = raw_ranked_mods.pop("$mods_checksum", None)
|
||||||
|
if mods_file_checksum is not None and mods_file_checksum != (current_checksum := _get_mods_file_checksum()):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Mods file has changed, please modify ranked_mods.json or delete it to regenerate\n"
|
||||||
|
f"Current mods checksum: {current_checksum}"
|
||||||
|
)
|
||||||
|
for ruleset_id_str, mods in raw_ranked_mods.items():
|
||||||
|
ruleset_id = int(ruleset_id_str)
|
||||||
|
RANKED_MODS[ruleset_id] = mods
|
||||||
|
else:
|
||||||
|
generate_ranked_mod_settings()
|
||||||
|
init_ranked_mods()
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_model(settings: dict[str, dict[str, Any]]) -> type[BaseModel]:
|
||||||
|
fields = {}
|
||||||
|
for setting, validation in settings.items():
|
||||||
|
type_ = validation.get("type")
|
||||||
|
if type_ is None:
|
||||||
|
raise ValueError("Type is required")
|
||||||
|
py_type = TYPE_TO_PY.get(type_)
|
||||||
|
if py_type is None:
|
||||||
|
raise ValueError(f"Unknown type: {type_}")
|
||||||
|
|
||||||
|
if validation.get("check", True) is False:
|
||||||
|
fields[setting] = (Any, None)
|
||||||
|
elif (const_value := validation.get("eq")) is not None:
|
||||||
|
fields[setting] = (Literal[const_value], const_value)
|
||||||
|
elif (some_values := validation.get("in")) is not None:
|
||||||
|
if not isinstance(some_values, list) or len(some_values) == 0:
|
||||||
|
raise ValueError("In must be a non-empty list")
|
||||||
|
fields[setting] = (Literal[*some_values], some_values[0])
|
||||||
|
else:
|
||||||
|
copy = validation.copy()
|
||||||
|
copy.pop("type", None)
|
||||||
|
fields[setting] = (py_type | None, Field(default=None, **copy))
|
||||||
|
if not fields:
|
||||||
|
raise ValueError("No fields")
|
||||||
|
return create_model("ModSettingsValidator", __config__=ConfigDict(extra="forbid"), **fields)
|
||||||
|
|
||||||
|
|
||||||
|
def check_settings(mod: APIMod, ranked_mods: RulesetRankedMods) -> bool:
|
||||||
|
if (settings := ranked_mods.get(mod["acronym"])) is None:
|
||||||
|
return False
|
||||||
|
if settings == {}:
|
||||||
return True
|
return True
|
||||||
ranked_mods = RANKED_MODS[ruleset_id]
|
model = _generate_model(settings)
|
||||||
|
try:
|
||||||
|
model.model_validate(mod.get("settings", {}))
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _mods_can_get_pp(ruleset_id: int, mods: list[APIMod], ranked_mods: RankedMods) -> bool:
|
||||||
for mod in mods:
|
for mod in mods:
|
||||||
if app_settings.enable_rx and mod["acronym"] == "RX" and ruleset_id in {0, 1, 2}:
|
if app_settings.enable_rx and mod["acronym"] == "RX" and ruleset_id in {0, 1, 2}:
|
||||||
continue
|
continue
|
||||||
if app_settings.enable_ap and mod["acronym"] == "AP" and ruleset_id == 0:
|
if app_settings.enable_ap and mod["acronym"] == "AP" and ruleset_id == 0:
|
||||||
continue
|
continue
|
||||||
|
check_settings_result = check_settings(mod, ranked_mods[ruleset_id])
|
||||||
mod["settings"] = mod.get("settings", {})
|
if not check_settings_result:
|
||||||
if (settings := ranked_mods.get(mod["acronym"])) is None:
|
|
||||||
return False
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def mods_can_get_pp_vanilla(ruleset_id: int, mods: list[APIMod]) -> bool:
|
||||||
|
return _mods_can_get_pp(ruleset_id, mods, DEFAULT_RANKED_MODS)
|
||||||
|
|
||||||
|
|
||||||
|
def mods_can_get_pp(ruleset_id: int, mods: list[APIMod]) -> bool:
|
||||||
|
return _mods_can_get_pp(ruleset_id, mods, RANKED_MODS)
|
||||||
|
|
||||||
|
|
||||||
ENUM_TO_STR = {
|
ENUM_TO_STR = {
|
||||||
0: {
|
0: {
|
||||||
"MR": {"reflection"},
|
"MR": {"reflection"},
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ from __future__ import annotations
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
STATIC_DIR = Path(__file__).parent.parent / "static"
|
STATIC_DIR = Path(__file__).parent.parent / "static"
|
||||||
|
CONFIG_DIR = Path(__file__).parent.parent / "config"
|
||||||
ACHIEVEMENTS_DIR = Path(__file__).parent / "achievements"
|
ACHIEVEMENTS_DIR = Path(__file__).parent / "achievements"
|
||||||
|
|||||||
0
config/.gitkeep
Normal file
0
config/.gitkeep
Normal file
@@ -30,6 +30,7 @@ services:
|
|||||||
- ./static:/app/static
|
- ./static:/app/static
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
- ./newrelic.ini:/app/newrelic.ini:ro
|
- ./newrelic.ini:/app/newrelic.ini:ro
|
||||||
|
- ./config:/app/config
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- osu-network
|
- osu-network
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ services:
|
|||||||
- ./static:/app/static
|
- ./static:/app/static
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
- ./newrelic.ini:/app/newrelic.ini:ro
|
- ./newrelic.ini:/app/newrelic.ini:ro
|
||||||
|
- ./config:/app/config
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- osu-network
|
- osu-network
|
||||||
|
|||||||
3
main.py
3
main.py
@@ -10,6 +10,7 @@ from app.dependencies.fetcher import get_fetcher
|
|||||||
from app.dependencies.scheduler import start_scheduler, stop_scheduler
|
from app.dependencies.scheduler import start_scheduler, stop_scheduler
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.middleware.verify_session import VerifySessionMiddleware
|
from app.middleware.verify_session import VerifySessionMiddleware
|
||||||
|
from app.models.mods import init_mods, init_ranked_mods
|
||||||
from app.router import (
|
from app.router import (
|
||||||
api_v1_router,
|
api_v1_router,
|
||||||
api_v2_router,
|
api_v2_router,
|
||||||
@@ -51,6 +52,8 @@ import sentry_sdk
|
|||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
# on startup
|
# on startup
|
||||||
|
init_mods()
|
||||||
|
init_ranked_mods()
|
||||||
await FastAPILimiter.init(get_redis())
|
await FastAPILimiter.init(get_redis())
|
||||||
await get_fetcher() # 初始化 fetcher
|
await get_fetcher() # 初始化 fetcher
|
||||||
await init_geoip() # 初始化 GeoIP 数据库
|
await init_geoip() # 初始化 GeoIP 数据库
|
||||||
|
|||||||
11
tools/generate_ranked_mods.py
Normal file
11
tools/generate_ranked_mods.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
from app.models.mods import generate_ranked_mod_settings, init_mods
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
init_mods()
|
||||||
|
generate_ranked_mod_settings(enable_all="--all" in sys.argv)
|
||||||
@@ -242,7 +242,7 @@ async def _recalculate_statistics(statistics: UserStatistics, session: AsyncSess
|
|||||||
|
|
||||||
for score in scores:
|
for score in scores:
|
||||||
beatmap: Beatmap = score.beatmap
|
beatmap: Beatmap = score.beatmap
|
||||||
ranked = beatmap.beatmap_status.has_pp() | settings.enable_all_mods_pp
|
ranked = beatmap.beatmap_status.has_pp() | settings.enable_all_beatmap_pp
|
||||||
|
|
||||||
statistics.play_count += 1
|
statistics.play_count += 1
|
||||||
statistics.total_score += score.total_score
|
statistics.total_score += score.total_score
|
||||||
|
|||||||
Reference in New Issue
Block a user