Browse Source

Automatically unrank approved, qualified, loved maps above a certain pp threshold

tags/v1.18.0
Giuseppe Guerra 1 year ago
parent
commit
fbfa3364d7
7 changed files with 216 additions and 33 deletions
  1. +5
    -5
      constants/rankedStatuses.py
  2. +77
    -0
      helpers/aqlHelper.py
  3. +11
    -0
      lets.py
  4. +82
    -13
      objects/beatmap.pyx
  5. +3
    -1
      objects/glob.py
  6. +27
    -14
      objects/score.pyx
  7. +11
    -0
      pp/__init__.py

+ 5
- 5
constants/rankedStatuses.py View File

@@ -1,8 +1,8 @@
UNKNOWN = -2
NOT_SUBMITTED = -1
PENDING = 0
PENDING = 0 # No leaderboard
NEED_UPDATE = 1
RANKED = 2
APPROVED = 3
QUALIFIED = 4
LOVED = 5
RANKED = 2 # With leaderboard.
APPROVED = 3 # With leaderboard. Different icon. No alert.
QUALIFIED = 4 # With leaderboard. Different icon. Alert.
LOVED = 5 # With leaderboard. Different icon. Alert.

+ 77
- 0
helpers/aqlHelper.py View File

@@ -0,0 +1,77 @@
from common.constants import gameModes
from objects import glob


class AqlThresholds:
"""
A class representing the AQL thresholds configuration.
"""

def __init__(self):
"""
Initializes a new AQL thresholds configuration.
"""
self._thresholds = {}

def reload(self):
"""
Reloads the AQL thresholds configuration from DB

:return:
"""
self._thresholds = {}
for x in glob.db.fetchAll(
"SELECT `name`, value_string FROM system_settings WHERE `name` LIKE 'aql\_threshold\_%%'"
):
parts = x["name"].split("aql_threshold_")
if len(parts) < 1:
continue
m = gameModes.getGameModeFromDB(parts[1])
if m is None:
continue
try:
self._thresholds[m] = float(x["value_string"])
except ValueError:
continue
if not all(x in self._thresholds for x in range(gameModes.STD, gameModes.MANIA)):
raise RuntimeError("Invalid AQL thresholds. Please check your system_settings table.")

def __getitem__(self, item):
"""
Magic method that makes it possible to use an AqlThresholds object as a dictionary:
```
>>> glob.aqlThresholds[gameModes.STD]
<<< 1333.77
```

:param item:
:return:
"""
return self._thresholds[item]

def __iter__(self):
"""
Magic method that makes it possible to use iterate over an AqlThresholds object:
```
>>> tuple(gameModes.getGameModeForDB(x) for x in glob.aqlThresholds)
<<< ('std', 'taiko', 'ctb', 'mania')
```

:return:
"""
return iter(self._thresholds.keys())

def __contains__(self, item):
"""
Magic method that makes it possible to use the "in" operator on an AqlThresholds object:
```
>>> gameModes.STD in glob.aqlThresholds
<<< True
>>> "not_a_game_mode" in glob.aqlThresholds
<<< False
```

:param item:
:return:
"""
return item in self._thresholds

+ 11
- 0
lets.py View File

@@ -199,6 +199,16 @@ if __name__ == "__main__":
glob.redis.set("lets:achievements_version", glob.ACHIEVEMENTS_VERSION)
consoleHelper.printColored("Achievements version is {}".format(glob.ACHIEVEMENTS_VERSION), bcolors.YELLOW)

# Load AQL thresholds
consoleHelper.printNoNl("Loading AQL thresholds... ")
try:
glob.aqlThresholds.reload()
except Exception as e:
consoleHelper.printError()
consoleHelper.printColored("[!] {}".format(e), bcolors.RED,)
sys.exit()
consoleHelper.printDone()

# Discord
if generalUtils.stringToBool(glob.conf.config["discord"]["enable"]):
glob.schiavo = schiavo.schiavo(glob.conf.config["discord"]["boturl"], "**lets**")
@@ -241,6 +251,7 @@ if __name__ == "__main__":
# Connect to pubsub channels
pubSub.listener(glob.redis, {
"lets:beatmap_updates": beatmapUpdateHandler.handler(),
"lets:reload_aql": lambda x: x == b"reload" and glob.aqlThresholds.reload(),
}).start()

# Server start message and console output


+ 82
- 13
objects/beatmap.pyx View File

@@ -1,5 +1,6 @@
import time

from common.constants import gameModes
from common.log import logUtils as log
from constants import rankedStatuses
from helpers import osuapiHelper
@@ -7,9 +8,9 @@ from objects import glob


class beatmap:
__slots__ = ["songName", "fileMD5", "rankedStatus", "rankedStatusFrozen", "beatmapID", "beatmapSetID", "offset",
__slots__ = ("songName", "fileMD5", "rankedStatus", "rankedStatusFrozen", "beatmapID", "beatmapSetID", "offset",
"rating", "starsStd", "starsTaiko", "starsCtb", "starsMania", "AR", "OD", "maxCombo", "hitLength",
"bpm", "playcount" ,"passcount", "refresh"]
"bpm", "playcount" ,"passcount", "refresh", "disablePP")

