Files
bancho.py/app/constants/mods.py
2025-04-04 21:30:31 +09:00

297 lines
7.6 KiB
Python

from __future__ import annotations
import functools
from enum import IntFlag
from enum import unique
from app.utils import escape_enum
from app.utils import pymysql_encode
@unique
@pymysql_encode(escape_enum)
class Mods(IntFlag):
NOMOD = 0
NOFAIL = 1 << 0
EASY = 1 << 1
TOUCHSCREEN = 1 << 2 # old: 'NOVIDEO'
HIDDEN = 1 << 3
HARDROCK = 1 << 4
SUDDENDEATH = 1 << 5
DOUBLETIME = 1 << 6
RELAX = 1 << 7
HALFTIME = 1 << 8
NIGHTCORE = 1 << 9
FLASHLIGHT = 1 << 10
AUTOPLAY = 1 << 11
SPUNOUT = 1 << 12
AUTOPILOT = 1 << 13
PERFECT = 1 << 14
KEY4 = 1 << 15
KEY5 = 1 << 16
KEY6 = 1 << 17
KEY7 = 1 << 18
KEY8 = 1 << 19
FADEIN = 1 << 20
RANDOM = 1 << 21
CINEMA = 1 << 22
TARGET = 1 << 23
KEY9 = 1 << 24
KEYCOOP = 1 << 25
KEY1 = 1 << 26
KEY3 = 1 << 27
KEY2 = 1 << 28
SCOREV2 = 1 << 29
MIRROR = 1 << 30
@functools.cache
def __repr__(self) -> str:
if self.value == Mods.NOMOD:
return "NM"
mod_str = []
_dict = mod2modstr_dict # global
for mod in Mods:
if self.value & mod:
mod_str.append(_dict[mod])
return "".join(mod_str)
def filter_invalid_combos(self, mode_vn: int) -> Mods:
"""Remove any invalid mod combinations."""
# 1. mode-inspecific mod conflictions
_dtnc = self & (Mods.DOUBLETIME | Mods.NIGHTCORE)
if _dtnc == (Mods.DOUBLETIME | Mods.NIGHTCORE):
self &= ~Mods.DOUBLETIME # DTNC
elif _dtnc and self & Mods.HALFTIME:
self &= ~Mods.HALFTIME # (DT|NC)HT
if self & Mods.EASY and self & Mods.HARDROCK:
self &= ~Mods.HARDROCK # EZHR
if self & (Mods.NOFAIL | Mods.RELAX | Mods.AUTOPILOT):
if self & Mods.SUDDENDEATH:
self &= ~Mods.SUDDENDEATH # (NF|RX|AP)SD
if self & Mods.PERFECT:
self &= ~Mods.PERFECT # (NF|RX|AP)PF
if self & (Mods.RELAX | Mods.AUTOPILOT):
if self & Mods.NOFAIL:
self &= ~Mods.NOFAIL # (RX|AP)NF
if self & Mods.PERFECT and self & Mods.SUDDENDEATH:
self &= ~Mods.SUDDENDEATH # PFSD
# 2. remove mode-unique mods from incorrect gamemodes
if mode_vn != 0: # osu! specific
self &= ~OSU_SPECIFIC_MODS
# ctb & taiko have no unique mods
if mode_vn != 3: # mania specific
self &= ~MANIA_SPECIFIC_MODS
# 3. mode-specific mod conflictions
if mode_vn == 0:
if self & Mods.AUTOPILOT:
if self & (Mods.SPUNOUT | Mods.RELAX):
self &= ~Mods.AUTOPILOT # (SO|RX)AP
if mode_vn == 3:
self &= ~Mods.RELAX # rx is std/taiko/ctb common
if self & Mods.HIDDEN and self & Mods.FADEIN:
self &= ~Mods.FADEIN # HDFI
# 4 remove multiple keymods
keymods_used = self & KEY_MODS
if bin(keymods_used).count("1") > 1:
# keep only the first
first_keymod = None
for mod in KEY_MODS:
if keymods_used & mod:
first_keymod = mod
break
assert first_keymod is not None
# remove all but the first keymod.
self &= ~(keymods_used & ~first_keymod)
return self
@classmethod
@functools.lru_cache(maxsize=64)
def from_modstr(cls, s: str) -> Mods:
# from fmt: `HDDTRX`
mods = cls.NOMOD
_dict = modstr2mod_dict # global
# split into 2 character chunks
mod_strs = [s[idx : idx + 2].upper() for idx in range(0, len(s), 2)]
# find matching mods
for mod in mod_strs:
if mod not in _dict:
continue
mods |= _dict[mod]
return mods
@classmethod
@functools.lru_cache(maxsize=64)
def from_np(cls, s: str, mode_vn: int) -> Mods:
mods = cls.NOMOD
_dict = npstr2mod_dict # global
for mod in s.split(" "):
if mod not in _dict:
continue
mods |= _dict[mod]
# NOTE: for fetching from /np, we automatically
# call cls.filter_invalid_combos as we assume
# the input string is from user input.
return mods.filter_invalid_combos(mode_vn)
modstr2mod_dict = {
"NF": Mods.NOFAIL,
"EZ": Mods.EASY,
"TD": Mods.TOUCHSCREEN,
"HD": Mods.HIDDEN,
"HR": Mods.HARDROCK,
"SD": Mods.SUDDENDEATH,
"DT": Mods.DOUBLETIME,
"RX": Mods.RELAX,
"HT": Mods.HALFTIME,
"NC": Mods.NIGHTCORE,
"FL": Mods.FLASHLIGHT,
"AU": Mods.AUTOPLAY,
"SO": Mods.SPUNOUT,
"AP": Mods.AUTOPILOT,
"PF": Mods.PERFECT,
"FI": Mods.FADEIN,
"RN": Mods.RANDOM,
"CN": Mods.CINEMA,
"TP": Mods.TARGET,
"V2": Mods.SCOREV2,
"MR": Mods.MIRROR,
"1K": Mods.KEY1,
"2K": Mods.KEY2,
"3K": Mods.KEY3,
"4K": Mods.KEY4,
"5K": Mods.KEY5,
"6K": Mods.KEY6,
"7K": Mods.KEY7,
"8K": Mods.KEY8,
"9K": Mods.KEY9,
"CO": Mods.KEYCOOP,
}
npstr2mod_dict = {
"-NoFail": Mods.NOFAIL,
"-Easy": Mods.EASY,
"+Hidden": Mods.HIDDEN,
"+HardRock": Mods.HARDROCK,
"+SuddenDeath": Mods.SUDDENDEATH,
"+DoubleTime": Mods.DOUBLETIME,
"~Relax~": Mods.RELAX,
"-HalfTime": Mods.HALFTIME,
"+Nightcore": Mods.NIGHTCORE,
"+Flashlight": Mods.FLASHLIGHT,
"|Autoplay|": Mods.AUTOPLAY,
"-SpunOut": Mods.SPUNOUT,
"~Autopilot~": Mods.AUTOPILOT,
"+Perfect": Mods.PERFECT,
"|Cinema|": Mods.CINEMA,
"~Target~": Mods.TARGET,
# perhaps could modify regex
# to only allow these once,
# and only at the end of str?
"|1K|": Mods.KEY1,
"|2K|": Mods.KEY2,
"|3K|": Mods.KEY3,
"|4K|": Mods.KEY4,
"|5K|": Mods.KEY5,
"|6K|": Mods.KEY6,
"|7K|": Mods.KEY7,
"|8K|": Mods.KEY8,
"|9K|": Mods.KEY9,
# XXX: kinda mood that there's no way
# to tell K1-K4 co-op from /np, but
# scores won't submit or anything, so
# it's not ultimately a problem.
"|10K|": Mods.KEY5 | Mods.KEYCOOP,
"|12K|": Mods.KEY6 | Mods.KEYCOOP,
"|14K|": Mods.KEY7 | Mods.KEYCOOP,
"|16K|": Mods.KEY8 | Mods.KEYCOOP,
"|18K|": Mods.KEY9 | Mods.KEYCOOP,
}
mod2modstr_dict = {
Mods.NOFAIL: "NF",
Mods.EASY: "EZ",
Mods.TOUCHSCREEN: "TD",
Mods.HIDDEN: "HD",
Mods.HARDROCK: "HR",
Mods.SUDDENDEATH: "SD",
Mods.DOUBLETIME: "DT",
Mods.RELAX: "RX",
Mods.HALFTIME: "HT",
Mods.NIGHTCORE: "NC",
Mods.FLASHLIGHT: "FL",
Mods.AUTOPLAY: "AU",
Mods.SPUNOUT: "SO",
Mods.AUTOPILOT: "AP",
Mods.PERFECT: "PF",
Mods.FADEIN: "FI",
Mods.RANDOM: "RN",
Mods.CINEMA: "CN",
Mods.TARGET: "TP",
Mods.SCOREV2: "V2",
Mods.MIRROR: "MR",
Mods.KEY1: "1K",
Mods.KEY2: "2K",
Mods.KEY3: "3K",
Mods.KEY4: "4K",
Mods.KEY5: "5K",
Mods.KEY6: "6K",
Mods.KEY7: "7K",
Mods.KEY8: "8K",
Mods.KEY9: "9K",
Mods.KEYCOOP: "CO",
}
KEY_MODS = (
Mods.KEY1
| Mods.KEY2
| Mods.KEY3
| Mods.KEY4
| Mods.KEY5
| Mods.KEY6
| Mods.KEY7
| Mods.KEY8
| Mods.KEY9
)
# FREE_MOD_ALLOWED = (
# Mods.NOFAIL | Mods.EASY | Mods.HIDDEN | Mods.HARDROCK |
# Mods.SUDDENDEATH | Mods.FLASHLIGHT | Mods.FADEIN |
# Mods.RELAX | Mods.AUTOPILOT | Mods.SPUNOUT | KEY_MODS
# )
SCORE_INCREASE_MODS = (
Mods.HIDDEN | Mods.HARDROCK | Mods.FADEIN | Mods.DOUBLETIME | Mods.FLASHLIGHT
)
SPEED_CHANGING_MODS = Mods.DOUBLETIME | Mods.NIGHTCORE | Mods.HALFTIME
OSU_SPECIFIC_MODS = Mods.AUTOPILOT | Mods.SPUNOUT | Mods.TARGET
# taiko & catch have no specific mods
MANIA_SPECIFIC_MODS = Mods.MIRROR | Mods.RANDOM | Mods.FADEIN | KEY_MODS