From b359be363746cfa4ff920836734c74cc6baa2626 Mon Sep 17 00:00:00 2001 From: MingxuanGame Date: Sun, 27 Jul 2025 02:33:42 +0000 Subject: [PATCH] feat(solo-score): support submit solo scores --- app/database/__init__.py | 13 + app/database/beatmap.py | 4 + app/database/beatmapset.py | 3 +- app/database/score.py | 259 ++- app/database/score_token.py | 47 + app/database/team.py | 7 +- app/database/user.py | 10 +- app/dependencies/user.py | 2 +- app/models/score.py | 209 +- app/path.py | 7 + app/router/beatmap.py | 5 +- app/utils.py | 7 +- static/README.md | 5 + static/mods.json | 3656 +++++++++++++++++++++++++++++++++++ 14 files changed, 4170 insertions(+), 64 deletions(-) create mode 100644 app/database/score_token.py create mode 100644 app/path.py create mode 100644 static/README.md create mode 100644 static/mods.json diff --git a/app/database/__init__.py b/app/database/__init__.py index b7df7d6..65ca463 100644 --- a/app/database/__init__.py +++ b/app/database/__init__.py @@ -9,6 +9,13 @@ from .beatmapset import ( ) from .legacy import LegacyOAuthToken, LegacyUserStatistics from .relationship import Relationship, RelationshipResp, RelationshipType +from .score import ( + Score, + ScoreBase, + ScoreResp, + ScoreStatistics, +) +from .score_token import ScoreToken, ScoreTokenResp from .team import Team, TeamMember from .user import ( DailyChallengeStats, @@ -57,6 +64,12 @@ __all__ = [ "Relationship", "RelationshipResp", "RelationshipType", + "Score", + "ScoreBase", + "ScoreResp", + "ScoreStatistics", + "ScoreToken", + "ScoreTokenResp", "Team", "TeamMember", "User", diff --git a/app/database/beatmap.py b/app/database/beatmap.py index e5b230b..ea15799 100644 --- a/app/database/beatmap.py +++ b/app/database/beatmap.py @@ -65,6 +65,10 @@ class Beatmap(BeatmapBase, table=True): # optional beatmapset: Beatmapset = Relationship(back_populates="beatmaps") + @property + def can_ranked(self) -> bool: + return self.beatmap_status > BeatmapRankStatus.PENDING + @classmethod async def from_resp(cls, session: AsyncSession, resp: "BeatmapResp") -> "Beatmap": d = resp.model_dump() diff --git a/app/database/beatmapset.py b/app/database/beatmapset.py index f978814..1e6ba27 100644 --- a/app/database/beatmapset.py +++ b/app/database/beatmapset.py @@ -2,6 +2,7 @@ from datetime import datetime from typing import TYPE_CHECKING, TypedDict, cast from app.models.beatmap import BeatmapRankStatus, Genre, Language +from app.models.score import GameMode from pydantic import BaseModel, model_serializer from sqlalchemy import DECIMAL, JSON, Column, DateTime, Text @@ -68,7 +69,7 @@ class BeatmapNomination(TypedDict): beatmapset_id: int reset: bool user_id: int - rulesets: list[str] | None + rulesets: list[GameMode] | None class BeatmapDescription(SQLModel): diff --git a/app/database/score.py b/app/database/score.py index 82e9f34..f82a813 100644 --- a/app/database/score.py +++ b/app/database/score.py @@ -2,15 +2,34 @@ from datetime import datetime import math from app.database.user import User +from app.models.beatmap import BeatmapRankStatus from app.models.mods import APIMod -from app.models.score import MODE_TO_INT, GameMode, Rank +from app.models.score import ( + MODE_TO_INT, + GameMode, + HitResult, + LeaderboardType, + Rank, + ScoreStatistics, +) from .beatmap import Beatmap, BeatmapResp from .beatmapset import BeatmapsetResp -from pydantic import BaseModel from sqlalchemy import Column, DateTime -from sqlmodel import JSON, BigInteger, Field, Relationship, SQLModel +from sqlalchemy.orm import joinedload +from sqlmodel import ( + JSON, + BigInteger, + Field, + Relationship, + SQLModel, + col, + false, + func, + select, +) +from sqlmodel.ext.asyncio.session import AsyncSession class ScoreBase(SQLModel): @@ -34,6 +53,9 @@ class ScoreBase(SQLModel): room_id: int | None = Field(default=None) # multiplayer started_at: datetime = Field(sa_column=Column(DateTime)) total_score: int = Field(default=0, sa_column=Column(BigInteger)) + total_score_without_mods: int = Field( + default=0, sa_column=Column(BigInteger), exclude=True + ) type: str # optional @@ -41,20 +63,11 @@ class ScoreBase(SQLModel): position: int | None = Field(default=None) # multiplayer -class ScoreStatistics(BaseModel): - count_miss: int - count_50: int - count_100: int - count_300: int - count_geki: int - count_katu: int - count_large_tick_miss: int | None = None - count_slider_tail_hit: int | None = None - - class Score(ScoreBase, table=True): __tablename__ = "scores" # pyright: ignore[reportAssignmentType] - id: int = Field(primary_key=True) + id: int | None = Field( + default=None, sa_column=Column(BigInteger, autoincrement=True, primary_key=True) + ) beatmap_id: int = Field(index=True, foreign_key="beatmaps.id") user_id: int = Field(foreign_key="users.id", index=True) # ScoreStatistics @@ -72,6 +85,10 @@ class Score(ScoreBase, table=True): beatmap: "Beatmap" = Relationship() user: "User" = Relationship() + @property + def is_perfect_combo(self) -> bool: + return self.max_combo == self.beatmap.max_combo + class ScoreResp(ScoreBase): id: int @@ -85,10 +102,13 @@ class ScoreResp(ScoreBase): beatmapset: BeatmapsetResp | None = None # FIXME: user: APIUser | None = None statistics: ScoreStatistics | None = None + rank_global: int | None = None + rank_country: int | None = None @classmethod - def from_db(cls, score: Score) -> "ScoreResp": + async def from_db(cls, session: AsyncSession, score: Score) -> "ScoreResp": s = cls.model_validate(score.model_dump()) + assert score.id s.beatmap = BeatmapResp.from_db(score.beatmap) s.beatmapset = BeatmapsetResp.from_db(score.beatmap.beatmapset) s.is_perfect_combo = s.max_combo == s.beatmap.max_combo @@ -97,14 +117,203 @@ class ScoreResp(ScoreBase): if score.best_id: # https://osu.ppy.sh/wiki/Performance_points/Weighting_system s.weight = math.pow(0.95, score.best_id) - s.statistics = ScoreStatistics( - count_miss=score.nmiss, - count_50=score.n50, - count_100=score.n100, - count_300=score.n300, - count_geki=score.ngeki, - count_katu=score.nkatu, - count_large_tick_miss=score.nlarge_tick_miss, - count_slider_tail_hit=score.nslider_tail_hit, + s.statistics = { + HitResult.MISS: score.nmiss, + HitResult.MEH: score.n50, + HitResult.OK: score.n100, + HitResult.GREAT: score.n300, + HitResult.PERFECT: score.ngeki, + HitResult.GOOD: score.nkatu, + } + if score.nlarge_tick_miss is not None: + s.statistics[HitResult.LARGE_TICK_MISS] = score.nlarge_tick_miss + if score.nslider_tail_hit is not None: + s.statistics[HitResult.SLIDER_TAIL_HIT] = score.nslider_tail_hit + # s.user = await convert_db_user_to_api_user(score.user) + s.rank_global = ( + await get_score_position_by_id( + session, + score.map_md5, + score.id, + mode=score.gamemode, + user=score.user, + ) + or None + ) + s.rank_country = ( + await get_score_position_by_id( + session, + score.map_md5, + score.id, + score.gamemode, + score.user, + ) + or None ) return s + + +async def get_leaderboard( + session: AsyncSession, + beatmap_md5: str, + mode: GameMode, + type: LeaderboardType = LeaderboardType.GLOBAL, + mods: list[APIMod] | None = None, + user: User | None = None, + limit: int = 50, +) -> list[Score]: + scores = [] + if type == LeaderboardType.GLOBAL: + query = ( + select(Score) + .where( + col(Beatmap.beatmap_status).in_( + [ + BeatmapRankStatus.RANKED, + BeatmapRankStatus.LOVED, + BeatmapRankStatus.QUALIFIED, + BeatmapRankStatus.APPROVED, + ] + ), + Score.map_md5 == beatmap_md5, + Score.gamemode == mode, + col(Score.passed).is_(True), + Score.mods == mods if user and user.is_supporter else false(), + ) + .limit(limit) + .order_by( + col(Score.total_score).desc(), + ) + ) + result = await session.exec(query) + scores = list[Score](result.all()) + elif type == LeaderboardType.FRIENDS and user and user.is_supporter: + # TODO + ... + elif type == LeaderboardType.TEAM and user and user.team_membership: + team_id = user.team_membership.team_id + query = ( + select(Score) + .join(Beatmap) + .options(joinedload(Score.user)) # pyright: ignore[reportArgumentType] + .where( + Score.map_md5 == beatmap_md5, + Score.gamemode == mode, + col(Score.passed).is_(True), + col(Score.user.team_membership).is_not(None), + Score.user.team_membership.team_id == team_id, # pyright: ignore[reportOptionalMemberAccess] + Score.mods == mods if user and user.is_supporter else false(), + ) + .limit(limit) + .order_by( + col(Score.total_score).desc(), + ) + ) + result = await session.exec(query) + scores = list[Score](result.all()) + if user: + user_score = ( + await session.exec( + select(Score).where( + Score.map_md5 == beatmap_md5, + Score.gamemode == mode, + Score.user_id == user.id, + col(Score.passed).is_(True), + ) + ) + ).first() + if user_score and user_score not in scores: + scores.append(user_score) + return scores + + +async def get_score_position_by_user( + session: AsyncSession, + beatmap_md5: str, + user: User, + mode: GameMode, + type: LeaderboardType = LeaderboardType.GLOBAL, + mods: list[APIMod] | None = None, +) -> int: + where_clause = [ + Score.map_md5 == beatmap_md5, + Score.gamemode == mode, + col(Score.passed).is_(True), + col(Beatmap.beatmap_status).in_( + [ + BeatmapRankStatus.RANKED, + BeatmapRankStatus.LOVED, + BeatmapRankStatus.QUALIFIED, + BeatmapRankStatus.APPROVED, + ] + ), + ] + if mods and user.is_supporter: + where_clause.append(Score.mods == mods) + else: + where_clause.append(false()) + if type == LeaderboardType.FRIENDS and user.is_supporter: + # TODO + ... + elif type == LeaderboardType.TEAM and user.team_membership: + team_id = user.team_membership.team_id + where_clause.append( + col(Score.user.team_membership).is_not(None), + ) + where_clause.append( + Score.user.team_membership.team_id == team_id, # pyright: ignore[reportOptionalMemberAccess] + ) + rownum = ( + func.row_number() + .over( + partition_by=Score.map_md5, + order_by=col(Score.total_score).desc(), + ) + .label("row_number") + ) + subq = select(Score, rownum).join(Beatmap).where(*where_clause).subquery() + stmt = select(subq.c.row_number).where(subq.c.user == user) + result = await session.exec(stmt) + s = result.one_or_none() + return s if s else 0 + + +async def get_score_position_by_id( + session: AsyncSession, + beatmap_md5: str, + score_id: int, + mode: GameMode, + user: User | None = None, + type: LeaderboardType = LeaderboardType.GLOBAL, + mods: list[APIMod] | None = None, +) -> int: + where_clause = [ + Score.map_md5 == beatmap_md5, + Score.id == score_id, + Score.gamemode == mode, + col(Beatmap.beatmap_status).in_( + [ + BeatmapRankStatus.RANKED, + BeatmapRankStatus.LOVED, + BeatmapRankStatus.QUALIFIED, + BeatmapRankStatus.APPROVED, + ] + ), + ] + if mods and user and user.is_supporter: + where_clause.append(Score.mods == mods) + elif mods: + where_clause.append(false()) + rownum = ( + func.row_number() + .over( + partition_by=Score.map_md5, + order_by=col(Score.total_score).desc(), + ) + .label("row_number") + ) + subq = select(Score, rownum).join(Beatmap).where(*where_clause).subquery() + stmt = select(subq.c.row_number).where(subq.c.id == score_id) + result = await session.exec(stmt) + s = result.one_or_none() + return s if s else 0 diff --git a/app/database/score_token.py b/app/database/score_token.py new file mode 100644 index 0000000..195a174 --- /dev/null +++ b/app/database/score_token.py @@ -0,0 +1,47 @@ +from datetime import datetime + +from app.models.score import GameMode + +from .beatmap import Beatmap +from .user import User + +from sqlalchemy import Column, DateTime, Index +from sqlmodel import BigInteger, Field, ForeignKey, Relationship, SQLModel + + +class ScoreTokenBase(SQLModel): + score_id: int | None = Field(sa_column=Column(BigInteger), default=None) + ruleset_id: GameMode + playlist_item_id: int | None = Field(default=None) # playlist + created_at: datetime = Field( + default_factory=datetime.utcnow, sa_column=Column(DateTime) + ) + updated_at: datetime = Field( + default_factory=datetime.utcnow, sa_column=Column(DateTime) + ) + + +class ScoreToken(ScoreTokenBase, table=True): + __tablename__ = "score_tokens" # pyright: ignore[reportAssignmentType] + __table_args__ = (Index("idx_user_playlist", "user_id", "playlist_item_id"),) + + id: int | None = Field( + default=None, + primary_key=True, + index=True, + sa_column_kwargs={"autoincrement": True}, + ) + user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("users.id"))) + beatmap_id: int = Field(sa_column=Column(BigInteger, ForeignKey("beatmaps.id"))) + user: "User" = Relationship() + beatmap: "Beatmap" = Relationship() + + +class ScoreTokenResp(ScoreTokenBase): + id: int + user_id: int + beatmap_id: int + + @classmethod + def from_db(cls, obj: ScoreToken) -> "ScoreTokenResp": + return cls.model_validate(obj) diff --git a/app/database/team.py b/app/database/team.py index 5dabf71..2722319 100644 --- a/app/database/team.py +++ b/app/database/team.py @@ -2,7 +2,6 @@ from datetime import datetime from typing import TYPE_CHECKING from sqlalchemy import Column, DateTime -from sqlalchemy.orm import Mapped from sqlmodel import Field, Relationship, SQLModel if TYPE_CHECKING: @@ -20,7 +19,7 @@ class Team(SQLModel, table=True): default_factory=datetime.utcnow, sa_column=Column(DateTime) ) - members: Mapped[list["TeamMember"]] = Relationship(back_populates="team") + members: list["TeamMember"] = Relationship(back_populates="team") class TeamMember(SQLModel, table=True): @@ -33,5 +32,5 @@ class TeamMember(SQLModel, table=True): default_factory=datetime.utcnow, sa_column=Column(DateTime) ) - user: Mapped["User"] = Relationship(back_populates="team_membership") - team: Mapped["Team"] = Relationship(back_populates="members") + user: "User" = Relationship(back_populates="team_membership") + team: "Team" = Relationship(back_populates="members") diff --git a/app/database/user.py b/app/database/user.py index 8b6fe02..71a6eda 100644 --- a/app/database/user.py +++ b/app/database/user.py @@ -14,7 +14,9 @@ class User(SQLModel, table=True): __tablename__ = "users" # pyright: ignore[reportAssignmentType] # 主键 - id: int = Field(default=None, primary_key=True, index=True, nullable=False) + id: int = Field( + default=None, sa_column=Column(BigInteger, primary_key=True, index=True) + ) # 基本信息(匹配 migrations 中的结构) name: str = Field(max_length=32, unique=True, index=True) # 用户名 @@ -65,6 +67,10 @@ class User(SQLModel, table=True): latest_activity = getattr(self, "latest_activity", 0) return datetime.fromtimestamp(latest_activity) if latest_activity > 0 else None + @property + def is_supporter(self): + return self.lazer_profile.is_supporter if self.lazer_profile else False + # 关联关系 lazer_profile: Optional["LazerUserProfile"] = Relationship(back_populates="user") lazer_statistics: list["LazerUserStatistics"] = Relationship(back_populates="user") @@ -76,7 +82,7 @@ class User(SQLModel, table=True): back_populates="user" ) statistics: list["LegacyUserStatistics"] = Relationship(back_populates="user") - team_membership: list["TeamMember"] = Relationship(back_populates="user") + team_membership: Optional["TeamMember"] = Relationship(back_populates="user") daily_challenge_stats: Optional["DailyChallengeStats"] = Relationship( back_populates="user" ) diff --git a/app/dependencies/user.py b/app/dependencies/user.py index 5c3b396..8d3bb74 100644 --- a/app/dependencies/user.py +++ b/app/dependencies/user.py @@ -45,7 +45,7 @@ async def get_current_user_by_token(token: str, db: AsyncSession) -> DBUser | No selectinload(DBUser.lazer_achievements), # pyright: ignore[reportArgumentType] selectinload(DBUser.lazer_profile_sections), # pyright: ignore[reportArgumentType] selectinload(DBUser.statistics), # pyright: ignore[reportArgumentType] - selectinload(DBUser.team_membership), # pyright: ignore[reportArgumentType] + joinedload(DBUser.team_membership), # pyright: ignore[reportArgumentType] selectinload(DBUser.rank_history), # pyright: ignore[reportArgumentType] selectinload(DBUser.active_banners), # pyright: ignore[reportArgumentType] selectinload(DBUser.lazer_badges), # pyright: ignore[reportArgumentType] diff --git a/app/models/score.py b/app/models/score.py index 50c80f7..680afd6 100644 --- a/app/models/score.py +++ b/app/models/score.py @@ -1,7 +1,12 @@ from __future__ import annotations -from enum import Enum, IntEnum +from enum import Enum +import json +from typing import Any, Literal, TypedDict +from app.path import STATIC_DIR + +from pydantic import BaseModel, Field, ValidationInfo, field_validator import rosu_pp_py as rosu @@ -30,40 +35,186 @@ INT_TO_MODE = {v: k for k, v in MODE_TO_INT.items()} class Rank(str, Enum): - X = "ss" - XH = "ssh" - S = "s" - SH = "sh" - A = "a" - B = "b" - C = "c" - D = "d" - F = "f" + X = "X" + XH = "XH" + S = "S" + SH = "SH" + A = "A" + B = "B" + C = "C" + D = "D" + 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 -class HitResult(IntEnum): - PERFECT = 0 # [Order(0)] - GREAT = 1 # [Order(1)] - GOOD = 2 # [Order(2)] - OK = 3 # [Order(3)] - MEH = 4 # [Order(4)] - MISS = 5 # [Order(5)] +class HitResult(str, Enum): + PERFECT = "perfect" # [Order(0)] + GREAT = "great" # [Order(1)] + GOOD = "good" # [Order(2)] + OK = "ok" # [Order(3)] + MEH = "meh" # [Order(4)] + MISS = "miss" # [Order(5)] - LARGE_TICK_HIT = 6 # [Order(6)] - SMALL_TICK_HIT = 7 # [Order(7)] - SLIDER_TAIL_HIT = 8 # [Order(8)] + LARGE_TICK_HIT = "large_tick_hit" # [Order(6)] + SMALL_TICK_HIT = "small_tick_hit" # [Order(7)] + SLIDER_TAIL_HIT = "slider_tail_hit" # [Order(8)] - LARGE_BONUS = 9 # [Order(9)] - SMALL_BONUS = 10 # [Order(10)] + LARGE_BONUS = "large_bonus" # [Order(9)] + SMALL_BONUS = "small_bonus" # [Order(10)] - LARGE_TICK_MISS = 11 # [Order(11)] - SMALL_TICK_MISS = 12 # [Order(12)] + LARGE_TICK_MISS = "large_tick_miss" # [Order(11)] + SMALL_TICK_MISS = "small_tick_miss" # [Order(12)] - IGNORE_HIT = 13 # [Order(13)] - IGNORE_MISS = 14 # [Order(14)] + IGNORE_HIT = "ignore_hit" # [Order(13)] + IGNORE_MISS = "ignore_miss" # [Order(14)] - NONE = 15 # [Order(15)] - COMBO_BREAK = 16 # [Order(16)] + NONE = "none" # [Order(15)] + COMBO_BREAK = "combo_break" # [Order(16)] - LEGACY_COMBO_INCREASE = 99 # [Order(99)] @deprecated + LEGACY_COMBO_INCREASE = "legacy_combo_increase" # [Order(99)] @deprecated + + def is_hit(self) -> bool: + return self not in ( + HitResult.NONE, + HitResult.IGNORE_MISS, + HitResult.COMBO_BREAK, + HitResult.LARGE_TICK_MISS, + HitResult.SMALL_TICK_MISS, + HitResult.MISS, + ) + + +class LeaderboardType(Enum): + GLOBAL = "global" + FRIENDS = "friends" + COUNTRY = "country" + 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] + + +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): + rank: Rank + total_score: int = Field(ge=0, le=2**31 - 1) + total_score_without_mods: int = Field(ge=0, le=2**31 - 1) + accuracy: float = Field(ge=0, le=1) + pp: float = Field(default=0, ge=0, le=2**31 - 1) + max_combo: int = 0 + ruleset_id: Literal[0, 1, 2, 3] + passed: bool = False + mods: list[APIMod] = Field(default_factory=list) + statistics: ScoreStatistics = Field(default_factory=dict) + maximum_statistics: ScoreStatistics = Field(default_factory=dict) + + @field_validator("mods", mode="after") + @classmethod + def validate_mods(cls, mods: list[APIMod], info: ValidationInfo): + if not MODS: + _init_mods() + incompatible_mods = set() + # check incompatible mods + for mod in mods: + if mod["acronym"] in incompatible_mods: + raise ValueError( + f"Mod {mod['acronym']} is incompatible with other mods" + ) + setting_mods = MODS[info.data["ruleset_id"]].get(mod["acronym"]) + if not setting_mods: + raise ValueError(f"Invalid mod: {mod['acronym']}") + incompatible_mods.update(setting_mods["IncompatibleMods"]) + + +class LegacyReplaySoloScoreInfo(TypedDict): + online_id: int + mods: list[APIMod] + statistics: ScoreStatistics + maximum_statistics: ScoreStatistics + client_version: str + rank: Rank + user_id: int + total_score_without_mods: int diff --git a/app/path.py b/app/path.py new file mode 100644 index 0000000..b61309c --- /dev/null +++ b/app/path.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from pathlib import Path + +STATIC_DIR = Path(__file__).parent.parent / "static" + +REPLAY_DIR = Path(__file__).parent.parent / "replays" diff --git a/app/router/beatmap.py b/app/router/beatmap.py index 47a1137..4cf717e 100644 --- a/app/router/beatmap.py +++ b/app/router/beatmap.py @@ -16,7 +16,10 @@ from app.dependencies.user import get_current_user from app.fetcher import Fetcher from app.models.beatmap import BeatmapAttributes from app.models.mods import APIMod, int_to_mods -from app.models.score import INT_TO_MODE, GameMode +from app.models.score import ( + INT_TO_MODE, + GameMode, +) from app.utils import calculate_beatmap_attribute from .api_router import router diff --git a/app/utils.py b/app/utils.py index fc5020a..fe0c3fc 100644 --- a/app/utils.py +++ b/app/utils.py @@ -28,6 +28,11 @@ from app.models.user import ( import rosu_pp_py as rosu +def unix_timestamp_to_windows(timestamp: int) -> int: + """Convert a Unix timestamp to a Windows timestamp.""" + return timestamp * 10_000 + 11_644_473_600_000_000 + + async def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu") -> User: """将数据库用户模型转换为API用户模型(使用 Lazer 表)""" @@ -205,7 +210,7 @@ async def convert_db_user_to_api_user(db_user: DBUser, ruleset: str = "osu") -> # 转换团队信息 team = None if db_user.team_membership: - team_member = db_user.team_membership[0] # 假设用户只属于一个团队 + team_member = db_user.team_membership # 假设用户只属于一个团队 team = team_member.team # 创建用户对象 diff --git a/static/README.md b/static/README.md new file mode 100644 index 0000000..16ece63 --- /dev/null +++ b/static/README.md @@ -0,0 +1,5 @@ +# 静态文件 + +- `mods.json`: 包含了游戏中的所有可用mod的详细信息。 + - Origin: https://github.com/ppy/osu-web/blob/master/database/mods.json + - Version: 2025/6/10 `b68c920b1db3d443b9302fdc3f86010c875fe380` diff --git a/static/mods.json b/static/mods.json new file mode 100644 index 0000000..defb57f --- /dev/null +++ b/static/mods.json @@ -0,0 +1,3656 @@ +[ + { + "Name": "osu", + "RulesetID": 0, + "Mods": [ + { + "Acronym": "EZ", + "Name": "Easy", + "Description": "Larger circles, more forgiving HP drain, less accuracy required, and extra lives!", + "Type": "DifficultyReduction", + "Settings": [ + { + "Name": "retries", + "Type": "number", + "Label": "Extra Lives", + "Description": "Number of extra lives" + } + ], + "IncompatibleMods": [ + "HR", + "AC", + "DA" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "NF", + "Name": "No Fail", + "Description": "You can't fail, no matter what.", + "Type": "DifficultyReduction", + "Settings": [], + "IncompatibleMods": [ + "SD", + "PF", + "AC", + "CN" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "HT", + "Name": "Half Time", + "Description": "Less zoom...", + "Type": "DifficultyReduction", + "Settings": [ + { + "Name": "speed_change", + "Type": "number", + "Label": "Speed decrease", + "Description": "The actual decrease to apply" + }, + { + "Name": "adjust_pitch", + "Type": "boolean", + "Label": "Adjust pitch", + "Description": "Should pitch be adjusted with speed" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WU", + "WD", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "DC", + "Name": "Daycore", + "Description": "Whoaaaaa...", + "Type": "DifficultyReduction", + "Settings": [ + { + "Name": "speed_change", + "Type": "number", + "Label": "Speed decrease", + "Description": "The actual decrease to apply" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WU", + "WD", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "HR", + "Name": "Hard Rock", + "Description": "Everything just got a bit harder...", + "Type": "DifficultyIncrease", + "Settings": [], + "IncompatibleMods": [ + "EZ", + "DA", + "MR" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "SD", + "Name": "Sudden Death", + "Description": "Miss and fail.", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "fail_on_slider_tail", + "Type": "boolean", + "Label": "Also fail when missing a slider tail", + "Description": "" + }, + { + "Name": "restart", + "Type": "boolean", + "Label": "Restart on fail", + "Description": "Automatically restarts when failed." + } + ], + "IncompatibleMods": [ + "NF", + "PF", + "TP", + "CN" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "PF", + "Name": "Perfect", + "Description": "SS or quit.", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "restart", + "Type": "boolean", + "Label": "Restart on fail", + "Description": "Automatically restarts when failed." + } + ], + "IncompatibleMods": [ + "NF", + "SD", + "AC", + "CN" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "DT", + "Name": "Double Time", + "Description": "Zoooooooooom...", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "speed_change", + "Type": "number", + "Label": "Speed increase", + "Description": "The actual increase to apply" + }, + { + "Name": "adjust_pitch", + "Type": "boolean", + "Label": "Adjust pitch", + "Description": "Should pitch be adjusted with speed" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WU", + "WD", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "NC", + "Name": "Nightcore", + "Description": "Uguuuuuuuu...", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "speed_change", + "Type": "number", + "Label": "Speed increase", + "Description": "The actual increase to apply" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WU", + "WD", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "HD", + "Name": "Hidden", + "Description": "Play with no approach circles and fading circles/sliders.", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "only_fade_approach_circles", + "Type": "boolean", + "Label": "Only fade approach circles", + "Description": "The main object body will not fade when enabled." + } + ], + "IncompatibleMods": [ + "SI", + "TC", + "AD", + "DP" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "FL", + "Name": "Flashlight", + "Description": "Restricted view area.", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "follow_delay", + "Type": "number", + "Label": "Follow delay", + "Description": "Milliseconds until the flashlight reaches the cursor" + }, + { + "Name": "size_multiplier", + "Type": "number", + "Label": "Flashlight size", + "Description": "Multiplier applied to the default flashlight size." + }, + { + "Name": "combo_based_size", + "Type": "boolean", + "Label": "Change size based on combo", + "Description": "Decrease the flashlight size as combo increases." + } + ], + "IncompatibleMods": [ + "BL", + "BM" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "BL", + "Name": "Blinds", + "Description": "Play with blinds on your screen.", + "Type": "DifficultyIncrease", + "Settings": [], + "IncompatibleMods": [ + "FL" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "ST", + "Name": "Strict Tracking", + "Description": "Once you start a slider, follow precisely or get a miss.", + "Type": "DifficultyIncrease", + "Settings": [], + "IncompatibleMods": [ + "TP", + "CL" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "AC", + "Name": "Accuracy Challenge", + "Description": "Fail if your accuracy drops too low!", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "minimum_accuracy", + "Type": "number", + "Label": "Minimum accuracy", + "Description": "Trigger a failure if your accuracy goes below this value." + }, + { + "Name": "accuracy_judge_mode", + "Type": "string", + "Label": "Accuracy mode", + "Description": "The mode of accuracy that will trigger failure." + }, + { + "Name": "restart", + "Type": "boolean", + "Label": "Restart on fail", + "Description": "Automatically restarts when failed." + } + ], + "IncompatibleMods": [ + "EZ", + "NF", + "PF", + "CN" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "TP", + "Name": "Target Practice", + "Description": "Practice keeping up with the beat of the song.", + "Type": "Conversion", + "Settings": [ + { + "Name": "seed", + "Type": "number", + "Label": "Seed", + "Description": "Use a custom seed instead of a random one" + }, + { + "Name": "metronome", + "Type": "boolean", + "Label": "Metronome ticks", + "Description": "Whether a metronome beat should play in the background" + } + ], + "IncompatibleMods": [ + "SD", + "ST", + "RD", + "SO", + "TC", + "AD", + "DP" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "DA", + "Name": "Difficulty Adjust", + "Description": "Override a beatmap's difficulty settings.", + "Type": "Conversion", + "Settings": [ + { + "Name": "circle_size", + "Type": "number", + "Label": "Circle Size", + "Description": "Override a beatmap's set CS." + }, + { + "Name": "approach_rate", + "Type": "number", + "Label": "Approach Rate", + "Description": "Override a beatmap's set AR." + }, + { + "Name": "drain_rate", + "Type": "number", + "Label": "HP Drain", + "Description": "Override a beatmap's set HP." + }, + { + "Name": "overall_difficulty", + "Type": "number", + "Label": "Accuracy", + "Description": "Override a beatmap's set OD." + }, + { + "Name": "extended_limits", + "Type": "boolean", + "Label": "Extended Limits", + "Description": "Adjust difficulty beyond sane limits." + } + ], + "IncompatibleMods": [ + "EZ", + "HR" + ], + "RequiresConfiguration": true, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "CL", + "Name": "Classic", + "Description": "Feeling nostalgic?", + "Type": "Conversion", + "Settings": [ + { + "Name": "no_slider_head_accuracy", + "Type": "boolean", + "Label": "No slider head accuracy requirement", + "Description": "Scores sliders proportionally to the number of ticks hit." + }, + { + "Name": "classic_note_lock", + "Type": "boolean", + "Label": "Apply classic note lock", + "Description": "Applies note lock to the full hit window." + }, + { + "Name": "always_play_tail_sample", + "Type": "boolean", + "Label": "Always play a slider's tail sample", + "Description": "Always plays a slider's tail sample regardless of whether it was hit or not." + }, + { + "Name": "fade_hit_circle_early", + "Type": "boolean", + "Label": "Fade out hit circles earlier", + "Description": "Make hit circles fade out into a miss, rather than after it." + }, + { + "Name": "classic_health", + "Type": "boolean", + "Label": "Classic health", + "Description": "More closely resembles the original HP drain mechanics." + } + ], + "IncompatibleMods": [ + "ST" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "RD", + "Name": "Random", + "Description": "It never gets boring!", + "Type": "Conversion", + "Settings": [ + { + "Name": "angle_sharpness", + "Type": "number", + "Label": "Angle sharpness", + "Description": "How sharp angles should be" + }, + { + "Name": "seed", + "Type": "number", + "Label": "Seed", + "Description": "Use a custom seed instead of a random one" + } + ], + "IncompatibleMods": [ + "TP" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "MR", + "Name": "Mirror", + "Description": "Flip objects on the chosen axes.", + "Type": "Conversion", + "Settings": [ + { + "Name": "reflection", + "Type": "string", + "Label": "Flipped axes", + "Description": "" + } + ], + "IncompatibleMods": [ + "HR" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "AL", + "Name": "Alternate", + "Description": "Don't use the same key twice in a row!", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [ + "SG", + "AT", + "CN", + "RX" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "SG", + "Name": "Single Tap", + "Description": "You must only use one key!", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [ + "AL", + "AT", + "CN", + "RX" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "AT", + "Name": "Autoplay", + "Description": "Watch a perfect automated play through the song.", + "Type": "Automation", + "Settings": [], + "IncompatibleMods": [ + "AL", + "SG", + "CN", + "RX", + "AP", + "SO", + "MG", + "RP", + "AS", + "TD" + ], + "RequiresConfiguration": false, + "UserPlayable": false, + "ValidForMultiplayer": false, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "CN", + "Name": "Cinema", + "Description": "Watch the video without visual distractions.", + "Type": "Automation", + "Settings": [], + "IncompatibleMods": [ + "NF", + "SD", + "PF", + "AC", + "AL", + "SG", + "AT", + "CN", + "RX", + "AP", + "SO", + "MG", + "RP", + "AS", + "TD" + ], + "RequiresConfiguration": false, + "UserPlayable": false, + "ValidForMultiplayer": false, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "RX", + "Name": "Relax", + "Description": "You don't need to click. Give your clicking/tapping fingers a break from the heat of things.", + "Type": "Automation", + "Settings": [], + "IncompatibleMods": [ + "AL", + "SG", + "AT", + "CN", + "AP", + "MG" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "AP", + "Name": "Autopilot", + "Description": "Automatic cursor movement - just follow the rhythm.", + "Type": "Automation", + "Settings": [], + "IncompatibleMods": [ + "AT", + "CN", + "RX", + "SO", + "MG", + "RP", + "TD" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "SO", + "Name": "Spun Out", + "Description": "Spinners will be automatically completed.", + "Type": "Automation", + "Settings": [], + "IncompatibleMods": [ + "TP", + "AT", + "CN", + "AP" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "TR", + "Name": "Transform", + "Description": "Everything rotates. EVERYTHING.", + "Type": "Fun", + "Settings": [], + "IncompatibleMods": [ + "WG", + "MG", + "RP", + "FR", + "DP" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "WG", + "Name": "Wiggle", + "Description": "They just won't stay still...", + "Type": "Fun", + "Settings": [ + { + "Name": "strength", + "Type": "number", + "Label": "Strength", + "Description": "Multiplier applied to the wiggling strength." + } + ], + "IncompatibleMods": [ + "TR", + "MG", + "RP", + "DP" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "SI", + "Name": "Spin In", + "Description": "Circles spin in. No approach circles.", + "Type": "Fun", + "Settings": [], + "IncompatibleMods": [ + "HD", + "GR", + "DF", + "TC", + "AD", + "DP" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "GR", + "Name": "Grow", + "Description": "Hit them at the right size!", + "Type": "Fun", + "Settings": [ + { + "Name": "start_scale", + "Type": "number", + "Label": "Starting Size", + "Description": "The initial size multiplier applied to all objects." + } + ], + "IncompatibleMods": [ + "SI", + "GR", + "DF", + "TC", + "AD", + "DP" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "DF", + "Name": "Deflate", + "Description": "Hit them at the right size!", + "Type": "Fun", + "Settings": [ + { + "Name": "start_scale", + "Type": "number", + "Label": "Starting Size", + "Description": "The initial size multiplier applied to all objects." + } + ], + "IncompatibleMods": [ + "SI", + "GR", + "DF", + "TC", + "AD", + "DP" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "WU", + "Name": "Wind Up", + "Description": "Can you keep up?", + "Type": "Fun", + "Settings": [ + { + "Name": "initial_rate", + "Type": "number", + "Label": "Initial rate", + "Description": "The starting speed of the track" + }, + { + "Name": "final_rate", + "Type": "number", + "Label": "Final rate", + "Description": "The final speed to ramp to" + }, + { + "Name": "adjust_pitch", + "Type": "boolean", + "Label": "Adjust pitch", + "Description": "Should pitch be adjusted with speed" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WD", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "WD", + "Name": "Wind Down", + "Description": "Sloooow doooown...", + "Type": "Fun", + "Settings": [ + { + "Name": "initial_rate", + "Type": "number", + "Label": "Initial rate", + "Description": "The starting speed of the track" + }, + { + "Name": "final_rate", + "Type": "number", + "Label": "Final rate", + "Description": "The final speed to ramp to" + }, + { + "Name": "adjust_pitch", + "Type": "boolean", + "Label": "Adjust pitch", + "Description": "Should pitch be adjusted with speed" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WU", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "TC", + "Name": "Traceable", + "Description": "Put your faith in the approach circles...", + "Type": "Fun", + "Settings": [], + "IncompatibleMods": [ + "HD", + "TP", + "SI", + "GR", + "DF", + "DP" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "BR", + "Name": "Barrel Roll", + "Description": "The whole playfield is on a wheel!", + "Type": "Fun", + "Settings": [ + { + "Name": "spin_speed", + "Type": "number", + "Label": "Roll speed", + "Description": "Rotations per minute" + }, + { + "Name": "direction", + "Type": "string", + "Label": "Direction", + "Description": "The direction of rotation" + } + ], + "IncompatibleMods": [ + "BU" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "AD", + "Name": "Approach Different", + "Description": "Never trust the approach circles...", + "Type": "Fun", + "Settings": [ + { + "Name": "scale", + "Type": "number", + "Label": "Initial size", + "Description": "Change the initial size of the approach circle, relative to hit circles." + }, + { + "Name": "style", + "Type": "string", + "Label": "Style", + "Description": "Change the animation style of the approach circles." + } + ], + "IncompatibleMods": [ + "HD", + "TP", + "SI", + "GR", + "DF", + "FR" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "MU", + "Name": "Muted", + "Description": "Can you still feel the rhythm without music?", + "Type": "Fun", + "Settings": [ + { + "Name": "inverse_muting", + "Type": "boolean", + "Label": "Start muted", + "Description": "Increase volume as combo builds." + }, + { + "Name": "enable_metronome", + "Type": "boolean", + "Label": "Enable metronome", + "Description": "Add a metronome beat to help you keep track of the rhythm." + }, + { + "Name": "mute_combo_count", + "Type": "number", + "Label": "Final volume at combo", + "Description": "The combo count at which point the track reaches its final volume." + }, + { + "Name": "affects_hit_sounds", + "Type": "boolean", + "Label": "Mute hit sounds", + "Description": "Hit sounds are also muted alongside the track." + } + ], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "NS", + "Name": "No Scope", + "Description": "Where's the cursor?", + "Type": "Fun", + "Settings": [ + { + "Name": "hidden_combo_count", + "Type": "number", + "Label": "Hidden at combo", + "Description": "The combo count at which the cursor becomes completely hidden" + } + ], + "IncompatibleMods": [ + "BM" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "MG", + "Name": "Magnetised", + "Description": "No need to chase the circles – your cursor is a magnet!", + "Type": "Fun", + "Settings": [ + { + "Name": "attraction_strength", + "Type": "number", + "Label": "Attraction strength", + "Description": "How strong the pull is." + } + ], + "IncompatibleMods": [ + "AT", + "CN", + "RX", + "AP", + "TR", + "WG", + "RP", + "BU", + "DP" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "RP", + "Name": "Repel", + "Description": "Hit objects run away!", + "Type": "Fun", + "Settings": [ + { + "Name": "repulsion_strength", + "Type": "number", + "Label": "Repulsion strength", + "Description": "How strong the repulsion is." + } + ], + "IncompatibleMods": [ + "AT", + "CN", + "AP", + "TR", + "WG", + "MG", + "BU", + "DP" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "AS", + "Name": "Adaptive Speed", + "Description": "Let track speed adapt to you.", + "Type": "Fun", + "Settings": [ + { + "Name": "initial_rate", + "Type": "number", + "Label": "Initial rate", + "Description": "The starting speed of the track" + }, + { + "Name": "adjust_pitch", + "Type": "boolean", + "Label": "Adjust pitch", + "Description": "Should pitch be adjusted with speed" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "AT", + "CN", + "WU", + "WD" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": false, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "FR", + "Name": "Freeze Frame", + "Description": "Burn the notes into your memory.", + "Type": "Fun", + "Settings": [], + "IncompatibleMods": [ + "TR", + "AD", + "DP" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "BU", + "Name": "Bubbles", + "Description": "Don't let their popping distract you!", + "Type": "Fun", + "Settings": [], + "IncompatibleMods": [ + "BR", + "MG", + "RP" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "SY", + "Name": "Synesthesia", + "Description": "Colours hit objects based on the rhythm.", + "Type": "Fun", + "Settings": [], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "DP", + "Name": "Depth", + "Description": "3D. Almost.", + "Type": "Fun", + "Settings": [ + { + "Name": "max_depth", + "Type": "number", + "Label": "Maximum depth", + "Description": "How far away objects appear." + }, + { + "Name": "show_approach_circles", + "Type": "boolean", + "Label": "Show Approach Circles", + "Description": "Whether approach circles should be visible." + } + ], + "IncompatibleMods": [ + "HD", + "TP", + "TR", + "WG", + "SI", + "GR", + "DF", + "TC", + "MG", + "RP", + "FR", + "DP" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "BM", + "Name": "Bloom", + "Description": "The cursor blooms into.. a larger cursor!", + "Type": "Fun", + "Settings": [ + { + "Name": "max_size_combo_count", + "Type": "number", + "Label": "Max size at combo", + "Description": "The combo count at which the cursor reaches its maximum size" + }, + { + "Name": "max_cursor_size", + "Type": "number", + "Label": "Final size multiplier", + "Description": "The multiplier applied to cursor size when combo reaches maximum" + } + ], + "IncompatibleMods": [ + "FL", + "NS", + "TD" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "TD", + "Name": "Touch Device", + "Description": "Automatically applied to plays on devices with a touchscreen.", + "Type": "System", + "Settings": [], + "IncompatibleMods": [ + "AT", + "CN", + "AP", + "BM" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": false, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": true + }, + { + "Acronym": "SV2", + "Name": "Score V2", + "Description": "Score set on earlier osu! versions with the V2 scoring algorithm active.", + "Type": "System", + "Settings": [], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": false, + "ValidForMultiplayer": false, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + } + ] + }, + { + "Name": "taiko", + "RulesetID": 1, + "Mods": [ + { + "Acronym": "EZ", + "Name": "Easy", + "Description": "Beats move slower, and less accuracy required!", + "Type": "DifficultyReduction", + "Settings": [], + "IncompatibleMods": [ + "HR", + "DA" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "NF", + "Name": "No Fail", + "Description": "You can't fail, no matter what.", + "Type": "DifficultyReduction", + "Settings": [], + "IncompatibleMods": [ + "SD", + "PF", + "AC", + "CN" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "HT", + "Name": "Half Time", + "Description": "Less zoom...", + "Type": "DifficultyReduction", + "Settings": [ + { + "Name": "speed_change", + "Type": "number", + "Label": "Speed decrease", + "Description": "The actual decrease to apply" + }, + { + "Name": "adjust_pitch", + "Type": "boolean", + "Label": "Adjust pitch", + "Description": "Should pitch be adjusted with speed" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WU", + "WD", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "DC", + "Name": "Daycore", + "Description": "Whoaaaaa...", + "Type": "DifficultyReduction", + "Settings": [ + { + "Name": "speed_change", + "Type": "number", + "Label": "Speed decrease", + "Description": "The actual decrease to apply" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WU", + "WD", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "SR", + "Name": "Simplified Rhythm", + "Description": "Simplify tricky rhythms!", + "Type": "DifficultyReduction", + "Settings": [ + { + "Name": "one_third_conversion", + "Type": "boolean", + "Label": "1/3 to 1/2 conversion", + "Description": "Converts 1/3 patterns to 1/2 rhythm." + }, + { + "Name": "one_sixth_conversion", + "Type": "boolean", + "Label": "1/6 to 1/4 conversion", + "Description": "Converts 1/6 patterns to 1/4 rhythm." + }, + { + "Name": "one_eighth_conversion", + "Type": "boolean", + "Label": "1/8 to 1/4 conversion", + "Description": "Converts 1/8 patterns to 1/4 rhythm." + } + ], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "HR", + "Name": "Hard Rock", + "Description": "Everything just got a bit harder...", + "Type": "DifficultyIncrease", + "Settings": [], + "IncompatibleMods": [ + "EZ", + "DA" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "SD", + "Name": "Sudden Death", + "Description": "Miss and fail.", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "restart", + "Type": "boolean", + "Label": "Restart on fail", + "Description": "Automatically restarts when failed." + } + ], + "IncompatibleMods": [ + "NF", + "PF", + "CN" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "PF", + "Name": "Perfect", + "Description": "SS or quit.", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "restart", + "Type": "boolean", + "Label": "Restart on fail", + "Description": "Automatically restarts when failed." + } + ], + "IncompatibleMods": [ + "NF", + "SD", + "AC", + "CN" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "DT", + "Name": "Double Time", + "Description": "Zoooooooooom...", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "speed_change", + "Type": "number", + "Label": "Speed increase", + "Description": "The actual increase to apply" + }, + { + "Name": "adjust_pitch", + "Type": "boolean", + "Label": "Adjust pitch", + "Description": "Should pitch be adjusted with speed" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WU", + "WD", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "NC", + "Name": "Nightcore", + "Description": "Uguuuuuuuu...", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "speed_change", + "Type": "number", + "Label": "Speed increase", + "Description": "The actual increase to apply" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WU", + "WD", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "HD", + "Name": "Hidden", + "Description": "Beats fade out before you hit them!", + "Type": "DifficultyIncrease", + "Settings": [], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "FL", + "Name": "Flashlight", + "Description": "Restricted view area.", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "size_multiplier", + "Type": "number", + "Label": "Flashlight size", + "Description": "Multiplier applied to the default flashlight size." + }, + { + "Name": "combo_based_size", + "Type": "boolean", + "Label": "Change size based on combo", + "Description": "Decrease the flashlight size as combo increases." + } + ], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "AC", + "Name": "Accuracy Challenge", + "Description": "Fail if your accuracy drops too low!", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "minimum_accuracy", + "Type": "number", + "Label": "Minimum accuracy", + "Description": "Trigger a failure if your accuracy goes below this value." + }, + { + "Name": "accuracy_judge_mode", + "Type": "string", + "Label": "Accuracy mode", + "Description": "The mode of accuracy that will trigger failure." + }, + { + "Name": "restart", + "Type": "boolean", + "Label": "Restart on fail", + "Description": "Automatically restarts when failed." + } + ], + "IncompatibleMods": [ + "NF", + "PF", + "CN" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "RD", + "Name": "Random", + "Description": "Shuffle around the colours!", + "Type": "Conversion", + "Settings": [ + { + "Name": "seed", + "Type": "number", + "Label": "Seed", + "Description": "Use a custom seed instead of a random one" + } + ], + "IncompatibleMods": [ + "SW" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "DA", + "Name": "Difficulty Adjust", + "Description": "Override a beatmap's difficulty settings.", + "Type": "Conversion", + "Settings": [ + { + "Name": "scroll_speed", + "Type": "number", + "Label": "Scroll Speed", + "Description": "Adjust a beatmap's set scroll speed" + }, + { + "Name": "drain_rate", + "Type": "number", + "Label": "HP Drain", + "Description": "Override a beatmap's set HP." + }, + { + "Name": "overall_difficulty", + "Type": "number", + "Label": "Accuracy", + "Description": "Override a beatmap's set OD." + }, + { + "Name": "extended_limits", + "Type": "boolean", + "Label": "Extended Limits", + "Description": "Adjust difficulty beyond sane limits." + } + ], + "IncompatibleMods": [ + "EZ", + "HR" + ], + "RequiresConfiguration": true, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "CL", + "Name": "Classic", + "Description": "Feeling nostalgic?", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "SW", + "Name": "Swap", + "Description": "Dons become kats, kats become dons", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [ + "RD" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "SG", + "Name": "Single Tap", + "Description": "One key for dons, one key for kats.", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [ + "AT", + "CN", + "RX" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "CS", + "Name": "Constant Speed", + "Description": "No more tricky speed changes!", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "AT", + "Name": "Autoplay", + "Description": "Watch a perfect automated play through the song.", + "Type": "Automation", + "Settings": [], + "IncompatibleMods": [ + "SG", + "CN", + "RX", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": false, + "ValidForMultiplayer": false, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "CN", + "Name": "Cinema", + "Description": "Watch the video without visual distractions.", + "Type": "Automation", + "Settings": [], + "IncompatibleMods": [ + "NF", + "SD", + "PF", + "AC", + "SG", + "AT", + "CN", + "RX", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": false, + "ValidForMultiplayer": false, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "RX", + "Name": "Relax", + "Description": "No need to remember which key is correct anymore!", + "Type": "Automation", + "Settings": [], + "IncompatibleMods": [ + "SG", + "AT", + "CN" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "WU", + "Name": "Wind Up", + "Description": "Can you keep up?", + "Type": "Fun", + "Settings": [ + { + "Name": "initial_rate", + "Type": "number", + "Label": "Initial rate", + "Description": "The starting speed of the track" + }, + { + "Name": "final_rate", + "Type": "number", + "Label": "Final rate", + "Description": "The final speed to ramp to" + }, + { + "Name": "adjust_pitch", + "Type": "boolean", + "Label": "Adjust pitch", + "Description": "Should pitch be adjusted with speed" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WD", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "WD", + "Name": "Wind Down", + "Description": "Sloooow doooown...", + "Type": "Fun", + "Settings": [ + { + "Name": "initial_rate", + "Type": "number", + "Label": "Initial rate", + "Description": "The starting speed of the track" + }, + { + "Name": "final_rate", + "Type": "number", + "Label": "Final rate", + "Description": "The final speed to ramp to" + }, + { + "Name": "adjust_pitch", + "Type": "boolean", + "Label": "Adjust pitch", + "Description": "Should pitch be adjusted with speed" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WU", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "MU", + "Name": "Muted", + "Description": "Can you still feel the rhythm without music?", + "Type": "Fun", + "Settings": [ + { + "Name": "inverse_muting", + "Type": "boolean", + "Label": "Start muted", + "Description": "Increase volume as combo builds." + }, + { + "Name": "enable_metronome", + "Type": "boolean", + "Label": "Enable metronome", + "Description": "Add a metronome beat to help you keep track of the rhythm." + }, + { + "Name": "mute_combo_count", + "Type": "number", + "Label": "Final volume at combo", + "Description": "The combo count at which point the track reaches its final volume." + }, + { + "Name": "affects_hit_sounds", + "Type": "boolean", + "Label": "Mute hit sounds", + "Description": "Hit sounds are also muted alongside the track." + } + ], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "AS", + "Name": "Adaptive Speed", + "Description": "Let track speed adapt to you.", + "Type": "Fun", + "Settings": [ + { + "Name": "initial_rate", + "Type": "number", + "Label": "Initial rate", + "Description": "The starting speed of the track" + }, + { + "Name": "adjust_pitch", + "Type": "boolean", + "Label": "Adjust pitch", + "Description": "Should pitch be adjusted with speed" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "AT", + "CN", + "WU", + "WD" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": false, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "SV2", + "Name": "Score V2", + "Description": "Score set on earlier osu! versions with the V2 scoring algorithm active.", + "Type": "System", + "Settings": [], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": false, + "ValidForMultiplayer": false, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + } + ] + }, + { + "Name": "fruits", + "RulesetID": 2, + "Mods": [ + { + "Acronym": "EZ", + "Name": "Easy", + "Description": "Larger fruits, more forgiving HP drain, less accuracy required, and extra lives!", + "Type": "DifficultyReduction", + "Settings": [ + { + "Name": "retries", + "Type": "number", + "Label": "Extra Lives", + "Description": "Number of extra lives" + } + ], + "IncompatibleMods": [ + "HR", + "AC", + "DA" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "NF", + "Name": "No Fail", + "Description": "You can't fail, no matter what.", + "Type": "DifficultyReduction", + "Settings": [], + "IncompatibleMods": [ + "SD", + "PF", + "AC", + "CN" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "HT", + "Name": "Half Time", + "Description": "Less zoom...", + "Type": "DifficultyReduction", + "Settings": [ + { + "Name": "speed_change", + "Type": "number", + "Label": "Speed decrease", + "Description": "The actual decrease to apply" + }, + { + "Name": "adjust_pitch", + "Type": "boolean", + "Label": "Adjust pitch", + "Description": "Should pitch be adjusted with speed" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WU", + "WD" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "DC", + "Name": "Daycore", + "Description": "Whoaaaaa...", + "Type": "DifficultyReduction", + "Settings": [ + { + "Name": "speed_change", + "Type": "number", + "Label": "Speed decrease", + "Description": "The actual decrease to apply" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WU", + "WD" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "HR", + "Name": "Hard Rock", + "Description": "Everything just got a bit harder...", + "Type": "DifficultyIncrease", + "Settings": [], + "IncompatibleMods": [ + "EZ", + "DA" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "SD", + "Name": "Sudden Death", + "Description": "Miss and fail.", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "restart", + "Type": "boolean", + "Label": "Restart on fail", + "Description": "Automatically restarts when failed." + } + ], + "IncompatibleMods": [ + "NF", + "PF", + "CN" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "PF", + "Name": "Perfect", + "Description": "SS or quit.", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "restart", + "Type": "boolean", + "Label": "Restart on fail", + "Description": "Automatically restarts when failed." + } + ], + "IncompatibleMods": [ + "NF", + "SD", + "AC", + "CN" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "DT", + "Name": "Double Time", + "Description": "Zoooooooooom...", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "speed_change", + "Type": "number", + "Label": "Speed increase", + "Description": "The actual increase to apply" + }, + { + "Name": "adjust_pitch", + "Type": "boolean", + "Label": "Adjust pitch", + "Description": "Should pitch be adjusted with speed" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WU", + "WD" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "NC", + "Name": "Nightcore", + "Description": "Uguuuuuuuu...", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "speed_change", + "Type": "number", + "Label": "Speed increase", + "Description": "The actual increase to apply" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WU", + "WD" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "HD", + "Name": "Hidden", + "Description": "Play with fading fruits.", + "Type": "DifficultyIncrease", + "Settings": [], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "FL", + "Name": "Flashlight", + "Description": "Restricted view area.", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "size_multiplier", + "Type": "number", + "Label": "Flashlight size", + "Description": "Multiplier applied to the default flashlight size." + }, + { + "Name": "combo_based_size", + "Type": "boolean", + "Label": "Change size based on combo", + "Description": "Decrease the flashlight size as combo increases." + } + ], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "AC", + "Name": "Accuracy Challenge", + "Description": "Fail if your accuracy drops too low!", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "minimum_accuracy", + "Type": "number", + "Label": "Minimum accuracy", + "Description": "Trigger a failure if your accuracy goes below this value." + }, + { + "Name": "accuracy_judge_mode", + "Type": "string", + "Label": "Accuracy mode", + "Description": "The mode of accuracy that will trigger failure." + }, + { + "Name": "restart", + "Type": "boolean", + "Label": "Restart on fail", + "Description": "Automatically restarts when failed." + } + ], + "IncompatibleMods": [ + "EZ", + "NF", + "PF", + "CN" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "DA", + "Name": "Difficulty Adjust", + "Description": "Override a beatmap's difficulty settings.", + "Type": "Conversion", + "Settings": [ + { + "Name": "circle_size", + "Type": "number", + "Label": "Circle Size", + "Description": "Override a beatmap's set CS." + }, + { + "Name": "approach_rate", + "Type": "number", + "Label": "Approach Rate", + "Description": "Override a beatmap's set AR." + }, + { + "Name": "hard_rock_offsets", + "Type": "boolean", + "Label": "Spicy Patterns", + "Description": "Adjust the patterns as if Hard Rock is enabled." + }, + { + "Name": "drain_rate", + "Type": "number", + "Label": "HP Drain", + "Description": "Override a beatmap's set HP." + }, + { + "Name": "overall_difficulty", + "Type": "number", + "Label": "Accuracy", + "Description": "Override a beatmap's set OD." + }, + { + "Name": "extended_limits", + "Type": "boolean", + "Label": "Extended Limits", + "Description": "Adjust difficulty beyond sane limits." + } + ], + "IncompatibleMods": [ + "EZ", + "HR" + ], + "RequiresConfiguration": true, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "CL", + "Name": "Classic", + "Description": "Feeling nostalgic?", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "MR", + "Name": "Mirror", + "Description": "Fruits are flipped horizontally.", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "AT", + "Name": "Autoplay", + "Description": "Watch a perfect automated play through the song.", + "Type": "Automation", + "Settings": [], + "IncompatibleMods": [ + "CN", + "RX" + ], + "RequiresConfiguration": false, + "UserPlayable": false, + "ValidForMultiplayer": false, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "CN", + "Name": "Cinema", + "Description": "Watch the video without visual distractions.", + "Type": "Automation", + "Settings": [], + "IncompatibleMods": [ + "NF", + "SD", + "PF", + "AC", + "AT", + "CN", + "RX" + ], + "RequiresConfiguration": false, + "UserPlayable": false, + "ValidForMultiplayer": false, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "RX", + "Name": "Relax", + "Description": "Use the mouse to control the catcher.", + "Type": "Automation", + "Settings": [], + "IncompatibleMods": [ + "AT", + "CN" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "WU", + "Name": "Wind Up", + "Description": "Can you keep up?", + "Type": "Fun", + "Settings": [ + { + "Name": "initial_rate", + "Type": "number", + "Label": "Initial rate", + "Description": "The starting speed of the track" + }, + { + "Name": "final_rate", + "Type": "number", + "Label": "Final rate", + "Description": "The final speed to ramp to" + }, + { + "Name": "adjust_pitch", + "Type": "boolean", + "Label": "Adjust pitch", + "Description": "Should pitch be adjusted with speed" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WD" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "WD", + "Name": "Wind Down", + "Description": "Sloooow doooown...", + "Type": "Fun", + "Settings": [ + { + "Name": "initial_rate", + "Type": "number", + "Label": "Initial rate", + "Description": "The starting speed of the track" + }, + { + "Name": "final_rate", + "Type": "number", + "Label": "Final rate", + "Description": "The final speed to ramp to" + }, + { + "Name": "adjust_pitch", + "Type": "boolean", + "Label": "Adjust pitch", + "Description": "Should pitch be adjusted with speed" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WU" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "FF", + "Name": "Floating Fruits", + "Description": "The fruits are... floating?", + "Type": "Fun", + "Settings": [], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "MU", + "Name": "Muted", + "Description": "Can you still feel the rhythm without music?", + "Type": "Fun", + "Settings": [ + { + "Name": "inverse_muting", + "Type": "boolean", + "Label": "Start muted", + "Description": "Increase volume as combo builds." + }, + { + "Name": "enable_metronome", + "Type": "boolean", + "Label": "Enable metronome", + "Description": "Add a metronome beat to help you keep track of the rhythm." + }, + { + "Name": "mute_combo_count", + "Type": "number", + "Label": "Final volume at combo", + "Description": "The combo count at which point the track reaches its final volume." + }, + { + "Name": "affects_hit_sounds", + "Type": "boolean", + "Label": "Mute hit sounds", + "Description": "Hit sounds are also muted alongside the track." + } + ], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "NS", + "Name": "No Scope", + "Description": "Where's the catcher?", + "Type": "Fun", + "Settings": [ + { + "Name": "hidden_combo_count", + "Type": "number", + "Label": "Hidden at combo", + "Description": "The combo count at which the cursor becomes completely hidden" + } + ], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "SV2", + "Name": "Score V2", + "Description": "Score set on earlier osu! versions with the V2 scoring algorithm active.", + "Type": "System", + "Settings": [], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": false, + "ValidForMultiplayer": false, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + } + ] + }, + { + "Name": "mania", + "RulesetID": 3, + "Mods": [ + { + "Acronym": "EZ", + "Name": "Easy", + "Description": "More forgiving HP drain, less accuracy required, and extra lives!", + "Type": "DifficultyReduction", + "Settings": [ + { + "Name": "retries", + "Type": "number", + "Label": "Extra Lives", + "Description": "Number of extra lives" + } + ], + "IncompatibleMods": [ + "HR", + "AC", + "DA" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "NF", + "Name": "No Fail", + "Description": "You can't fail, no matter what.", + "Type": "DifficultyReduction", + "Settings": [], + "IncompatibleMods": [ + "SD", + "PF", + "AC", + "CN" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "HT", + "Name": "Half Time", + "Description": "Less zoom...", + "Type": "DifficultyReduction", + "Settings": [ + { + "Name": "speed_change", + "Type": "number", + "Label": "Speed decrease", + "Description": "The actual decrease to apply" + }, + { + "Name": "adjust_pitch", + "Type": "boolean", + "Label": "Adjust pitch", + "Description": "Should pitch be adjusted with speed" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WU", + "WD", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "DC", + "Name": "Daycore", + "Description": "Whoaaaaa...", + "Type": "DifficultyReduction", + "Settings": [ + { + "Name": "speed_change", + "Type": "number", + "Label": "Speed decrease", + "Description": "The actual decrease to apply" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WU", + "WD", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "NR", + "Name": "No Release", + "Description": "No more timing the end of hold notes.", + "Type": "DifficultyReduction", + "Settings": [], + "IncompatibleMods": [ + "HO" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "HR", + "Name": "Hard Rock", + "Description": "Everything just got a bit harder...", + "Type": "DifficultyIncrease", + "Settings": [], + "IncompatibleMods": [ + "EZ", + "DA" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "SD", + "Name": "Sudden Death", + "Description": "Miss and fail.", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "restart", + "Type": "boolean", + "Label": "Restart on fail", + "Description": "Automatically restarts when failed." + } + ], + "IncompatibleMods": [ + "NF", + "PF", + "CN" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "PF", + "Name": "Perfect", + "Description": "SS or quit.", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "require_perfect_hits", + "Type": "boolean", + "Label": "Require perfect hits", + "Description": "" + }, + { + "Name": "restart", + "Type": "boolean", + "Label": "Restart on fail", + "Description": "Automatically restarts when failed." + } + ], + "IncompatibleMods": [ + "NF", + "SD", + "AC", + "CN" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "DT", + "Name": "Double Time", + "Description": "Zoooooooooom...", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "speed_change", + "Type": "number", + "Label": "Speed increase", + "Description": "The actual increase to apply" + }, + { + "Name": "adjust_pitch", + "Type": "boolean", + "Label": "Adjust pitch", + "Description": "Should pitch be adjusted with speed" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WU", + "WD", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "NC", + "Name": "Nightcore", + "Description": "Uguuuuuuuu...", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "speed_change", + "Type": "number", + "Label": "Speed increase", + "Description": "The actual increase to apply" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WU", + "WD", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "FI", + "Name": "Fade In", + "Description": "Keys appear out of nowhere!", + "Type": "DifficultyIncrease", + "Settings": [], + "IncompatibleMods": [ + "FI", + "HD", + "CO", + "FL" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "HD", + "Name": "Hidden", + "Description": "Keys fade out before you hit them!", + "Type": "DifficultyIncrease", + "Settings": [], + "IncompatibleMods": [ + "FI", + "CO", + "FL" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "CO", + "Name": "Cover", + "Description": "Decrease the playfield's viewing area.", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "coverage", + "Type": "number", + "Label": "Coverage", + "Description": "The proportion of playfield height that notes will be hidden for." + }, + { + "Name": "direction", + "Type": "string", + "Label": "Direction", + "Description": "The direction on which the cover is applied" + } + ], + "IncompatibleMods": [ + "FI", + "HD", + "FL" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "FL", + "Name": "Flashlight", + "Description": "Restricted view area.", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "size_multiplier", + "Type": "number", + "Label": "Flashlight size", + "Description": "Multiplier applied to the default flashlight size." + }, + { + "Name": "combo_based_size", + "Type": "boolean", + "Label": "Change size based on combo", + "Description": "Decrease the flashlight size as combo increases." + } + ], + "IncompatibleMods": [ + "FI", + "HD", + "CO" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "AC", + "Name": "Accuracy Challenge", + "Description": "Fail if your accuracy drops too low!", + "Type": "DifficultyIncrease", + "Settings": [ + { + "Name": "minimum_accuracy", + "Type": "number", + "Label": "Minimum accuracy", + "Description": "Trigger a failure if your accuracy goes below this value." + }, + { + "Name": "accuracy_judge_mode", + "Type": "string", + "Label": "Accuracy mode", + "Description": "The mode of accuracy that will trigger failure." + }, + { + "Name": "restart", + "Type": "boolean", + "Label": "Restart on fail", + "Description": "Automatically restarts when failed." + } + ], + "IncompatibleMods": [ + "EZ", + "NF", + "PF", + "CN" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "RD", + "Name": "Random", + "Description": "Shuffle around the keys!", + "Type": "Conversion", + "Settings": [ + { + "Name": "seed", + "Type": "number", + "Label": "Seed", + "Description": "Use a custom seed instead of a random one" + } + ], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "DS", + "Name": "Dual Stages", + "Description": "Double the stages, double the fun!", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "MR", + "Name": "Mirror", + "Description": "Notes are flipped horizontally.", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "DA", + "Name": "Difficulty Adjust", + "Description": "Override a beatmap's difficulty settings.", + "Type": "Conversion", + "Settings": [ + { + "Name": "drain_rate", + "Type": "number", + "Label": "HP Drain", + "Description": "Override a beatmap's set HP." + }, + { + "Name": "overall_difficulty", + "Type": "number", + "Label": "Accuracy", + "Description": "Override a beatmap's set OD." + }, + { + "Name": "extended_limits", + "Type": "boolean", + "Label": "Extended Limits", + "Description": "Adjust difficulty beyond sane limits." + } + ], + "IncompatibleMods": [ + "EZ", + "HR" + ], + "RequiresConfiguration": true, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "CL", + "Name": "Classic", + "Description": "Feeling nostalgic?", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "IN", + "Name": "Invert", + "Description": "Hold the keys. To the beat.", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [ + "HO" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "CS", + "Name": "Constant Speed", + "Description": "No more tricky speed changes!", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "HO", + "Name": "Hold Off", + "Description": "Replaces all hold notes with normal notes.", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [ + "NR", + "IN" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "1K", + "Name": "One Key", + "Description": "Play with one key.", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [ + "2K", + "3K", + "4K", + "5K", + "6K", + "7K", + "8K", + "9K", + "10K" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "2K", + "Name": "Two Keys", + "Description": "Play with two keys.", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [ + "1K", + "3K", + "4K", + "5K", + "6K", + "7K", + "8K", + "9K", + "10K" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "3K", + "Name": "Three Keys", + "Description": "Play with three keys.", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [ + "1K", + "2K", + "4K", + "5K", + "6K", + "7K", + "8K", + "9K", + "10K" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "4K", + "Name": "Four Keys", + "Description": "Play with four keys.", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [ + "1K", + "2K", + "3K", + "5K", + "6K", + "7K", + "8K", + "9K", + "10K" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "5K", + "Name": "Five Keys", + "Description": "Play with five keys.", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [ + "1K", + "2K", + "3K", + "4K", + "6K", + "7K", + "8K", + "9K", + "10K" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "6K", + "Name": "Six Keys", + "Description": "Play with six keys.", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [ + "1K", + "2K", + "3K", + "4K", + "5K", + "7K", + "8K", + "9K", + "10K" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "7K", + "Name": "Seven Keys", + "Description": "Play with seven keys.", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [ + "1K", + "2K", + "3K", + "4K", + "5K", + "6K", + "8K", + "9K", + "10K" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "8K", + "Name": "Eight Keys", + "Description": "Play with eight keys.", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [ + "1K", + "2K", + "3K", + "4K", + "5K", + "6K", + "7K", + "9K", + "10K" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "9K", + "Name": "Nine Keys", + "Description": "Play with nine keys.", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [ + "1K", + "2K", + "3K", + "4K", + "5K", + "6K", + "7K", + "8K", + "10K" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "10K", + "Name": "Ten Keys", + "Description": "Play with ten keys.", + "Type": "Conversion", + "Settings": [], + "IncompatibleMods": [ + "1K", + "2K", + "3K", + "4K", + "5K", + "6K", + "7K", + "8K", + "9K" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "AT", + "Name": "Autoplay", + "Description": "Watch a perfect automated play through the song.", + "Type": "Automation", + "Settings": [], + "IncompatibleMods": [ + "CN", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": false, + "ValidForMultiplayer": false, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "CN", + "Name": "Cinema", + "Description": "Watch the video without visual distractions.", + "Type": "Automation", + "Settings": [], + "IncompatibleMods": [ + "NF", + "SD", + "PF", + "AC", + "AT", + "CN", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": false, + "ValidForMultiplayer": false, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "WU", + "Name": "Wind Up", + "Description": "Can you keep up?", + "Type": "Fun", + "Settings": [ + { + "Name": "initial_rate", + "Type": "number", + "Label": "Initial rate", + "Description": "The starting speed of the track" + }, + { + "Name": "final_rate", + "Type": "number", + "Label": "Final rate", + "Description": "The final speed to ramp to" + }, + { + "Name": "adjust_pitch", + "Type": "boolean", + "Label": "Adjust pitch", + "Description": "Should pitch be adjusted with speed" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WD", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "WD", + "Name": "Wind Down", + "Description": "Sloooow doooown...", + "Type": "Fun", + "Settings": [ + { + "Name": "initial_rate", + "Type": "number", + "Label": "Initial rate", + "Description": "The starting speed of the track" + }, + { + "Name": "final_rate", + "Type": "number", + "Label": "Final rate", + "Description": "The final speed to ramp to" + }, + { + "Name": "adjust_pitch", + "Type": "boolean", + "Label": "Adjust pitch", + "Description": "Should pitch be adjusted with speed" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "WU", + "AS" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "MU", + "Name": "Muted", + "Description": "Can you still feel the rhythm without music?", + "Type": "Fun", + "Settings": [ + { + "Name": "inverse_muting", + "Type": "boolean", + "Label": "Start muted", + "Description": "Increase volume as combo builds." + }, + { + "Name": "enable_metronome", + "Type": "boolean", + "Label": "Enable metronome", + "Description": "Add a metronome beat to help you keep track of the rhythm." + }, + { + "Name": "mute_combo_count", + "Type": "number", + "Label": "Final volume at combo", + "Description": "The combo count at which point the track reaches its final volume." + }, + { + "Name": "affects_hit_sounds", + "Type": "boolean", + "Label": "Mute hit sounds", + "Description": "Hit sounds are also muted alongside the track." + } + ], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": true, + "ValidForFreestyleAsRequiredMod": true, + "ValidForMultiplayerAsFreeMod": true, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "AS", + "Name": "Adaptive Speed", + "Description": "Let track speed adapt to you.", + "Type": "Fun", + "Settings": [ + { + "Name": "initial_rate", + "Type": "number", + "Label": "Initial rate", + "Description": "The starting speed of the track" + }, + { + "Name": "adjust_pitch", + "Type": "boolean", + "Label": "Adjust pitch", + "Description": "Should pitch be adjusted with speed" + } + ], + "IncompatibleMods": [ + "HT", + "DC", + "DT", + "NC", + "AT", + "CN", + "WU", + "WD" + ], + "RequiresConfiguration": false, + "UserPlayable": true, + "ValidForMultiplayer": false, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + }, + { + "Acronym": "SV2", + "Name": "Score V2", + "Description": "Score set on earlier osu! versions with the V2 scoring algorithm active.", + "Type": "System", + "Settings": [], + "IncompatibleMods": [], + "RequiresConfiguration": false, + "UserPlayable": false, + "ValidForMultiplayer": false, + "ValidForFreestyleAsRequiredMod": false, + "ValidForMultiplayerAsFreeMod": false, + "AlwaysValidForSubmission": false + } + ] + } +]