def __init__(self, md5 = None, beatmapSetID = None, gameMode = 0, refresh=False):
"""
@@ -27,7 +28,7 @@ class beatmap:
self.offset = 0 # Won't implement
self.rating = 10.0 # Won't implement

self.starsStd = 0.0 # stars for converted
self.starsStd = 0.0 # stars for converted
self.starsTaiko = 0.0 # stars for converted
self.starsCtb = 0.0 # stars for converted
self.starsMania = 0.0 # stars for converted
@@ -36,6 +37,7 @@ class beatmap:
self.maxCombo = 0
self.hitLength = 0
self.bpm = 0
self.disablePP = False

# Statistics for ranking panel
self.playcount = 0
@@ -51,22 +53,61 @@ class beatmap:
Add current beatmap data in db if not in yet
"""
# Make sure the beatmap is not already in db
bdata = glob.db.fetch("SELECT id, ranked_status_freezed, ranked FROM beatmaps WHERE beatmap_md5 = %s OR beatmap_id = %s LIMIT 1", [self.fileMD5, self.beatmapID])
bdata = glob.db.fetch(
"SELECT id, ranked_status_freezed, ranked FROM beatmaps WHERE beatmap_md5 = %s OR beatmap_id = %s LIMIT 1",
(self.fileMD5, self.beatmapID)
)
if bdata is not None:
# This beatmap is already in db, remove old record
# Get current frozen status
frozen = bdata["ranked_status_freezed"]
if frozen == 1:
if frozen:
self.rankedStatus = bdata["ranked"]
log.debug("Deleting old beatmap data ({})".format(bdata["id"]))
glob.db.execute("DELETE FROM beatmaps WHERE id = %s LIMIT 1", [bdata["id"]])
else:
# Unfreeze beatmap status
frozen = 0
frozen = False

# Unrank broken approved/qualified/loved maps
if self.rankedStatus >= rankedStatuses.APPROVED:
from objects.score import PerfectScoreFactory
# Calculate PP for every game mode
log.debug("Caching A/Q/L map ({}). Checking if it's broken.".format(self.fileMD5))

# Calculate pp for every game mode
broken = False
for gameMode in (
range(gameModes.STD, gameModes.MANIA) if not self.is_mode_specific
else (self.specific_game_mode,)
):
log.debug("Calculating A/Q/L pp for beatmap {}, mode {}".format(self.fileMD5, gameMode))
s = PerfectScoreFactory.create(self, game_mode=gameMode)
s.calculatePP(self)
if s.pp == 0:
log.warning("Got 0.0pp while checking A/Q/L pp for beatmap {}".format(self.fileMD5))

if s.pp >= glob.aqlThresholds[gameMode]:
# More pp than the threshold
broken = True
break

# This game mode is fine, try the next one
s.pp = 0.

if broken:
# dont()
# TODO: set pp = 0 to old scores
self.disablePP = True
log.warning("Disabling PP on broken A/Q/L map {} (pp={})".format(self.fileMD5, s.pp))

# Add new beatmap data
log.debug("Saving beatmap data in db...")
glob.db.execute("INSERT INTO `beatmaps` (`id`, `beatmap_id`, `beatmapset_id`, `beatmap_md5`, `song_name`, `ar`, `od`, `difficulty_std`, `difficulty_taiko`, `difficulty_ctb`, `difficulty_mania`, `max_combo`, `hit_length`, `bpm`, `ranked`, `latest_update`, `ranked_status_freezed`) VALUES (NULL, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);", [
glob.db.execute(
"INSERT INTO `beatmaps` (`id`, `beatmap_id`, `beatmapset_id`, `beatmap_md5`, `song_name`, "
"`ar`, `od`, `difficulty_std`, `difficulty_taiko`, `difficulty_ctb`, `difficulty_mania`, "
"`max_combo`, `hit_length`, `bpm`, `ranked`, `latest_update`, `ranked_status_freezed`, `disable_pp`) "
"VALUES (NULL, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)", (
self.beatmapID,
self.beatmapSetID,
self.fileMD5,
@@ -80,10 +121,11 @@ class beatmap:
self.maxCombo,
self.hitLength,
self.bpm,
self.rankedStatus if frozen == 0 else 2,
self.rankedStatus if not frozen else 2,
int(time.time()),
frozen
])
frozen,
self.disablePP
))

def setDataFromDB(self, md5):
"""
@@ -144,6 +186,7 @@ class beatmap:
self.maxCombo = int(data["max_combo"])
self.hitLength = int(data["hit_length"])
self.bpm = int(data["bpm"])
self.disablePP = bool(data["disable_pp"])
# Ranking panel statistics
self.playcount = int(data["playcount"]) if "playcount" in data else 0
self.passcount = int(data["passcount"]) if "passcount" in data else 0
@@ -269,11 +312,16 @@ class beatmap:

return -- beatmap header for getscores
"""
rankedStatusOutput = self.rankedStatus

# Force approved for A/Q/L beatmaps that give PP, so we don't get the alert in game
if self.rankedStatus >= rankedStatuses.APPROVED and self.is_rankable:
rankedStatusOutput = rankedStatuses.APPROVED

