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

@@ -0,0 +1,122 @@
"""gamemode: add custom rulesets for sentakki, tau, rush, hishigata & soyokaze
Revision ID: 2d395ba2b4fd
Revises: ceabe941b207
Create Date: 2025-10-25 12:20:06.681929
"""
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "2d395ba2b4fd"
down_revision: str | Sequence[str] | None = "ceabe941b207"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
OLD_MODES: tuple[str, ...] = (
"OSU",
"TAIKO",
"FRUITS",
"MANIA",
"OSURX",
"OSUAP",
"TAIKORX",
"FRUITSRX",
)
CUSTOM_MODES: tuple[str, ...] = (
"SENTAKKI",
"TAU",
"RUSH",
"HISHIGATA",
"SOYOKAZE",
)
NEW_MODES: tuple[str, ...] = OLD_MODES + CUSTOM_MODES
TARGET_COLUMNS: tuple[tuple[str, str], ...] = (
("lazer_users", "playmode"),
("lazer_users", "g0v0_playmode"),
("beatmaps", "mode"),
("lazer_user_statistics", "mode"),
("score_tokens", "ruleset_id"),
("scores", "gamemode"),
("best_scores", "gamemode"),
("total_score_best_scores", "gamemode"),
("rank_history", "mode"),
("rank_top", "mode"),
("teams", "playmode"),
)
def _gamemode_enum(values: tuple[str, ...]) -> mysql.ENUM:
return mysql.ENUM(*values, name="gamemode")
def upgrade() -> None:
"""Upgrade schema."""
for table, column in TARGET_COLUMNS:
op.alter_column(
table,
column,
existing_type=_gamemode_enum(OLD_MODES),
type_=_gamemode_enum(NEW_MODES),
)
def downgrade() -> None:
"""Downgrade schema."""
placeholders = ", ".join(f":mode_{index}" for index in range(len(CUSTOM_MODES)))
mode_params = {f"mode_{index}": mode for index, mode in enumerate(CUSTOM_MODES)}
cleanup_templates = [
"DELETE FROM playlist_best_scores WHERE score_id IN (SELECT id FROM scores WHERE gamemode IN ({placeholders}))",
"DELETE FROM total_score_best_scores WHERE gamemode IN ({placeholders})",
"DELETE FROM best_scores WHERE gamemode IN ({placeholders})",
"DELETE FROM score_tokens WHERE ruleset_id IN ({placeholders})",
"DELETE FROM score_tokens WHERE score_id IN (SELECT id FROM scores WHERE gamemode IN ({placeholders}))",
"DELETE FROM score_tokens WHERE beatmap_id IN (SELECT id FROM beatmaps WHERE mode IN ({placeholders}))",
"DELETE FROM scores WHERE gamemode IN ({placeholders})",
"DELETE FROM rank_history WHERE mode IN ({placeholders})",
"DELETE FROM rank_top WHERE mode IN ({placeholders})",
"DELETE FROM lazer_user_statistics WHERE mode IN ({placeholders})",
"DELETE FROM team_requests WHERE team_id IN (SELECT id FROM teams WHERE playmode IN ({placeholders}))",
"DELETE FROM team_members WHERE team_id IN (SELECT id FROM teams WHERE playmode IN ({placeholders}))",
"DELETE FROM teams WHERE playmode IN ({placeholders})",
(
"DELETE FROM matchmaking_pool_beatmaps WHERE beatmap_id IN "
"(SELECT id FROM beatmaps WHERE mode IN ({placeholders}))"
),
"DELETE FROM beatmap_playcounts WHERE beatmap_id IN (SELECT id FROM beatmaps WHERE mode IN ({placeholders}))",
"DELETE FROM beatmap_tags WHERE beatmap_id IN (SELECT id FROM beatmaps WHERE mode IN ({placeholders}))",
"DELETE FROM failtime WHERE beatmap_id IN (SELECT id FROM beatmaps WHERE mode IN ({placeholders}))",
"DELETE FROM room_playlists WHERE beatmap_id IN (SELECT id FROM beatmaps WHERE mode IN ({placeholders}))",
"DELETE FROM banned_beatmaps WHERE beatmap_id IN (SELECT id FROM beatmaps WHERE mode IN ({placeholders}))",
"DELETE FROM beatmaps WHERE mode IN ({placeholders})",
]
for template in cleanup_templates:
statement = template.format(placeholders=placeholders)
op.execute(sa.text(statement), parameters=dict(mode_params))
# Reset persisted user modes to a supported option before shrinking the enum domain.
update_templates = [
"UPDATE lazer_users SET g0v0_playmode = 'OSU' WHERE g0v0_playmode IN ({placeholders})",
"UPDATE lazer_users SET playmode = 'OSU' WHERE playmode IN ({placeholders})",
]
for template in update_templates:
op.execute(sa.text(template.format(placeholders=placeholders)), parameters=dict(mode_params))
for table, column in TARGET_COLUMNS:
op.alter_column(
table,
column,
existing_type=_gamemode_enum(NEW_MODES),
type_=_gamemode_enum(OLD_MODES),
)