commit
fc5a025bf0
8
handlers/generalHelper.py
Normal file
8
handlers/generalHelper.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
def zingonify(d):
|
||||||
|
"""
|
||||||
|
Zingonifies a string
|
||||||
|
:param d: input dict
|
||||||
|
:return: zingonified dict as str
|
||||||
|
"""
|
||||||
|
|
||||||
|
return "|".join(f"{k}:{v}" for k, v in d.items())
|
|
@ -30,6 +30,8 @@ from objects import score
|
||||||
from objects import scoreboard
|
from objects import scoreboard
|
||||||
from objects import relaxboard
|
from objects import relaxboard
|
||||||
from objects import rxscore
|
from objects import rxscore
|
||||||
|
from helpers.generalHelper import zingonify
|
||||||
|
from objects.charts import BeatmapChart, OverallChart
|
||||||
from common import generalUtils
|
from common import generalUtils
|
||||||
|
|
||||||
MODULE_NAME = "submit_modular"
|
MODULE_NAME = "submit_modular"
|
||||||
|
@ -215,6 +217,19 @@ class handler(requestsManager.asyncRequestHandler):
|
||||||
log.warning("**{}** ({}) has been restricted due to notepad hack".format(username, userID), "cm")
|
log.warning("**{}** ({}) has been restricted due to notepad hack".format(username, userID), "cm")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Right before submitting the score, get the personal best score object (we need it for charts)
|
||||||
|
if s.passed and s.oldPersonalBest > 0:
|
||||||
|
oldPersonalBestRank = glob.personalBestCache.get(userID, s.fileMd5)
|
||||||
|
if oldPersonalBestRank == 0:
|
||||||
|
oldScoreboard = scoreboard.scoreboard(username, s.gameMode, beatmapInfo, False)
|
||||||
|
oldScoreboard.setPersonalBestRank()
|
||||||
|
oldPersonalBestRank = max(oldScoreboard.personalBestRank, 0)
|
||||||
|
oldPersonalBest = score.score(s.oldPersonalBest, oldPersonalBestRank)
|
||||||
|
else:
|
||||||
|
oldPersonalBestRank = 0
|
||||||
|
oldPersonalBest = None
|
||||||
|
|
||||||
|
|
||||||
# Save score in db
|
# Save score in db
|
||||||
s.saveScoreInDB()
|
s.saveScoreInDB()
|
||||||
|
|
||||||
|
@ -331,17 +346,6 @@ class handler(requestsManager.asyncRequestHandler):
|
||||||
oldUserData = glob.userStatsCache.get(userID, s.gameMode)
|
oldUserData = glob.userStatsCache.get(userID, s.gameMode)
|
||||||
oldRank = userUtils.getGameRank(userID, s.gameMode)
|
oldRank = userUtils.getGameRank(userID, s.gameMode)
|
||||||
|
|
||||||
# Try to get oldPersonalBestRank from cache
|
|
||||||
oldPersonalBestRank = glob.personalBestCache.get(userID, s.fileMd5)
|
|
||||||
if oldPersonalBestRank == 0:
|
|
||||||
# oldPersonalBestRank not found in cache, get it from db
|
|
||||||
if isRelaxing:
|
|
||||||
oldScoreboard = relaxboard.scoreboard(username, s.gameMode, beatmapInfo, False)
|
|
||||||
else:
|
|
||||||
oldScoreboard = scoreboard.scoreboard(username, s.gameMode, beatmapInfo, False)
|
|
||||||
|
|
||||||
oldScoreboard.setPersonalBest()
|
|
||||||
oldPersonalBestRank = oldScoreboard.personalBestRank if oldScoreboard.personalBestRank > 0 else 0
|
|
||||||
|
|
||||||
# Always update users stats (total/ranked score, playcount, level, acc and pp)
|
# Always update users stats (total/ranked score, playcount, level, acc and pp)
|
||||||
# even if not passed
|
# even if not passed
|
||||||
|
@ -401,8 +405,11 @@ class handler(requestsManager.asyncRequestHandler):
|
||||||
# Trigger bancho stats cache update
|
# Trigger bancho stats cache update
|
||||||
glob.redis.publish("peppy:update_rxcached_stats", userID)
|
glob.redis.publish("peppy:update_rxcached_stats", userID)
|
||||||
|
|
||||||
# Get personal best after submitting the score
|
newScoreboard = relaxboard.scoreboard(username, s.gameMode, beatmapInfo, False)
|
||||||
newScoreboard = relaxboard.scoreboard(username, s.gameMode, beatmapInfo, True)
|
newScoreboard.setPersonalBestRank()
|
||||||
|
personalBestID = newScoreboard.getPersonalBestID()
|
||||||
|
assert personalBestID is not None
|
||||||
|
currentPersonalBest = rxscore.score(personalBestID, newScoreboard.personalBestRank)
|
||||||
|
|
||||||
# Get rank info (current rank, pp/score to next rank, user who is 1 rank above us)
|
# Get rank info (current rank, pp/score to next rank, user who is 1 rank above us)
|
||||||
rankInfo = leaderboardHelper.rxgetRankInfo(userID, s.gameMode)
|
rankInfo = leaderboardHelper.rxgetRankInfo(userID, s.gameMode)
|
||||||
|
@ -411,53 +418,70 @@ class handler(requestsManager.asyncRequestHandler):
|
||||||
# Trigger bancho stats cache update
|
# Trigger bancho stats cache update
|
||||||
glob.redis.publish("peppy:update_cached_stats", userID)
|
glob.redis.publish("peppy:update_cached_stats", userID)
|
||||||
|
|
||||||
# Get personal best after submitting the score
|
newScoreboard = scoreboard.scoreboard(username, s.gameMode, beatmapInfo, False)
|
||||||
newScoreboard = scoreboard.scoreboard(username, s.gameMode, beatmapInfo, True)
|
newScoreboard.setPersonalBestRank()
|
||||||
|
personalBestID = newScoreboard.getPersonalBestID()
|
||||||
|
assert personalBestID is not None
|
||||||
|
currentPersonalBest = score.score(personalBestID, newScoreboard.personalBestRank)
|
||||||
|
|
||||||
# Get rank info (current rank, pp/score to next rank, user who is 1 rank above us)
|
# Get rank info (current rank, pp/score to next rank, user who is 1 rank above us)
|
||||||
rankInfo = leaderboardHelper.getRankInfo(userID, s.gameMode)
|
rankInfo = leaderboardHelper.getRankInfo(userID, s.gameMode)
|
||||||
|
|
||||||
# Output dictionary
|
if newCharts:
|
||||||
output = collections.OrderedDict()
|
log.debug("Using new charts")
|
||||||
output["beatmapId"] = beatmapInfo.beatmapID
|
dicts = [
|
||||||
output["beatmapSetId"] = beatmapInfo.beatmapSetID
|
collections.OrderedDict([
|
||||||
output["beatmapPlaycount"] = beatmapInfo.playcount
|
("beatmapId", beatmapInfo.beatmapID),
|
||||||
output["beatmapPasscount"] = beatmapInfo.passcount
|
("beatmapSetId", beatmapInfo.beatmapSetID),
|
||||||
#output["approvedDate"] = "2015-07-09 23:20:14\n"
|
("beatmapPlaycount", beatmapInfo.playcount + 1),
|
||||||
output["approvedDate"] = "\n"
|
("beatmapPasscount", beatmapInfo.passcount + (s.completed == 3)),
|
||||||
output["chartId"] = "overall"
|
("approvedDate", beatmapInfo.rankingDate)
|
||||||
output["chartName"] = "Overall Ranking"
|
]),
|
||||||
output["chartEndDate"] = ""
|
BeatmapChart(
|
||||||
output["beatmapRankingBefore"] = oldPersonalBestRank
|
oldPersonalBest if s.completed == 3 else currentPersonalBest,
|
||||||
output["beatmapRankingAfter"] = newScoreboard.personalBestRank
|
currentPersonalBest if s.completed == 3 else s,
|
||||||
output["rankedScoreBefore"] = oldUserData["rankedScore"]
|
beatmapInfo.beatmapID,
|
||||||
output["rankedScoreAfter"] = newUserData["rankedScore"]
|
),
|
||||||
output["totalScoreBefore"] = oldUserData["totalScore"]
|
OverallChart(
|
||||||
output["totalScoreAfter"] = newUserData["totalScore"]
|
userID, oldUserData, newUserData, beatmapInfo, s, new_achievements, oldRank, rankInfo["currentRank"]
|
||||||
output["playCountBefore"] = newUserData["playcount"]
|
)
|
||||||
output["accuracyBefore"] = float(oldUserData["accuracy"])/100
|
]
|
||||||
output["accuracyAfter"] = float(newUserData["accuracy"])/100
|
else:
|
||||||
output["rankBefore"] = oldRank
|
log.debug("Using old charts")
|
||||||
output["rankAfter"] = rankInfo["currentRank"]
|
dicts = [
|
||||||
output["toNextRank"] = rankInfo["difference"]
|
collections.OrderedDict([
|
||||||
output["toNextRankUser"] = rankInfo["nextUsername"]
|
("beatmapId", beatmapInfo.beatmapID),
|
||||||
output["achievements"] = ""
|
("beatmapSetId", beatmapInfo.beatmapSetID),
|
||||||
output["achievements-new"] = secret.achievements.utils.achievements_response(new_achievements)
|
("beatmapPlaycount", beatmapInfo.playcount),
|
||||||
output["onlineScoreId"] = s.scoreID
|
("beatmapPasscount", beatmapInfo.passcount),
|
||||||
|
("approvedDate", beatmapInfo.rankingDate)
|
||||||
|
]),
|
||||||
|
collections.OrderedDict([
|
||||||
|
("chartId", "overall"),
|
||||||
|
("chartName", "Overall Ranking"),
|
||||||
|
("chartEndDate", ""),
|
||||||
|
("beatmapRankingBefore", oldPersonalBestRank),
|
||||||
|
("beatmapRankingAfter", newScoreboard.personalBestRank),
|
||||||
|
("rankedScoreBefore", oldUserData["rankedScore"]),
|
||||||
|
("rankedScoreAfter", newUserData["rankedScore"]),
|
||||||
|
("totalScoreBefore", oldUserData["totalScore"]),
|
||||||
|
("totalScoreAfter", newUserData["totalScore"]),
|
||||||
|
("playCountBefore", newUserData["playcount"]),
|
||||||
|
("accuracyBefore", float(oldUserData["accuracy"])/100),
|
||||||
|
("accuracyAfter", float(newUserData["accuracy"])/100),
|
||||||
|
("rankBefore", oldRank),
|
||||||
|
("rankAfter", rankInfo["currentRank"]),
|
||||||
|
("toNextRank", rankInfo["difference"]),
|
||||||
|
("toNextRankUser", rankInfo["nextUsername"]),
|
||||||
|
("achievements", ""),
|
||||||
|
("achievements-new", secret.achievements.utils.achievements_response(new_achievements)),
|
||||||
|
("onlineScoreId", s.scoreID)
|
||||||
|
])
|
||||||
|
]
|
||||||
|
output = "\n".join(zingonify(x) for x in dicts)
|
||||||
|
|
||||||
# Build final string
|
|
||||||
msg = ""
|
|
||||||
for line, val in output.items():
|
|
||||||
msg += "{}:{}".format(line, val)
|
|
||||||
if val != "\n":
|
|
||||||
if (len(output) - 1) != list(output.keys()).index(line):
|
|
||||||
msg += "|"
|
|
||||||
else:
|
|
||||||
msg += "\n"
|
|
||||||
|
|
||||||
# Some debug messages
|
|
||||||
log.debug("Generated output for online ranking screen!")
|
log.debug("Generated output for online ranking screen!")
|
||||||
log.debug(msg)
|
log.debug(output)
|
||||||
|
|
||||||
# Send message to #announce if we're rank #1
|
# Send message to #announce if we're rank #1
|
||||||
if newScoreboard.personalBestRank == 1 and s.completed == 3 and not restricted:
|
if newScoreboard.personalBestRank == 1 and s.completed == 3 and not restricted:
|
||||||
|
|
131
objects/charts.py
Normal file
131
objects/charts.py
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
from secret.achievements.utils import achievements_response
|
||||||
|
|
||||||
|
|
||||||
|
class Chart:
|
||||||
|
"""
|
||||||
|
Chart base class
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, id_, url, name):
|
||||||
|
"""
|
||||||
|
Initializes a new chart.
|
||||||
|
|
||||||
|
:param id_: chart id. Currently known values are 'beatmap' and 'overall'
|
||||||
|
:param url: URL to open when clicking on the chart title.
|
||||||
|
:param name: chart name displayed in the game client
|
||||||
|
"""
|
||||||
|
self.id_ = id_
|
||||||
|
self.url = url
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
"""
|
||||||
|
`items()` method that allows this class to be used as a iterable dict
|
||||||
|
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return self.output_attrs.items()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def output_attrs(self):
|
||||||
|
"""
|
||||||
|
An unzingonified dict containing the stuff that will be sent to the game client
|
||||||
|
|
||||||
|
:return: dict
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"chartId": self.id_,
|
||||||
|
"chartUrl": self.url,
|
||||||
|
"chartName": self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def before_after_dict(name, values, none_value="0"):
|
||||||
|
"""
|
||||||
|
Turns a tuple with two elements in a dict with two elements.
|
||||||
|
|
||||||
|
:param name: prefix of the keys
|
||||||
|
:param values: (value_before, value_after). value_before and value_after can be None.
|
||||||
|
:param none_value: value to use instead of None (None, when zingonified, is not recognized by the game client)
|
||||||
|
:return: { XXXBefore -> first element, XXXAfter -> second element }, where XXX is `name`
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
f"{name}{'Before' if i == 0 else 'After'}": x if x is not None else none_value for i, x in enumerate(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BeatmapChart(Chart):
|
||||||
|
"""
|
||||||
|
Beatmap ranking chart
|
||||||
|
"""
|
||||||
|
def __init__(self, old_score, new_score, beatmap_id):
|
||||||
|
"""
|
||||||
|
Initializes a new BeatmapChart object.
|
||||||
|
|
||||||
|
:param old_score: score object of the old score
|
||||||
|
:param new_score: score object of the currently submitted score
|
||||||
|
:param beatmap_id: beatmap id, for the clickable link
|
||||||
|
"""
|
||||||
|
super(BeatmapChart, self).__init__("beatmap", f"https://akatsuki.pw/b/{beatmap_id}", "Beatmap Ranking")
|
||||||
|
self.rank = (old_score.rank if old_score is not None else None, new_score.rank)
|
||||||
|
self.max_combo = (old_score.maxCombo if old_score is not None else None, new_score.maxCombo)
|
||||||
|
self.accuracy = (old_score.accuracy * 100 if old_score is not None else None, new_score.accuracy * 100)
|
||||||
|
self.ranked_score = (old_score.score if old_score is not None else None, new_score.score)
|
||||||
|
self.pp = (old_score.pp if old_score is not None else None, new_score.pp)
|
||||||
|
self.score_id = new_score.scoreID
|
||||||
|
|
||||||
|
@property
|
||||||
|
def output_attrs(self):
|
||||||
|
return {
|
||||||
|
**super(BeatmapChart, self).output_attrs,
|
||||||
|
**self.before_after_dict("rank", self.rank, none_value=""),
|
||||||
|
**self.before_after_dict("maxCombo", self.max_combo),
|
||||||
|
**self.before_after_dict("accuracy", self.accuracy),
|
||||||
|
**self.before_after_dict("rankedScore", self.ranked_score),
|
||||||
|
**self.before_after_dict("totalScore", self.ranked_score),
|
||||||
|
**self.before_after_dict("pp", self.pp),
|
||||||
|
"onlineScoreId": self.score_id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class OverallChart(Chart):
|
||||||
|
"""
|
||||||
|
Overall ranking chart achievements
|
||||||
|
"""
|
||||||
|
def __init__(self, user_id, old_user_stats, new_user_stats, bi, score, new_achievements, old_rank, new_rank):
|
||||||
|
"""
|
||||||
|
Initializes a new OverallChart object.
|
||||||
|
This constructor sucks because LETS itself sucks.
|
||||||
|
|
||||||
|
:param user_id: id of the user
|
||||||
|
:param old_user_stats: user stats dict before submitting the score
|
||||||
|
:param new_user_stats: user stats dict after submitting the score
|
||||||
|
:param score: score object of the scores that has just been submitted
|
||||||
|
:param new_achievements: achievements unlocked list
|
||||||
|
:param old_rank: global rank before submitting the scpre
|
||||||
|
:param new_rank: global rank after submitting the score
|
||||||
|
"""
|
||||||
|
super(OverallChart, self).__init__("overall", f"https://akatsuki.pw/u/{user_id}", "Overall Ranking")
|
||||||
|
self.rank = (old_rank, new_rank)
|
||||||
|
self.ranked_score = (old_user_stats["rankedScore"], new_user_stats["rankedScore"])
|
||||||
|
self.total_score = (old_user_stats["totalScore"], new_user_stats["totalScore"])
|
||||||
|
self.max_combo = (bi.maxCombo, bi.maxCombo)
|
||||||
|
self.accuracy = (old_user_stats["accuracy"], new_user_stats["accuracy"])
|
||||||
|
self.pp = (old_user_stats["pp"], new_user_stats["pp"])
|
||||||
|
self.new_achievements = new_achievements
|
||||||
|
self.score_id = score.scoreID
|
||||||
|
|
||||||
|
@property
|
||||||
|
def output_attrs(self):
|
||||||
|
return {
|
||||||
|
**super(OverallChart, self).output_attrs,
|
||||||
|
**self.before_after_dict("rank", self.rank),
|
||||||
|
**self.before_after_dict("rankedScore", self.ranked_score),
|
||||||
|
**self.before_after_dict("totalScore", self.total_score),
|
||||||
|
**self.before_after_dict("maxCombo", self.max_combo),
|
||||||
|
**self.before_after_dict("accuracy", self.accuracy),
|
||||||
|
**self.before_after_dict("pp", self.pp),
|
||||||
|
"achievements-new": achievements_response(self.new_achievements),
|
||||||
|
"onlineScoreId": self.score_id
|
||||||
|
}
|
|
@ -2,15 +2,14 @@ from objects import rxscore
|
||||||
from common.ripple import userUtils
|
from common.ripple import userUtils
|
||||||
from constants import rankedStatuses
|
from constants import rankedStatuses
|
||||||
from common.constants import mods as modsEnum
|
from common.constants import mods as modsEnum
|
||||||
from common.constants import privileges
|
|
||||||
from objects import glob
|
from objects import glob
|
||||||
|
from common.constants import privileges
|
||||||
|
|
||||||
|
|
||||||
class scoreboard:
|
class scoreboard:
|
||||||
def __init__(self, username, gameMode, beatmap, setScores = True, country = False, friends = False, mods = -1):
|
def __init__(self, username, gameMode, beatmap, setScores = True, country = False, friends = False, mods = -1):
|
||||||
"""
|
"""
|
||||||
Initialize a leaderboard object
|
Initialize a leaderboard object
|
||||||
|
|
||||||
username -- username of who's requesting the scoreboard. None if not known
|
username -- username of who's requesting the scoreboard. None if not known
|
||||||
gameMode -- requested gameMode
|
gameMode -- requested gameMode
|
||||||
beatmap -- beatmap objecy relative to this leaderboard
|
beatmap -- beatmap objecy relative to this leaderboard
|
||||||
|
@ -29,15 +28,48 @@ class scoreboard:
|
||||||
if setScores:
|
if setScores:
|
||||||
self.setScores()
|
self.setScores()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def buildQuery(params):
|
||||||
|
return "{select} {joins} {country} {mods} {friends} {order} {limit}".format(**params)
|
||||||
|
|
||||||
|
def getPersonalBestID(self):
|
||||||
|
if self.userID == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Query parts
|
||||||
|
cdef str select = ""
|
||||||
|
cdef str joins = ""
|
||||||
|
cdef str country = ""
|
||||||
|
cdef str mods = ""
|
||||||
|
cdef str friends = ""
|
||||||
|
cdef str order = ""
|
||||||
|
cdef str limit = ""
|
||||||
|
select = "SELECT id FROM scores_relax WHERE userid = %(userid)s AND beatmap_md5 = %(md5)s AND play_mode = %(mode)s AND completed = 3"
|
||||||
|
|
||||||
|
# Mods
|
||||||
|
if self.mods > -1:
|
||||||
|
mods = "AND mods = %(mods)s"
|
||||||
|
|
||||||
|
# Friends ranking
|
||||||
|
if self.friends:
|
||||||
|
friends = "AND (scores_relax.userid IN (SELECT user2 FROM users_relationships WHERE user1 = %(userid)s) OR scores_relax.userid = %(userid)s)"
|
||||||
|
|
||||||
|
# Sort and limit at the end
|
||||||
|
order = "ORDER BY pp DESC"
|
||||||
|
limit = "LIMIT 1"
|
||||||
|
|
||||||
|
# Build query, get params and run query
|
||||||
|
query = self.buildQuery(locals())
|
||||||
|
params = {"userid": self.userID, "md5": self.beatmap.fileMD5, "mode": self.gameMode, "mods": self.mods}
|
||||||
|
id_ = glob.db.fetch(query, params)
|
||||||
|
if id_ is None:
|
||||||
|
return None
|
||||||
|
return id_["id"]
|
||||||
|
|
||||||
def setScores(self):
|
def setScores(self):
|
||||||
"""
|
"""
|
||||||
Set scores list
|
Set scores list
|
||||||
"""
|
"""
|
||||||
|
|
||||||
isPremium = userUtils.getPrivileges(self.userID) & privileges.USER_PREMIUM
|
|
||||||
|
|
||||||
def buildQuery(params):
|
|
||||||
return "{select} {joins} {country} {mods} {friends} {order} {limit}".format(**params)
|
|
||||||
# Reset score list
|
# Reset score list
|
||||||
self.scores = []
|
self.scores = []
|
||||||
self.scores.append(-1)
|
self.scores.append(-1)
|
||||||
|
@ -56,38 +88,11 @@ class scoreboard:
|
||||||
cdef str limit = ""
|
cdef str limit = ""
|
||||||
|
|
||||||
# Find personal best score
|
# Find personal best score
|
||||||
if self.userID != 0:
|
personalBestScoreID = self.getPersonalBestID()
|
||||||
# Query parts
|
isPremium = userUtils.getPrivileges(self.userID) & privileges.USER_PREMIUM
|
||||||
select = "SELECT id FROM scores_relax WHERE userid = %(userid)s AND beatmap_md5 = %(md5)s AND play_mode = %(mode)s AND completed = 3"
|
|
||||||
|
|
||||||
# Mods
|
|
||||||
if self.mods > -1:
|
|
||||||
mods = "AND mods = %(mods)s"
|
|
||||||
|
|
||||||
# Friends ranking
|
|
||||||
if self.friends:
|
|
||||||
friends = "AND (scores_relax.userid IN (SELECT user2 FROM users_relationships WHERE user1 = %(userid)s) OR scores_relax.userid = %(userid)s)"
|
|
||||||
else:
|
|
||||||
friends = ""
|
|
||||||
|
|
||||||
# Sort and limit at the end
|
|
||||||
if self.beatmap.rankedStatus == rankedStatuses.LOVED:
|
|
||||||
order = "ORDER BY score DESC"
|
|
||||||
else:
|
|
||||||
order = "ORDER BY pp DESC"
|
|
||||||
|
|
||||||
limit = "LIMIT 1"
|
|
||||||
|
|
||||||
# Build query, get params and run query
|
|
||||||
query = buildQuery(locals())
|
|
||||||
params = {"userid": self.userID, "md5": self.beatmap.fileMD5, "mode": self.gameMode, "mods": self.mods}
|
|
||||||
personalBestScore = glob.db.fetch(query, params)
|
|
||||||
else:
|
|
||||||
personalBestScore = None
|
|
||||||
|
|
||||||
# Output our personal best if found
|
# Output our personal best if found
|
||||||
if personalBestScore is not None:
|
if personalBestScoreID is not None:
|
||||||
s = rxscore.score(personalBestScore["id"])
|
s = rxscore.score(personalBestScoreID)
|
||||||
self.scores[0] = s
|
self.scores[0] = s
|
||||||
else:
|
else:
|
||||||
# No personal best
|
# No personal best
|
||||||
|
@ -99,17 +104,12 @@ class scoreboard:
|
||||||
|
|
||||||
# Country ranking
|
# Country ranking
|
||||||
if self.country:
|
if self.country:
|
||||||
""" Honestly this is more of a preference thing than something that should be premium only?
|
|
||||||
if isPremium:
|
|
||||||
country = "AND user_clans.clan = (SELECT clan FROM user_clans WHERE user = %(userid)s LIMIT 1)"
|
|
||||||
else:
|
|
||||||
"""
|
|
||||||
country = "AND users_stats.country = (SELECT country FROM users_stats WHERE id = %(userid)s LIMIT 1)"
|
country = "AND users_stats.country = (SELECT country FROM users_stats WHERE id = %(userid)s LIMIT 1)"
|
||||||
else:
|
else:
|
||||||
country = ""
|
country = ""
|
||||||
|
|
||||||
# Mods ranking (ignore auto, since we use it for pp sorting)
|
# Mods ranking (ignore auto, since we use it for pp sorting)
|
||||||
if self.mods > -1:
|
if self.mods > -1 and self.mods & modsEnum.AUTOPLAY == 0:
|
||||||
mods = "AND scores_relax.mods = %(mods)s"
|
mods = "AND scores_relax.mods = %(mods)s"
|
||||||
else:
|
else:
|
||||||
mods = ""
|
mods = ""
|
||||||
|
@ -119,19 +119,19 @@ class scoreboard:
|
||||||
friends = "AND (scores_relax.userid IN (SELECT user2 FROM users_relationships WHERE user1 = %(userid)s) OR scores_relax.userid = %(userid)s)"
|
friends = "AND (scores_relax.userid IN (SELECT user2 FROM users_relationships WHERE user1 = %(userid)s) OR scores_relax.userid = %(userid)s)"
|
||||||
else:
|
else:
|
||||||
friends = ""
|
friends = ""
|
||||||
|
|
||||||
if self.beatmap.rankedStatus == rankedStatuses.LOVED:
|
if self.beatmap.rankedStatus == rankedStatuses.LOVED:
|
||||||
order = "ORDER BY score DESC"
|
order = "ORDER BY score DESC"
|
||||||
else:
|
else:
|
||||||
order = "ORDER BY pp DESC"
|
order = "ORDER BY pp DESC"
|
||||||
|
|
||||||
if isPremium: # Premium members can see up to 100 scores on leaderboards
|
if isPremium: # Premium members can see up to 100 scores on leaderboards
|
||||||
limit = "LIMIT 100"
|
limit = "LIMIT 100"
|
||||||
else:
|
else:
|
||||||
limit = "LIMIT 50"
|
limit = "LIMIT 50"
|
||||||
|
|
||||||
# Build query, get params and run query
|
# Build query, get params and run query
|
||||||
query = buildQuery(locals())
|
query = self.buildQuery(locals())
|
||||||
params = {"beatmap_md5": self.beatmap.fileMD5, "play_mode": self.gameMode, "userid": self.userID, "mods": self.mods}
|
params = {"beatmap_md5": self.beatmap.fileMD5, "play_mode": self.gameMode, "userid": self.userID, "mods": self.mods}
|
||||||
topScores = glob.db.fetchAll(query, params)
|
topScores = glob.db.fetchAll(query, params)
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ class scoreboard:
|
||||||
|
|
||||||
# Set data and rank from topScores's row
|
# Set data and rank from topScores's row
|
||||||
s.setDataFromDict(topScore)
|
s.setDataFromDict(topScore)
|
||||||
s.setRank(c)
|
s.rank = c
|
||||||
|
|
||||||
# Check if this top 50 score is our personal best
|
# Check if this top 50 score is our personal best
|
||||||
if s.playerName == self.username:
|
if s.playerName == self.username:
|
||||||
|
@ -160,9 +160,8 @@ class scoreboard:
|
||||||
# Count all scores on this map
|
# Count all scores on this map
|
||||||
select = "SELECT COUNT(*) AS count"
|
select = "SELECT COUNT(*) AS count"
|
||||||
limit = "LIMIT 1"
|
limit = "LIMIT 1"
|
||||||
|
|
||||||
# Build query, get params and run query
|
# Build query, get params and run query
|
||||||
query = buildQuery(locals())
|
query = self.buildQuery(locals())
|
||||||
count = glob.db.fetch(query, params)
|
count = glob.db.fetch(query, params)
|
||||||
if count == None:
|
if count == None:
|
||||||
self.totalScores = 0
|
self.totalScores = 0
|
||||||
|
@ -172,19 +171,19 @@ class scoreboard:
|
||||||
self.totalScores = c-1'''
|
self.totalScores = c-1'''
|
||||||
|
|
||||||
# If personal best score was not in top 50, try to get it from cache
|
# If personal best score was not in top 50, try to get it from cache
|
||||||
if personalBestScore is not None and self.personalBestRank < 1:
|
if personalBestScoreID is not None and self.personalBestRank < 1:
|
||||||
self.personalBestRank = glob.personalBestCache.get(self.userID, self.beatmap.fileMD5, self.country, self.friends, self.mods)
|
self.personalBestRank = glob.personalBestCache.get(self.userID, self.beatmap.fileMD5, self.country, self.friends, self.mods)
|
||||||
|
|
||||||
# It's not even in cache, get it from db
|
# It's not even in cache, get it from db
|
||||||
if personalBestScore is not None and self.personalBestRank < 1:
|
if personalBestScoreID is not None and self.personalBestRank < 1:
|
||||||
self.setPersonalBest()
|
self.setPersonalBestRank()
|
||||||
|
|
||||||
# Cache our personal best rank so we can eventually use it later as
|
# Cache our personal best rank so we can eventually use it later as
|
||||||
# before personal best rank" in submit modular when building ranking panel
|
# before personal best rank" in submit modular when building ranking panel
|
||||||
if self.personalBestRank >= 1:
|
if self.personalBestRank >= 1:
|
||||||
glob.personalBestCache.set(self.userID, self.personalBestRank, self.beatmap.fileMD5)
|
glob.personalBestCache.set(self.userID, self.personalBestRank, self.beatmap.fileMD5)
|
||||||
|
|
||||||
def setPersonalBest(self):
|
def setPersonalBestRank(self):
|
||||||
"""
|
"""
|
||||||
Set personal best rank ONLY
|
Set personal best rank ONLY
|
||||||
Ikr, that query is HUGE but xd
|
Ikr, that query is HUGE but xd
|
||||||
|
@ -203,12 +202,15 @@ class scoreboard:
|
||||||
if hasScore is None:
|
if hasScore is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
# We have a score, run the huge query
|
# We have a score, run the huge query
|
||||||
# Base query
|
# Base query
|
||||||
query = """SELECT COUNT(*) AS rank FROM scores_relax STRAIGHT_JOIN users ON scores_relax.userid = users.id STRAIGHT_JOIN users_stats ON users.id = users_stats.id WHERE scores_relax.{PPorScore} >= (
|
|
||||||
SELECT {PPorScore} FROM scores_relax WHERE beatmap_md5 = %(md5)s AND play_mode = %(mode)s AND completed = 3 AND userid = %(userid)s LIMIT 1
|
overwrite = "pp"
|
||||||
) AND scores_relax.beatmap_md5 = %(md5)s AND scores_relax.play_mode = %(mode)s AND scores_relax.completed = 3 AND users.privileges & 1 > 0""".format(PPorScore="score" if self.beatmap.rankedStatus == rankedStatuses.LOVED else "pp")
|
# We have a score, run the huge query
|
||||||
|
# Base query
|
||||||
|
query = """SELECT COUNT(*) AS rank FROM scores_relax STRAIGHT_JOIN users ON scores_relax.userid = users.id STRAIGHT_JOIN users_stats ON users.id = users_stats.id WHERE scores_relax.{0} >= (
|
||||||
|
SELECT {0} FROM scores_relax WHERE beatmap_md5 = %(md5)s AND play_mode = %(mode)s AND completed = 3 AND userid = %(userid)s LIMIT 1
|
||||||
|
) AND scores_relax.beatmap_md5 = %(md5)s AND scores_relax.play_mode = %(mode)s AND scores_relax.completed = 3 AND users.privileges & 1 > 0""".format(overwrite)
|
||||||
# Country
|
# Country
|
||||||
if self.country:
|
if self.country:
|
||||||
query += " AND users_stats.country = (SELECT country FROM users_stats WHERE id = %(userid)s LIMIT 1)"
|
query += " AND users_stats.country = (SELECT country FROM users_stats WHERE id = %(userid)s LIMIT 1)"
|
||||||
|
@ -219,12 +221,7 @@ class scoreboard:
|
||||||
if self.friends:
|
if self.friends:
|
||||||
query += " AND (scores_relax.userid IN (SELECT user2 FROM users_relationships WHERE user1 = %(userid)s) OR scores_relax.userid = %(userid)s)"
|
query += " AND (scores_relax.userid IN (SELECT user2 FROM users_relationships WHERE user1 = %(userid)s) OR scores_relax.userid = %(userid)s)"
|
||||||
# Sort and limit at the end
|
# Sort and limit at the end
|
||||||
|
query += " ORDER BY pp DESC LIMIT 1".format(overwrite)
|
||||||
if self.beatmap.rankedStatus == rankedStatuses.LOVED:
|
|
||||||
query += " ORDER BY score DESC LIMIT 1"
|
|
||||||
else:
|
|
||||||
query += " ORDER BY pp DESC LIMIT 1"
|
|
||||||
|
|
||||||
result = glob.db.fetch(query, {"md5": self.beatmap.fileMD5, "userid": self.userID, "mode": self.gameMode, "mods": self.mods})
|
result = glob.db.fetch(query, {"md5": self.beatmap.fileMD5, "userid": self.userID, "mode": self.gameMode, "mods": self.mods})
|
||||||
if result is not None:
|
if result is not None:
|
||||||
self.personalBestRank = result["rank"]
|
self.personalBestRank = result["rank"]
|
||||||
|
@ -232,7 +229,6 @@ class scoreboard:
|
||||||
def getScoresData(self):
|
def getScoresData(self):
|
||||||
"""
|
"""
|
||||||
Return scores data for getscores
|
Return scores data for getscores
|
||||||
|
|
||||||
return -- score data in getscores format
|
return -- score data in getscores format
|
||||||
"""
|
"""
|
||||||
data = ""
|
data = ""
|
||||||
|
@ -243,11 +239,12 @@ class scoreboard:
|
||||||
data += "\n"
|
data += "\n"
|
||||||
else:
|
else:
|
||||||
# Set personal best score rank
|
# Set personal best score rank
|
||||||
self.setPersonalBest() # sets self.personalBestRank with the huge query
|
self.setPersonalBestRank() # sets self.personalBestRank with the huge query
|
||||||
self.scores[0].setRank(self.personalBestRank)
|
self.scores[0].rank = self.personalBestRank
|
||||||
data += self.scores[0].getData()
|
data += self.scores[0].getData(pp=True)
|
||||||
|
|
||||||
# Output top 50 scores
|
# Output top 50 scores
|
||||||
for i in self.scores[1:]:
|
for i in self.scores[1:]:
|
||||||
data += i.getData(pp=self.beatmap.rankedStatus != rankedStatuses.LOVED)
|
data += i.getData(pp=True)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -2,15 +2,14 @@ from objects import score
|
||||||
from common.ripple import userUtils
|
from common.ripple import userUtils
|
||||||
from constants import rankedStatuses
|
from constants import rankedStatuses
|
||||||
from common.constants import mods as modsEnum
|
from common.constants import mods as modsEnum
|
||||||
from common.constants import privileges
|
|
||||||
from objects import glob
|
from objects import glob
|
||||||
|
from common.constants import privileges
|
||||||
|
|
||||||
|
|
||||||
class scoreboard:
|
class scoreboard:
|
||||||
def __init__(self, username, gameMode, beatmap, setScores = True, country = False, friends = False, mods = -1):
|
def __init__(self, username, gameMode, beatmap, setScores = True, country = False, friends = False, mods = -1):
|
||||||
"""
|
"""
|
||||||
Initialize a leaderboard object
|
Initialize a leaderboard object
|
||||||
|
|
||||||
username -- username of who's requesting the scoreboard. None if not known
|
username -- username of who's requesting the scoreboard. None if not known
|
||||||
gameMode -- requested gameMode
|
gameMode -- requested gameMode
|
||||||
beatmap -- beatmap objecy relative to this leaderboard
|
beatmap -- beatmap objecy relative to this leaderboard
|
||||||
|
@ -29,15 +28,48 @@ class scoreboard:
|
||||||
if setScores:
|
if setScores:
|
||||||
self.setScores()
|
self.setScores()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def buildQuery(params):
|
||||||
|
return "{select} {joins} {country} {mods} {friends} {order} {limit}".format(**params)
|
||||||
|
|
||||||
|
def getPersonalBestID(self):
|
||||||
|
if self.userID == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Query parts
|
||||||
|
cdef str select = ""
|
||||||
|
cdef str joins = ""
|
||||||
|
cdef str country = ""
|
||||||
|
cdef str mods = ""
|
||||||
|
cdef str friends = ""
|
||||||
|
cdef str order = ""
|
||||||
|
cdef str limit = ""
|
||||||
|
select = "SELECT id FROM scores WHERE userid = %(userid)s AND beatmap_md5 = %(md5)s AND play_mode = %(mode)s AND completed = 3"
|
||||||
|
|
||||||
|
# Mods
|
||||||
|
if self.mods > -1:
|
||||||
|
mods = "AND mods = %(mods)s"
|
||||||
|
|
||||||
|
# Friends ranking
|
||||||
|
if self.friends:
|
||||||
|
friends = "AND (scores.userid IN (SELECT user2 FROM users_relationships WHERE user1 = %(userid)s) OR scores.userid = %(userid)s)"
|
||||||
|
|
||||||
|
# Sort and limit at the end
|
||||||
|
order = "ORDER BY score DESC"
|
||||||
|
limit = "LIMIT 1"
|
||||||
|
|
||||||
|
# Build query, get params and run query
|
||||||
|
query = self.buildQuery(locals())
|
||||||
|
params = {"userid": self.userID, "md5": self.beatmap.fileMD5, "mode": self.gameMode, "mods": self.mods}
|
||||||
|
id_ = glob.db.fetch(query, params)
|
||||||
|
if id_ is None:
|
||||||
|
return None
|
||||||
|
return id_["id"]
|
||||||
|
|
||||||
def setScores(self):
|
def setScores(self):
|
||||||
"""
|
"""
|
||||||
Set scores list
|
Set scores list
|
||||||
"""
|
"""
|
||||||
|
|
||||||
isPremium = userUtils.getPrivileges(self.userID) & privileges.USER_PREMIUM
|
|
||||||
|
|
||||||
def buildQuery(params):
|
|
||||||
return "{select} {joins} {country} {mods} {friends} {order} {limit}".format(**params)
|
|
||||||
# Reset score list
|
# Reset score list
|
||||||
self.scores = []
|
self.scores = []
|
||||||
self.scores.append(-1)
|
self.scores.append(-1)
|
||||||
|
@ -56,32 +88,11 @@ class scoreboard:
|
||||||
cdef str limit = ""
|
cdef str limit = ""
|
||||||
|
|
||||||
# Find personal best score
|
# Find personal best score
|
||||||
if self.userID != 0:
|
personalBestScoreID = self.getPersonalBestID()
|
||||||
# Query parts
|
isPremium = userUtils.getPrivileges(self.userID) & privileges.USER_PREMIUM
|
||||||
select = "SELECT id FROM scores WHERE userid = %(userid)s AND beatmap_md5 = %(md5)s AND play_mode = %(mode)s AND completed = 3"
|
|
||||||
|
|
||||||
# Mods
|
|
||||||
if self.mods > -1:
|
|
||||||
mods = "AND mods = %(mods)s"
|
|
||||||
|
|
||||||
# Friends ranking
|
|
||||||
if self.friends:
|
|
||||||
friends = "AND (scores.userid IN (SELECT user2 FROM users_relationships WHERE user1 = %(userid)s) OR scores.userid = %(userid)s)"
|
|
||||||
|
|
||||||
# Sort and limit at the end
|
|
||||||
order = "ORDER BY score DESC"
|
|
||||||
limit = "LIMIT 1"
|
|
||||||
|
|
||||||
# Build query, get params and run query
|
|
||||||
query = buildQuery(locals())
|
|
||||||
params = {"userid": self.userID, "md5": self.beatmap.fileMD5, "mode": self.gameMode, "mods": self.mods}
|
|
||||||
personalBestScore = glob.db.fetch(query, params)
|
|
||||||
else:
|
|
||||||
personalBestScore = None
|
|
||||||
|
|
||||||
# Output our personal best if found
|
# Output our personal best if found
|
||||||
if personalBestScore is not None:
|
if personalBestScoreID is not None:
|
||||||
s = score.score(personalBestScore["id"])
|
s = score.score(personalBestScoreID)
|
||||||
self.scores[0] = s
|
self.scores[0] = s
|
||||||
else:
|
else:
|
||||||
# No personal best
|
# No personal best
|
||||||
|
@ -93,17 +104,12 @@ class scoreboard:
|
||||||
|
|
||||||
# Country ranking
|
# Country ranking
|
||||||
if self.country:
|
if self.country:
|
||||||
""" Honestly this is more of a preference thing than something that should be premium only?
|
|
||||||
if isPremium:
|
|
||||||
country = "AND user_clans.clan = (SELECT clan FROM user_clans WHERE user = %(userid)s LIMIT 1)"
|
|
||||||
else:
|
|
||||||
"""
|
|
||||||
country = "AND users_stats.country = (SELECT country FROM users_stats WHERE id = %(userid)s LIMIT 1)"
|
country = "AND users_stats.country = (SELECT country FROM users_stats WHERE id = %(userid)s LIMIT 1)"
|
||||||
else:
|
else:
|
||||||
country = ""
|
country = ""
|
||||||
|
|
||||||
# Mods ranking (ignore auto, since we use it for pp sorting)
|
# Mods ranking (ignore auto, since we use it for pp sorting)
|
||||||
if self.mods > -1:
|
if self.mods > -1 and self.mods & modsEnum.AUTOPLAY == 0:
|
||||||
mods = "AND scores.mods = %(mods)s"
|
mods = "AND scores.mods = %(mods)s"
|
||||||
else:
|
else:
|
||||||
mods = ""
|
mods = ""
|
||||||
|
@ -113,7 +119,7 @@ class scoreboard:
|
||||||
friends = "AND (scores.userid IN (SELECT user2 FROM users_relationships WHERE user1 = %(userid)s) OR scores.userid = %(userid)s)"
|
friends = "AND (scores.userid IN (SELECT user2 FROM users_relationships WHERE user1 = %(userid)s) OR scores.userid = %(userid)s)"
|
||||||
else:
|
else:
|
||||||
friends = ""
|
friends = ""
|
||||||
|
|
||||||
order = "ORDER BY score DESC"
|
order = "ORDER BY score DESC"
|
||||||
|
|
||||||
if isPremium: # Premium members can see up to 100 scores on leaderboards
|
if isPremium: # Premium members can see up to 100 scores on leaderboards
|
||||||
|
@ -122,7 +128,7 @@ class scoreboard:
|
||||||
limit = "LIMIT 50"
|
limit = "LIMIT 50"
|
||||||
|
|
||||||
# Build query, get params and run query
|
# Build query, get params and run query
|
||||||
query = buildQuery(locals())
|
query = self.buildQuery(locals())
|
||||||
params = {"beatmap_md5": self.beatmap.fileMD5, "play_mode": self.gameMode, "userid": self.userID, "mods": self.mods}
|
params = {"beatmap_md5": self.beatmap.fileMD5, "play_mode": self.gameMode, "userid": self.userID, "mods": self.mods}
|
||||||
topScores = glob.db.fetchAll(query, params)
|
topScores = glob.db.fetchAll(query, params)
|
||||||
|
|
||||||
|
@ -136,7 +142,7 @@ class scoreboard:
|
||||||
|
|
||||||
# Set data and rank from topScores's row
|
# Set data and rank from topScores's row
|
||||||
s.setDataFromDict(topScore)
|
s.setDataFromDict(topScore)
|
||||||
s.setRank(c)
|
s.rank = c
|
||||||
|
|
||||||
# Check if this top 50 score is our personal best
|
# Check if this top 50 score is our personal best
|
||||||
if s.playerName == self.username:
|
if s.playerName == self.username:
|
||||||
|
@ -151,9 +157,8 @@ class scoreboard:
|
||||||
# Count all scores on this map
|
# Count all scores on this map
|
||||||
select = "SELECT COUNT(*) AS count"
|
select = "SELECT COUNT(*) AS count"
|
||||||
limit = "LIMIT 1"
|
limit = "LIMIT 1"
|
||||||
|
|
||||||
# Build query, get params and run query
|
# Build query, get params and run query
|
||||||
query = buildQuery(locals())
|
query = self.buildQuery(locals())
|
||||||
count = glob.db.fetch(query, params)
|
count = glob.db.fetch(query, params)
|
||||||
if count == None:
|
if count == None:
|
||||||
self.totalScores = 0
|
self.totalScores = 0
|
||||||
|
@ -163,19 +168,19 @@ class scoreboard:
|
||||||
self.totalScores = c-1'''
|
self.totalScores = c-1'''
|
||||||
|
|
||||||
# If personal best score was not in top 50, try to get it from cache
|
# If personal best score was not in top 50, try to get it from cache
|
||||||
if personalBestScore is not None and self.personalBestRank < 1:
|
if personalBestScoreID is not None and self.personalBestRank < 1:
|
||||||
self.personalBestRank = glob.personalBestCache.get(self.userID, self.beatmap.fileMD5, self.country, self.friends, self.mods)
|
self.personalBestRank = glob.personalBestCache.get(self.userID, self.beatmap.fileMD5, self.country, self.friends, self.mods)
|
||||||
|
|
||||||
# It's not even in cache, get it from db
|
# It's not even in cache, get it from db
|
||||||
if personalBestScore is not None and self.personalBestRank < 1:
|
if personalBestScoreID is not None and self.personalBestRank < 1:
|
||||||
self.setPersonalBest()
|
self.setPersonalBestRank()
|
||||||
|
|
||||||
# Cache our personal best rank so we can eventually use it later as
|
# Cache our personal best rank so we can eventually use it later as
|
||||||
# before personal best rank" in submit modular when building ranking panel
|
# before personal best rank" in submit modular when building ranking panel
|
||||||
if self.personalBestRank >= 1:
|
if self.personalBestRank >= 1:
|
||||||
glob.personalBestCache.set(self.userID, self.personalBestRank, self.beatmap.fileMD5)
|
glob.personalBestCache.set(self.userID, self.personalBestRank, self.beatmap.fileMD5)
|
||||||
|
|
||||||
def setPersonalBest(self):
|
def setPersonalBestRank(self):
|
||||||
"""
|
"""
|
||||||
Set personal best rank ONLY
|
Set personal best rank ONLY
|
||||||
Ikr, that query is HUGE but xd
|
Ikr, that query is HUGE but xd
|
||||||
|
@ -217,7 +222,6 @@ class scoreboard:
|
||||||
def getScoresData(self):
|
def getScoresData(self):
|
||||||
"""
|
"""
|
||||||
Return scores data for getscores
|
Return scores data for getscores
|
||||||
|
|
||||||
return -- score data in getscores format
|
return -- score data in getscores format
|
||||||
"""
|
"""
|
||||||
data = ""
|
data = ""
|
||||||
|
@ -228,8 +232,8 @@ class scoreboard:
|
||||||
data += "\n"
|
data += "\n"
|
||||||
else:
|
else:
|
||||||
# Set personal best score rank
|
# Set personal best score rank
|
||||||
self.setPersonalBest() # sets self.personalBestRank with the huge query
|
self.setPersonalBestRank() # sets self.personalBestRank with the huge query
|
||||||
self.scores[0].setRank(self.personalBestRank)
|
self.scores[0].rank = self.personalBestRank
|
||||||
data += self.scores[0].getData()
|
data += self.scores[0].getData()
|
||||||
|
|
||||||
# Output top 50 scores
|
# Output top 50 scores
|
||||||
|
|
Reference in New Issue
Block a user