# Fix loved maps for old clients
if version < 4 and self.rankedStatus == rankedStatuses.LOVED:
rankedStatusOutput = rankedStatuses.QUALIFIED
else:
rankedStatusOutput = self.rankedStatus

data = "{}|false".format(rankedStatusOutput)
if self.rankedStatus != rankedStatuses.NOT_SUBMITTED and self.rankedStatus != rankedStatuses.NEED_UPDATE and self.rankedStatus != rankedStatuses.UNKNOWN:
# If the beatmap is updated and exists, the client needs more data
@@ -304,7 +352,28 @@ class beatmap:

@property
def is_rankable(self):
return self.rankedStatus >= rankedStatuses.RANKED and self.rankedStatus != rankedStatuses.UNKNOWN
return self.rankedStatus >= rankedStatuses.RANKED \
and self.rankedStatus != rankedStatuses.UNKNOWN \
and not self.disablePP

@property
def is_mode_specific(self):
return sum(x > 0 for x in (self.starsStd, self.starsTaiko, self.starsCtb, self.starsMania)) == 1

@property
def specific_game_mode(self):
if not self.is_mode_specific:
return None
try:
return next(
mode for mode, pp in zip(
(gameModes.STD, gameModes.TAIKO, gameModes.CTB, gameModes.MANIA),
(self.starsStd, self.starsTaiko, self.starsCtb, self.starsMania)
) if pp > 0
)
except StopIteration:
# FUBAR beatmap 🤔
return None

def convertRankedStatus(approvedStatus):
"""


+ 3
- 1
objects/glob.py View File

@@ -3,6 +3,7 @@ import userStatsCache
from common.ddog import datadogClient
from common.files import fileBuffer, fileLocks
from common.web import schiavo
from helpers.aqlHelper import AqlThresholds

try:
with open("version") as f:
@@ -30,4 +31,5 @@ personalBestCache = personalBestCache.personalBestCache()
fileBuffers = fileBuffer.buffersList()
dog = datadogClient.datadogClient()
schiavo = schiavo.schiavo()
achievementClasses = {}
achievementClasses = {}
aqlThresholds = AqlThresholds()

+ 27
- 14
objects/score.pyx View File

@@ -1,24 +1,15 @@
import time

from objects import beatmap
import pp
from common.constants import gameModes
from objects import beatmap
from common.log import logUtils as log
from common.ripple import userUtils
from constants import rankedStatuses
from common.ripple import scoreUtils
from objects import glob
from pp import rippoppai
from pp import wifipiano3
from pp import cicciobello


class score:
PP_CALCULATORS = {
gameModes.STD: rippoppai.oppai,
gameModes.TAIKO: rippoppai.oppai,
gameModes.CTB: cicciobello.Cicciobello,
gameModes.MANIA: wifipiano3.WiFiPiano
}
__slots__ = ["scoreID", "playerName", "score", "maxCombo", "c50", "c100", "c300", "cMiss", "cKatu", "cGeki",
"fullCombo", "mods", "playerUserID","rank","date", "hasReplay", "fileMd5", "passed", "playDateTime",
"gameMode", "completed", "accuracy", "pp", "oldPersonalBest", "rankedScoreIncrease"]
@@ -266,9 +257,31 @@ class score:
b = beatmap.beatmap(self.fileMd5, 0)

# Calculate pp
if b.rankedStatus >= rankedStatuses.RANKED and b.rankedStatus != rankedStatuses.UNKNOWN \
and scoreUtils.isRankable(self.mods) and self.passed and self.gameMode in score.PP_CALCULATORS:
calculator = score.PP_CALCULATORS[self.gameMode](b, self)
if b.is_rankable and scoreUtils.isRankable(self.mods) and self.passed and self.gameMode in pp.PP_CALCULATORS:
calculator = pp.PP_CALCULATORS[self.gameMode](b, self)
self.pp = calculator.pp
else:
self.pp = 0

class PerfectScoreFactory:
@staticmethod
def create(beatmap, game_mode=gameModes.STD):
"""
Factory method that creates a perfect score.
Used to calculate max pp amount for a specific beatmap.

:param beatmap: beatmap object
:param game_mode: game mode number. Default: `gameModes.STD`
:return: `score` object
"""
s = score()
s.accuracy = 1.
# max combo cli param/arg gets omitted if it's < 0 and oppai/catch-the-pp set it to max combo.
# maniapp ignores max combo entirely.
s.maxCombo = -1
s.fullCombo = True
s.passed = True
s.gameMode = game_mode
if s.gameMode == gameModes.MANIA:
s.score = 1000000
return s

+ 11
- 0
pp/__init__.py View File

@@ -0,0 +1,11 @@
from common.constants import gameModes
from pp import rippoppai
from pp import wifipiano3
from pp import cicciobello

PP_CALCULATORS = {
gameModes.STD: rippoppai.oppai,
gameModes.TAIKO: rippoppai.oppai,
gameModes.CTB: cicciobello.Cicciobello,
gameModes.MANIA: wifipiano3.WiFiPiano
}

Loading…
Cancel
Save