mirror of
https://github.com/nihilvux/bancho.py.git
synced 2025-09-16 10:38:39 -07:00
297 lines
7.6 KiB
Python
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
|