feat(custom-rulesets): support custom rulesets (#23)

* feat(custom_ruleset): add custom rulesets support

* feat(custom-ruleset): add version check

* feat(custom-ruleset): add LegacyIO API to get ruleset hashes

* feat(pp): add check for rulesets whose pp cannot be calculated

* docs(readme): update README to include support for custom rulesets

* fix(custom-ruleset): make `rulesets` empty instead of throw a error when version check is disabled

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore(custom-ruleset): apply the latest changes of generator

c891bcd159

and

e25041ad3b

* feat(calculator): add fallback performance calculation for unsupported modes

* fix(calculator): remove debug print

* fix: resolve reviews

* feat(calculator): add difficulty calculation checks

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
MingxuanGame
2025-10-26 21:10:36 +08:00
committed by GitHub
parent 8f4a9d5fed
commit 33f321952d
24 changed files with 3134 additions and 74 deletions

View File

@@ -25,11 +25,13 @@ logger = log("Calculator")
CALCULATOR: PerformanceCalculator | None = None
def init_calculator():
async def init_calculator():
global CALCULATOR
try:
module = importlib.import_module(f"app.calculators.performance.{settings.calculator}")
CALCULATOR = module.PerformanceCalculator(**settings.calculator_config)
if CALCULATOR is not None:
await CALCULATOR.init()
except (ImportError, AttributeError) as e:
raise ImportError(f"Failed to import performance calculator for {settings.calculator}") from e
return CALCULATOR
@@ -50,6 +52,26 @@ def clamp[T: int | float](n: T, min_value: T, max_value: T) -> T:
return n
def calculate_pp_for_no_calculator(score: "Score", star_rating: float) -> float:
# TODO: Improve this algorithm
# https://www.desmos.com/calculator/i2aa7qm3o6
k = 4.0
pmax = 1.4 * (star_rating**2.8)
b = 0.95 - 0.33 * ((clamp(star_rating, 1, 8) - 1) / 7)
x = score.total_score / 1000000
if x < b:
# Linear section
return pmax * x
else:
# Exponential reward section
x = (x - b) / (1 - b)
exp_part = (math.exp(k * x) - 1) / (math.exp(k) - 1)
return pmax * (b + (1 - b) * exp_part)
async def calculate_pp(score: "Score", beatmap: str, session: AsyncSession) -> float:
from app.database.beatmap import BannedBeatmaps
@@ -68,8 +90,18 @@ async def calculate_pp(score: "Score", beatmap: str, session: AsyncSession) -> f
except Exception:
logger.exception(f"Error checking if beatmap {score.beatmap_id} is suspicious")
attrs = await get_calculator().calculate_performance(beatmap, score)
pp = attrs.pp
if not (await get_calculator().can_calculate_performance(score.gamemode)):
if not settings.fallback_no_calculator_pp:
return 0
star_rating = -1
if await get_calculator().can_calculate_difficulty(score.gamemode):
star_rating = (await get_calculator().calculate_difficulty(beatmap, score.mods, score.gamemode)).star_rating
if star_rating < 0:
star_rating = (await score.awaitable_attrs.beatmap).difficulty_rating
pp = calculate_pp_for_no_calculator(score, star_rating)
else:
attrs = await get_calculator().calculate_performance(beatmap, score)
pp = attrs.pp
if settings.suspicious_score_check and (pp > 3000):
logger.warning(