Initial commit
This commit is contained in:
0
objects/__init__.py
Normal file
0
objects/__init__.py
Normal file
340
objects/beatmap.pyx
Normal file
340
objects/beatmap.pyx
Normal file
@@ -0,0 +1,340 @@
|
||||
import time
|
||||
|
||||
from common.log import logUtils as log
|
||||
from constants import rankedStatuses
|
||||
from helpers import osuapiHelper
|
||||
from objects import glob
|
||||
|
||||
|
||||
class beatmap:
|
||||
__slots__ = ["songName", "fileMD5", "rankedStatus", "rankedStatusFrozen", "beatmapID", "beatmapSetID", "offset",
|
||||
"rating", "starsStd", "starsTaiko", "starsCtb", "starsMania", "AR", "OD", "maxCombo", "hitLength",
|
||||
"bpm", "playcount" ,"passcount", "refresh"]
|
||||
|
||||
def __init__(self, md5 = None, beatmapSetID = None, gameMode = 0, refresh=False):
|
||||
"""
|
||||
Initialize a beatmap object.
|
||||
|
||||
md5 -- beatmap md5. Optional.
|
||||
beatmapSetID -- beatmapSetID. Optional.
|
||||
"""
|
||||
self.songName = ""
|
||||
self.fileMD5 = ""
|
||||
self.rankedStatus = rankedStatuses.NOT_SUBMITTED
|
||||
self.rankedStatusFrozen = 0
|
||||
self.beatmapID = 0
|
||||
self.beatmapSetID = 0
|
||||
self.offset = 0 # Won't implement
|
||||
self.rating = 10.0 # Won't implement
|
||||
|
||||
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
|
||||
self.AR = 0.0
|
||||
self.OD = 0.0
|
||||
self.maxCombo = 0
|
||||
self.hitLength = 0
|
||||
self.bpm = 0
|
||||
|
||||
# Statistics for ranking panel
|
||||
self.playcount = 0
|
||||
|
||||
# Force refresh from osu api
|
||||
self.refresh = refresh
|
||||
|
||||
if md5 is not None and beatmapSetID is not None:
|
||||
self.setData(md5, beatmapSetID)
|
||||
|
||||
def addBeatmapToDB(self):
|
||||
"""
|
||||
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])
|
||||
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:
|
||||
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
|
||||
|
||||
# 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);", [
|
||||
self.beatmapID,
|
||||
self.beatmapSetID,
|
||||
self.fileMD5,
|
||||
self.songName.encode("utf-8", "ignore").decode("utf-8"),
|
||||
self.AR,
|
||||
self.OD,
|
||||
self.starsStd,
|
||||
self.starsTaiko,
|
||||
self.starsCtb,
|
||||
self.starsMania,
|
||||
self.maxCombo,
|
||||
self.hitLength,
|
||||
self.bpm,
|
||||
self.rankedStatus if frozen == 0 else 2,
|
||||
int(time.time()),
|
||||
frozen
|
||||
])
|
||||
|
||||
def setDataFromDB(self, md5):
|
||||
"""
|
||||
Set this object's beatmap data from db.
|
||||
|
||||
md5 -- beatmap md5
|
||||
return -- True if set, False if not set
|
||||
"""
|
||||
# Get data from DB
|
||||
data = glob.db.fetch("SELECT * FROM beatmaps WHERE beatmap_md5 = %s LIMIT 1", [md5])
|
||||
|
||||
# Make sure the query returned something
|
||||
if data is None:
|
||||
return False
|
||||
|
||||
# Make sure the beatmap is not an old one
|
||||
if data["difficulty_taiko"] == 0 and data["difficulty_ctb"] == 0 and data["difficulty_mania"] == 0:
|
||||
log.debug("Difficulty for non-std gamemodes not found in DB, refreshing data from osu!api...")
|
||||
return False
|
||||
|
||||
# Set cached data period
|
||||
expire = int(glob.conf.config["server"]["beatmapcacheexpire"])
|
||||
|
||||
# If the beatmap is ranked, we don't need to refresh data from osu!api that often
|
||||
if data["ranked"] >= rankedStatuses.RANKED and data["ranked_status_freezed"] == 0:
|
||||
expire *= 3
|
||||
|
||||
# Make sure the beatmap data in db is not too old
|
||||
if int(expire) > 0 and time.time() > data["latest_update"]+int(expire):
|
||||
if data["ranked_status_freezed"] == 1:
|
||||
self.setDataFromDict(data)
|
||||
return False
|
||||
|
||||
# Data in DB, set beatmap data
|
||||
log.debug("Got beatmap data from db")
|
||||
self.setDataFromDict(data)
|
||||
return True
|
||||
|
||||
def setDataFromDict(self, data):
|
||||
"""
|
||||
Set this object's beatmap data from data dictionary.
|
||||
|
||||
data -- data dictionary
|
||||
return -- True if set, False if not set
|
||||
"""
|
||||
self.songName = data["song_name"]
|
||||
self.fileMD5 = data["beatmap_md5"]
|
||||
self.rankedStatus = int(data["ranked"])
|
||||
self.rankedStatusFrozen = int(data["ranked_status_freezed"])
|
||||
self.beatmapID = int(data["beatmap_id"])
|
||||
self.beatmapSetID = int(data["beatmapset_id"])
|
||||
self.AR = float(data["ar"])
|
||||
self.OD = float(data["od"])
|
||||
self.starsStd = float(data["difficulty_std"])
|
||||
self.starsTaiko = float(data["difficulty_taiko"])
|
||||
self.starsCtb = float(data["difficulty_ctb"])
|
||||
self.starsMania = float(data["difficulty_mania"])
|
||||
self.maxCombo = int(data["max_combo"])
|
||||
self.hitLength = int(data["hit_length"])
|
||||
self.bpm = int(data["bpm"])
|
||||
# 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
|
||||
|
||||
def setDataFromOsuApi(self, md5, beatmapSetID):
|
||||
"""
|
||||
Set this object's beatmap data from osu!api.
|
||||
|
||||
md5 -- beatmap md5
|
||||
beatmapSetID -- beatmap set ID, used to check if a map is outdated
|
||||
return -- True if set, False if not set
|
||||
"""
|
||||
# Check if osuapi is enabled
|
||||
mainData = None
|
||||
dataStd = osuapiHelper.osuApiRequest("get_beatmaps", "h={}&a=1&m=0".format(md5))
|
||||
dataTaiko = osuapiHelper.osuApiRequest("get_beatmaps", "h={}&a=1&m=1".format(md5))
|
||||
dataCtb = osuapiHelper.osuApiRequest("get_beatmaps", "h={}&a=1&m=2".format(md5))
|
||||
dataMania = osuapiHelper.osuApiRequest("get_beatmaps", "h={}&a=1&m=3".format(md5))
|
||||
if dataStd is not None:
|
||||
mainData = dataStd
|
||||
elif dataTaiko is not None:
|
||||
mainData = dataTaiko
|
||||
elif dataCtb is not None:
|
||||
mainData = dataCtb
|
||||
elif dataMania is not None:
|
||||
mainData = dataMania
|
||||
|
||||
# If the beatmap is frozen and still valid from osu!api, return True so we don't overwrite anything
|
||||
if mainData is not None and self.rankedStatusFrozen == 1:
|
||||
return True
|
||||
|
||||
# Can't fint beatmap by MD5. The beatmap has been updated. Check with beatmap set ID
|
||||
if mainData is None:
|
||||
log.debug("osu!api data is None")
|
||||
dataStd = osuapiHelper.osuApiRequest("get_beatmaps", "s={}&a=1&m=0".format(beatmapSetID))
|
||||
dataTaiko = osuapiHelper.osuApiRequest("get_beatmaps", "s={}&a=1&m=1".format(beatmapSetID))
|
||||
dataCtb = osuapiHelper.osuApiRequest("get_beatmaps", "s={}&a=1&m=2".format(beatmapSetID))
|
||||
dataMania = osuapiHelper.osuApiRequest("get_beatmaps", "s={}&a=1&m=3".format(beatmapSetID))
|
||||
if dataStd is not None:
|
||||
mainData = dataStd
|
||||
elif dataTaiko is not None:
|
||||
mainData = dataTaiko
|
||||
elif dataCtb is not None:
|
||||
mainData = dataCtb
|
||||
elif dataMania is not None:
|
||||
mainData = dataMania
|
||||
|
||||
if mainData is None:
|
||||
# Still no data, beatmap is not submitted
|
||||
return False
|
||||
else:
|
||||
# We have some data, but md5 doesn't match. Beatmap is outdated
|
||||
self.rankedStatus = rankedStatuses.NEED_UPDATE
|
||||
return True
|
||||
|
||||
|
||||
# We have data from osu!api, set beatmap data
|
||||
log.debug("Got beatmap data from osu!api")
|
||||
self.songName = "{} - {} [{}]".format(mainData["artist"], mainData["title"], mainData["version"])
|
||||
self.fileMD5 = md5
|
||||
self.rankedStatus = convertRankedStatus(int(mainData["approved"]))
|
||||
self.beatmapID = int(mainData["beatmap_id"])
|
||||
self.beatmapSetID = int(mainData["beatmapset_id"])
|
||||
self.AR = float(mainData["diff_approach"])
|
||||
self.OD = float(mainData["diff_overall"])
|
||||
|
||||
# Determine stars for every mode
|
||||
self.starsStd = 0.0
|
||||
self.starsTaiko = 0.0
|
||||
self.starsCtb = 0.0
|
||||
self.starsMania = 0.0
|
||||
if dataStd is not None:
|
||||
self.starsStd = float(dataStd["difficultyrating"])
|
||||
if dataTaiko is not None:
|
||||
self.starsTaiko = float(dataTaiko["difficultyrating"])
|
||||
if dataCtb is not None:
|
||||
self.starsCtb = float(dataCtb["difficultyrating"])
|
||||
if dataMania is not None:
|
||||
self.starsMania = float(dataMania["difficultyrating"])
|
||||
|
||||
self.maxCombo = int(mainData["max_combo"]) if mainData["max_combo"] is not None else 0
|
||||
self.hitLength = int(mainData["hit_length"])
|
||||
if mainData["bpm"] is not None:
|
||||
self.bpm = int(float(mainData["bpm"]))
|
||||
else:
|
||||
self.bpm = -1
|
||||
return True
|
||||
|
||||
def setData(self, md5, beatmapSetID):
|
||||
"""
|
||||
Set this object's beatmap data from highest level possible.
|
||||
|
||||
md5 -- beatmap MD5
|
||||
beatmapSetID -- beatmap set ID
|
||||
"""
|
||||
# Get beatmap from db
|
||||
dbResult = self.setDataFromDB(md5)
|
||||
|
||||
# Force refresh from osu api.
|
||||
# We get data before to keep frozen maps ranked
|
||||
# if they haven't been updated
|
||||
if dbResult and self.refresh:
|
||||
dbResult = False
|
||||
|
||||
if not dbResult:
|
||||
log.debug("Beatmap not found in db")
|
||||
# If this beatmap is not in db, get it from osu!api
|
||||
apiResult = self.setDataFromOsuApi(md5, beatmapSetID)
|
||||
if not apiResult:
|
||||
# If it's not even in osu!api, this beatmap is not submitted
|
||||
self.rankedStatus = rankedStatuses.NOT_SUBMITTED
|
||||
elif self.rankedStatus != rankedStatuses.NOT_SUBMITTED and self.rankedStatus != rankedStatuses.NEED_UPDATE:
|
||||
# We get beatmap data from osu!api, save it in db
|
||||
self.addBeatmapToDB()
|
||||
else:
|
||||
log.debug("Beatmap found in db")
|
||||
|
||||
log.debug("{}\n{}\n{}\n{}".format(self.starsStd, self.starsTaiko, self.starsCtb, self.starsMania))
|
||||
|
||||
def getData(self, totalScores=0, version=4):
|
||||
"""
|
||||
Return this beatmap's data (header) for getscores
|
||||
|
||||
return -- beatmap header for getscores
|
||||
"""
|
||||
# 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
|
||||
data += "|{}|{}|{}\n{}\n{}\n{}\n".format(self.beatmapID, self.beatmapSetID, totalScores, self.offset, self.songName, self.rating)
|
||||
|
||||
# Return the header
|
||||
return data
|
||||
|
||||
def getCachedTillerinoPP(self):
|
||||
"""
|
||||
Returned cached pp values for 100, 99, 98 and 95 acc nomod
|
||||
(used ONLY with Tillerino, pp is always calculated with oppai when submitting scores)
|
||||
|
||||
return -- list with pp values. [0,0,0,0] if not cached.
|
||||
"""
|
||||
data = glob.db.fetch("SELECT pp_100, pp_99, pp_98, pp_95 FROM beatmaps WHERE beatmap_md5 = %s LIMIT 1", [self.fileMD5])
|
||||
if data is None:
|
||||
return [0,0,0,0]
|
||||
return [data["pp_100"], data["pp_99"], data["pp_98"], data["pp_95"]]
|
||||
|
||||
def saveCachedTillerinoPP(self, l):
|
||||
"""
|
||||
Save cached pp for tillerino
|
||||
|
||||
l -- list with 4 default pp values ([100,99,98,95])
|
||||
"""
|
||||
glob.db.execute("UPDATE beatmaps SET pp_100 = %s, pp_99 = %s, pp_98 = %s, pp_95 = %s WHERE beatmap_md5 = %s", [l[0], l[1], l[2], l[3], self.fileMD5])
|
||||
|
||||
@property
|
||||
def is_rankable(self):
|
||||
return self.rankedStatus >= rankedStatuses.RANKED and self.rankedStatus != rankedStatuses.UNKNOWN
|
||||
|
||||
def convertRankedStatus(approvedStatus):
|
||||
"""
|
||||
Convert approved_status (from osu!api) to ranked status (for getscores)
|
||||
|
||||
approvedStatus -- approved status, from osu!api
|
||||
return -- rankedStatus for getscores
|
||||
"""
|
||||
|
||||
approvedStatus = int(approvedStatus)
|
||||
if approvedStatus <= 0:
|
||||
return rankedStatuses.PENDING
|
||||
elif approvedStatus == 1:
|
||||
return rankedStatuses.RANKED
|
||||
elif approvedStatus == 2:
|
||||
return rankedStatuses.APPROVED
|
||||
elif approvedStatus == 3:
|
||||
return rankedStatuses.QUALIFIED
|
||||
elif approvedStatus == 4:
|
||||
return rankedStatuses.LOVED
|
||||
else:
|
||||
return rankedStatuses.UNKNOWN
|
||||
|
||||
def incrementPlaycount(md5, passed):
|
||||
"""
|
||||
Increment playcount (and passcount) for a beatmap
|
||||
|
||||
md5 -- beatmap md5
|
||||
passed -- if True, increment passcount too
|
||||
"""
|
||||
glob.db.execute("UPDATE beatmaps SET playcount = playcount+1 WHERE beatmap_md5 = %s LIMIT 1", [md5])
|
||||
if passed:
|
||||
glob.db.execute("UPDATE beatmaps SET passcount = passcount+1 WHERE beatmap_md5 = %s LIMIT 1", [md5])
|
34
objects/glob.py
Normal file
34
objects/glob.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import personalBestCache
|
||||
import userStatsCache
|
||||
from common.ddog import datadogClient
|
||||
from common.files import fileBuffer, fileLocks
|
||||
from common.web import schiavo
|
||||
|
||||
try:
|
||||
with open("version") as f:
|
||||
VERSION = f.read().strip()
|
||||
except:
|
||||
VERSION = "Unknown"
|
||||
ACHIEVEMENTS_VERSION = 1
|
||||
|
||||
DATADOG_PREFIX = "lets"
|
||||
BOT_NAME = "Charlotte"
|
||||
db = None
|
||||
redis = None
|
||||
conf = None
|
||||
application = None
|
||||
pool = None
|
||||
pascoa = {}
|
||||
|
||||
busyThreads = 0
|
||||
debug = False
|
||||
sentry = False
|
||||
|
||||
# Cache and objects
|
||||
fLocks = fileLocks.fileLocks()
|
||||
userStatsCache = userStatsCache.userStatsCache()
|
||||
personalBestCache = personalBestCache.personalBestCache()
|
||||
fileBuffers = fileBuffer.buffersList()
|
||||
dog = datadogClient.datadogClient()
|
||||
schiavo = schiavo.schiavo()
|
||||
achievementClasses = {}
|
241
objects/relaxboard.pyx
Normal file
241
objects/relaxboard.pyx
Normal file
@@ -0,0 +1,241 @@
|
||||
from objects import rxscore
|
||||
from common.ripple import userUtils
|
||||
from constants import rankedStatuses
|
||||
from common.constants import mods as modsEnum
|
||||
from common.constants import privileges
|
||||
from objects import glob
|
||||
|
||||
|
||||
class scoreboard:
|
||||
def __init__(self, username, gameMode, beatmap, setScores = True, country = False, friends = False, mods = -1):
|
||||
"""
|
||||
Initialize a leaderboard object
|
||||
|
||||
username -- username of who's requesting the scoreboard. None if not known
|
||||
gameMode -- requested gameMode
|
||||
beatmap -- beatmap objecy relative to this leaderboard
|
||||
setScores -- if True, will get personal/top 50 scores automatically. Optional. Default: True
|
||||
"""
|
||||
self.scores = [] # list containing all top 50 scores objects. First object is personal best
|
||||
self.totalScores = 0
|
||||
self.personalBestRank = -1 # our personal best rank, -1 if not found yet
|
||||
self.username = username # username of who's requesting the scoreboard. None if not known
|
||||
self.userID = userUtils.getID(self.username) # username's userID
|
||||
self.gameMode = gameMode # requested gameMode
|
||||
self.beatmap = beatmap # beatmap objecy relative to this leaderboard
|
||||
self.country = country
|
||||
self.friends = friends
|
||||
self.mods = mods
|
||||
if setScores:
|
||||
self.setScores()
|
||||
|
||||
def setScores(self):
|
||||
"""
|
||||
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
|
||||
self.scores = []
|
||||
self.scores.append(-1)
|
||||
|
||||
# Make sure the beatmap is ranked
|
||||
if self.beatmap.rankedStatus < rankedStatuses.RANKED:
|
||||
return
|
||||
|
||||
# Query parts
|
||||
cdef str select = ""
|
||||
cdef str joins = ""
|
||||
cdef str country = ""
|
||||
cdef str mods = ""
|
||||
cdef str friends = ""
|
||||
cdef str order = ""
|
||||
cdef str limit = ""
|
||||
|
||||
# Find personal best score
|
||||
if self.userID != 0:
|
||||
# Query parts
|
||||
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
|
||||
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
|
||||
if personalBestScore is not None:
|
||||
s = rxscore.score(personalBestScore["id"])
|
||||
self.scores[0] = s
|
||||
else:
|
||||
# No personal best
|
||||
self.scores[0] = -1
|
||||
|
||||
# Get top 50 scores
|
||||
select = "SELECT *"
|
||||
joins = "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.beatmap_md5 = %(beatmap_md5)s AND scores_relax.play_mode = %(play_mode)s AND scores_relax.completed = 3 AND (users.privileges & 1 > 0 OR users.id = %(userid)s)"
|
||||
|
||||
# Country ranking
|
||||
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)"
|
||||
else:
|
||||
country = ""
|
||||
|
||||
# Mods ranking (ignore auto, since we use it for pp sorting)
|
||||
if self.mods > -1:
|
||||
mods = "AND scores_relax.mods = %(mods)s"
|
||||
else:
|
||||
mods = ""
|
||||
|
||||
# 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 = ""
|
||||
|
||||
order = "ORDER BY pp DESC"
|
||||
|
||||
if isPremium: # Premium members can see up to 100 scores on leaderboards
|
||||
limit = "LIMIT 100"
|
||||
else:
|
||||
limit = "LIMIT 50"
|
||||
|
||||
# Build query, get params and run query
|
||||
query = buildQuery(locals())
|
||||
params = {"beatmap_md5": self.beatmap.fileMD5, "play_mode": self.gameMode, "userid": self.userID, "mods": self.mods}
|
||||
topScores = glob.db.fetchAll(query, params)
|
||||
|
||||
# Set data for all scores
|
||||
cdef int c = 1
|
||||
cdef dict topScore
|
||||
if topScores is not None:
|
||||
for topScore in topScores:
|
||||
# Create score object
|
||||
s = rxscore.score(topScore["id"], setData=False)
|
||||
|
||||
# Set data and rank from topScores's row
|
||||
s.setDataFromDict(topScore)
|
||||
s.setRank(c)
|
||||
|
||||
# Check if this top 50 score is our personal best
|
||||
if s.playerName == self.username:
|
||||
self.personalBestRank = c
|
||||
|
||||
# Add this score to scores list and increment rank
|
||||
self.scores.append(s)
|
||||
c+=1
|
||||
|
||||
'''# If we have more than 50 scores, run query to get scores count
|
||||
if c >= 50:
|
||||
# Count all scores on this map
|
||||
select = "SELECT COUNT(*) AS count"
|
||||
limit = "LIMIT 1"
|
||||
|
||||
# Build query, get params and run query
|
||||
query = buildQuery(locals())
|
||||
count = glob.db.fetch(query, params)
|
||||
if count == None:
|
||||
self.totalScores = 0
|
||||
else:
|
||||
self.totalScores = count["count"]
|
||||
else:
|
||||
self.totalScores = c-1'''
|
||||
|
||||
# If personal best score was not in top 50, try to get it from cache
|
||||
if personalBestScore is not None and self.personalBestRank < 1:
|
||||
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
|
||||
if personalBestScore is not None and self.personalBestRank < 1:
|
||||
self.setPersonalBest()
|
||||
|
||||
# Cache our personal best rank so we can eventually use it later as
|
||||
# before personal best rank" in submit modular when building ranking panel
|
||||
if self.personalBestRank >= 1:
|
||||
glob.personalBestCache.set(self.userID, self.personalBestRank, self.beatmap.fileMD5)
|
||||
|
||||
def setPersonalBest(self):
|
||||
"""
|
||||
Set personal best rank ONLY
|
||||
Ikr, that query is HUGE but xd
|
||||
"""
|
||||
# Before running the HUGE query, make sure we have a score on that map
|
||||
cdef str query = "SELECT id FROM scores_relax WHERE beatmap_md5 = %(md5)s AND userid = %(userid)s AND play_mode = %(mode)s AND completed = 3"
|
||||
# Mods
|
||||
if self.mods > -1:
|
||||
query += " AND scores_relax.mods = %(mods)s"
|
||||
# Friends ranking
|
||||
if self.friends:
|
||||
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
|
||||
query += " LIMIT 1"
|
||||
hasScore = glob.db.fetch(query, {"md5": self.beatmap.fileMD5, "userid": self.userID, "mode": self.gameMode, "mods": self.mods})
|
||||
if hasScore is None:
|
||||
return
|
||||
|
||||
|
||||
# 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.pp >= (
|
||||
SELECT pp 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"""
|
||||
# Country
|
||||
if self.country:
|
||||
query += " AND users_stats.country = (SELECT country FROM users_stats WHERE id = %(userid)s LIMIT 1)"
|
||||
# Mods
|
||||
if self.mods > -1:
|
||||
query += " AND scores_relax.mods = %(mods)s"
|
||||
# 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)"
|
||||
# Sort and limit at the end
|
||||
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})
|
||||
if result is not None:
|
||||
self.personalBestRank = result["rank"]
|
||||
|
||||
def getScoresData(self):
|
||||
"""
|
||||
Return scores data for getscores
|
||||
|
||||
return -- score data in getscores format
|
||||
"""
|
||||
data = ""
|
||||
|
||||
# Output personal best
|
||||
if self.scores[0] == -1:
|
||||
# We don't have a personal best score
|
||||
data += "\n"
|
||||
else:
|
||||
# Set personal best score rank
|
||||
self.setPersonalBest() # sets self.personalBestRank with the huge query
|
||||
self.scores[0].setRank(self.personalBestRank)
|
||||
data += self.scores[0].getData()
|
||||
|
||||
# Output top 50 scores
|
||||
for i in self.scores[1:]:
|
||||
data += i.getData(pp=1)
|
||||
return data
|
274
objects/rxscore.pyx
Normal file
274
objects/rxscore.pyx
Normal file
@@ -0,0 +1,274 @@
|
||||
import time
|
||||
|
||||
from objects import beatmap
|
||||
from common.constants import gameModes
|
||||
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 rxoppai
|
||||
from pp import wifipiano2
|
||||
from pp import cicciobello
|
||||
|
||||
|
||||
class score:
|
||||
PP_CALCULATORS = {
|
||||
gameModes.STD: rxoppai.oppai,
|
||||
gameModes.TAIKO: rippoppai.oppai,
|
||||
gameModes.CTB: cicciobello.Cicciobello,
|
||||
gameModes.MANIA: wifipiano2.piano
|
||||
}
|
||||
__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"]
|
||||
def __init__(self, scoreID = None, rank = None, setData = True):
|
||||
"""
|
||||
Initialize a (empty) score object.
|
||||
|
||||
scoreID -- score ID, used to get score data from db. Optional.
|
||||
rank -- score rank. Optional
|
||||
setData -- if True, set score data from db using scoreID. Optional.
|
||||
"""
|
||||
self.scoreID = 0
|
||||
self.playerName = "nospe"
|
||||
self.score = 0
|
||||
self.maxCombo = 0
|
||||
self.c50 = 0
|
||||
self.c100 = 0
|
||||
self.c300 = 0
|
||||
self.cMiss = 0
|
||||
self.cKatu = 0
|
||||
self.cGeki = 0
|
||||
self.fullCombo = False
|
||||
self.mods = 0
|
||||
self.playerUserID = 0
|
||||
self.rank = rank # can be empty string too
|
||||
self.date = 0
|
||||
self.hasReplay = 0
|
||||
|
||||
self.fileMd5 = None
|
||||
self.passed = False
|
||||
self.playDateTime = 0
|
||||
self.gameMode = 0
|
||||
self.completed = 0
|
||||
|
||||
self.accuracy = 0.00
|
||||
|
||||
self.pp = 0.00
|
||||
|
||||
self.oldPersonalBest = 0
|
||||
self.rankedScoreIncrease = 0
|
||||
|
||||
if scoreID is not None and setData == True:
|
||||
self.setDataFromDB(scoreID, rank)
|
||||
|
||||
def calculateAccuracy(self):
|
||||
"""
|
||||
Calculate and set accuracy for that score
|
||||
"""
|
||||
if self.gameMode == 0:
|
||||
# std
|
||||
totalPoints = self.c50*50+self.c100*100+self.c300*300
|
||||
totalHits = self.c300+self.c100+self.c50+self.cMiss
|
||||
if totalHits == 0:
|
||||
self.accuracy = 1
|
||||
else:
|
||||
self.accuracy = totalPoints/(totalHits*300)
|
||||
elif self.gameMode == 1:
|
||||
# taiko
|
||||
totalPoints = (self.c100*50)+(self.c300*100)
|
||||
totalHits = self.cMiss+self.c100+self.c300
|
||||
if totalHits == 0:
|
||||
self.accuracy = 1
|
||||
else:
|
||||
self.accuracy = totalPoints / (totalHits * 100)
|
||||
elif self.gameMode == 2:
|
||||
# ctb
|
||||
fruits = self.c300+self.c100+self.c50
|
||||
totalFruits = fruits+self.cMiss+self.cKatu
|
||||
if totalFruits == 0:
|
||||
self.accuracy = 1
|
||||
else:
|
||||
self.accuracy = fruits / totalFruits
|
||||
elif self.gameMode == 3:
|
||||
# mania
|
||||
totalPoints = self.c50*50+self.c100*100+self.cKatu*200+self.c300*300+self.cGeki*300
|
||||
totalHits = self.cMiss+self.c50+self.c100+self.c300+self.cGeki+self.cKatu
|
||||
self.accuracy = totalPoints / (totalHits * 300)
|
||||
else:
|
||||
# unknown gamemode
|
||||
self.accuracy = 0
|
||||
|
||||
def setRank(self, rank):
|
||||
"""
|
||||
Force a score rank
|
||||
|
||||
rank -- new score rank
|
||||
"""
|
||||
self.rank = rank
|
||||
|
||||
def setDataFromDB(self, scoreID, rank = None):
|
||||
"""
|
||||
Set this object's score data from db
|
||||
Sets playerUserID too
|
||||
|
||||
scoreID -- score ID
|
||||
rank -- rank in scoreboard. Optional.
|
||||
"""
|
||||
data = glob.db.fetch("SELECT scores_relax.*, users.username FROM scores_relax LEFT JOIN users ON users.id = scores_relax.userid WHERE scores_relax.id = %s LIMIT 1", [scoreID])
|
||||
if data is not None:
|
||||
self.setDataFromDict(data, rank)
|
||||
|
||||
def setDataFromDict(self, data, rank = None):
|
||||
"""
|
||||
Set this object's score data from dictionary
|
||||
Doesn't set playerUserID
|
||||
|
||||
data -- score dictionarty
|
||||
rank -- rank in scoreboard. Optional.
|
||||
"""
|
||||
#print(str(data))
|
||||
self.scoreID = data["id"]
|
||||
if "username" in data:
|
||||
self.playerName = userUtils.getClan(data["userid"])
|
||||
else:
|
||||
self.playerName = userUtils.getUsername(data["userid"])
|
||||
self.playerUserID = data["userid"]
|
||||
self.score = data["score"]
|
||||
self.maxCombo = data["max_combo"]
|
||||
self.gameMode = data["play_mode"]
|
||||
self.c50 = data["50_count"]
|
||||
self.c100 = data["100_count"]
|
||||
self.c300 = data["300_count"]
|
||||
self.cMiss = data["misses_count"]
|
||||
self.cKatu = data["katus_count"]
|
||||
self.cGeki = data["gekis_count"]
|
||||
self.fullCombo = True if data["full_combo"] == 1 else False
|
||||
self.mods = data["mods"]
|
||||
self.rank = rank if rank is not None else ""
|
||||
self.date = data["time"]
|
||||
self.fileMd5 = data["beatmap_md5"]
|
||||
self.completed = data["completed"]
|
||||
#if "pp" in data:
|
||||
self.pp = data["pp"]
|
||||
self.calculateAccuracy()
|
||||
|
||||
def setDataFromScoreData(self, scoreData):
|
||||
"""
|
||||
Set this object's score data from scoreData list (submit modular)
|
||||
|
||||
scoreData -- scoreData list
|
||||
"""
|
||||
if len(scoreData) >= 16:
|
||||
self.fileMd5 = scoreData[0]
|
||||
self.playerName = scoreData[1].strip()
|
||||
# %s%s%s = scoreData[2]
|
||||
self.c300 = int(scoreData[3])
|
||||
self.c100 = int(scoreData[4])
|
||||
self.c50 = int(scoreData[5])
|
||||
self.cGeki = int(scoreData[6])
|
||||
self.cKatu = int(scoreData[7])
|
||||
self.cMiss = int(scoreData[8])
|
||||
self.score = int(scoreData[9])
|
||||
self.maxCombo = int(scoreData[10])
|
||||
self.fullCombo = True if scoreData[11] == 'True' else False
|
||||
#self.rank = scoreData[12]
|
||||
self.mods = int(scoreData[13])
|
||||
self.passed = True if scoreData[14] == 'True' else False
|
||||
self.gameMode = int(scoreData[15])
|
||||
#self.playDateTime = int(scoreData[16])
|
||||
self.playDateTime = int(time.time())
|
||||
self.calculateAccuracy()
|
||||
#osuVersion = scoreData[17]
|
||||
self.calculatePP()
|
||||
# Set completed status
|
||||
self.setCompletedStatus()
|
||||
|
||||
|
||||
def getData(self, pp=True):
|
||||
"""Return score row relative to this score for getscores"""
|
||||
return "{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|1\n".format(
|
||||
self.scoreID,
|
||||
self.playerName,
|
||||
int(self.pp) if pp else self.score,
|
||||
self.maxCombo,
|
||||
self.c50,
|
||||
self.c100,
|
||||
self.c300,
|
||||
self.cMiss,
|
||||
self.cKatu,
|
||||
self.cGeki,
|
||||
self.fullCombo,
|
||||
self.mods,
|
||||
self.playerUserID,
|
||||
self.rank,
|
||||
self.date)
|
||||
|
||||
def setCompletedStatus(self):
|
||||
"""
|
||||
Set this score completed status and rankedScoreIncrease
|
||||
"""
|
||||
self.completed = 0
|
||||
if self.passed == True and scoreUtils.isRankable(self.mods):
|
||||
# Get userID
|
||||
userID = userUtils.getID(self.playerName)
|
||||
|
||||
# Make sure we don't have another score identical to this one
|
||||
duplicate = glob.db.fetch("SELECT id FROM scores_relax WHERE userid = %s AND beatmap_md5 = %s AND play_mode = %s AND time = %s AND score = %s LIMIT 1", [userID, self.fileMd5, self.gameMode, self.date, self.score])
|
||||
if duplicate is not None:
|
||||
# Found same score in db. Don't save this score.
|
||||
self.completed = -1
|
||||
return
|
||||
|
||||
# No duplicates found.
|
||||
# Get right "completed" value
|
||||
personalBest = glob.db.fetch("SELECT id, pp, score FROM scores_relax WHERE userid = %s AND beatmap_md5 = %s AND play_mode = %s AND completed = 3 LIMIT 1", [userID, self.fileMd5, self.gameMode])
|
||||
if personalBest is None:
|
||||
# This is our first score on this map, so it's our best score
|
||||
self.completed = 3
|
||||
self.rankedScoreIncrease = self.score
|
||||
self.oldPersonalBest = 0
|
||||
else:
|
||||
# Compare personal best's score with current score
|
||||
if self.pp > personalBest["pp"]:
|
||||
# New best score
|
||||
self.completed = 3
|
||||
self.rankedScoreIncrease = self.score-personalBest["score"]
|
||||
self.oldPersonalBest = personalBest["id"]
|
||||
else:
|
||||
self.completed = 2
|
||||
self.rankedScoreIncrease = 0
|
||||
self.oldPersonalBest = 0
|
||||
|
||||
log.info("Completed status: {}".format(self.completed))
|
||||
|
||||
def saveScoreInDB(self):
|
||||
"""
|
||||
Save this score in DB (if passed and mods are valid)
|
||||
"""
|
||||
# Add this score
|
||||
if self.completed >= 2:
|
||||
query = "INSERT INTO scores_relax (id, beatmap_md5, userid, score, max_combo, full_combo, mods, 300_count, 100_count, 50_count, katus_count, gekis_count, misses_count, time, play_mode, completed, accuracy, pp) VALUES (NULL, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);"
|
||||
self.scoreID = int(glob.db.execute(query, [self.fileMd5, userUtils.getID(self.playerName), self.score, self.maxCombo, 1 if self.fullCombo == True else 0, self.mods, self.c300, self.c100, self.c50, self.cKatu, self.cGeki, self.cMiss, self.playDateTime, self.gameMode, self.completed, self.accuracy * 100, self.pp]))
|
||||
|
||||
# Set old personal best to completed = 2
|
||||
if self.oldPersonalBest != 0:
|
||||
glob.db.execute("UPDATE scores_relax SET completed = 2 WHERE id = %s", [self.oldPersonalBest])
|
||||
|
||||
def calculatePP(self, b = None):
|
||||
"""
|
||||
Calculate this score's pp value if completed == 3
|
||||
"""
|
||||
# Create beatmap object
|
||||
if b is None:
|
||||
b = beatmap.beatmap(self.fileMd5, 0)
|
||||
|
||||
# Calculate pp
|
||||
if b.rankedStatus >= rankedStatuses.RANKED and b.rankedStatus != rankedStatuses.LOVED 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)
|
||||
self.pp = calculator.pp
|
||||
else:
|
||||
self.pp = 0
|
273
objects/score.pyx
Normal file
273
objects/score.pyx
Normal file
@@ -0,0 +1,273 @@
|
||||
import time
|
||||
|
||||
from objects import beatmap
|
||||
from common.constants import gameModes
|
||||
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 wifipiano2
|
||||
from pp import cicciobello
|
||||
|
||||
|
||||
class score:
|
||||
PP_CALCULATORS = {
|
||||
gameModes.STD: rippoppai.oppai,
|
||||
gameModes.TAIKO: rippoppai.oppai,
|
||||
gameModes.CTB: cicciobello.Cicciobello,
|
||||
gameModes.MANIA: wifipiano2.piano
|
||||
}
|
||||
__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"]
|
||||
def __init__(self, scoreID = None, rank = None, setData = True):
|
||||
"""
|
||||
Initialize a (empty) score object.
|
||||
|
||||
scoreID -- score ID, used to get score data from db. Optional.
|
||||
rank -- score rank. Optional
|
||||
setData -- if True, set score data from db using scoreID. Optional.
|
||||
"""
|
||||
self.scoreID = 0
|
||||
self.playerName = "nospe"
|
||||
self.score = 0
|
||||
self.maxCombo = 0
|
||||
self.c50 = 0
|
||||
self.c100 = 0
|
||||
self.c300 = 0
|
||||
self.cMiss = 0
|
||||
self.cKatu = 0
|
||||
self.cGeki = 0
|
||||
self.fullCombo = False
|
||||
self.mods = 0
|
||||
self.playerUserID = 0
|
||||
self.rank = rank # can be empty string too
|
||||
self.date = 0
|
||||
self.hasReplay = 0
|
||||
|
||||
self.fileMd5 = None
|
||||
self.passed = False
|
||||
self.playDateTime = 0
|
||||
self.gameMode = 0
|
||||
self.completed = 0
|
||||
|
||||
self.accuracy = 0.00
|
||||
|
||||
self.pp = 0.00
|
||||
|
||||
self.oldPersonalBest = 0
|
||||
self.rankedScoreIncrease = 0
|
||||
|
||||
if scoreID is not None and setData == True:
|
||||
self.setDataFromDB(scoreID, rank)
|
||||
|
||||
def calculateAccuracy(self):
|
||||
"""
|
||||
Calculate and set accuracy for that score
|
||||
"""
|
||||
if self.gameMode == 0:
|
||||
# std
|
||||
totalPoints = self.c50*50+self.c100*100+self.c300*300
|
||||
totalHits = self.c300+self.c100+self.c50+self.cMiss
|
||||
if totalHits == 0:
|
||||
self.accuracy = 1
|
||||
else:
|
||||
self.accuracy = totalPoints/(totalHits*300)
|
||||
elif self.gameMode == 1:
|
||||
# taiko
|
||||
totalPoints = (self.c100*50)+(self.c300*100)
|
||||
totalHits = self.cMiss+self.c100+self.c300
|
||||
if totalHits == 0:
|
||||
self.accuracy = 1
|
||||
else:
|
||||
self.accuracy = totalPoints / (totalHits * 100)
|
||||
elif self.gameMode == 2:
|
||||
# ctb
|
||||
fruits = self.c300+self.c100+self.c50
|
||||
totalFruits = fruits+self.cMiss+self.cKatu
|
||||
if totalFruits == 0:
|
||||
self.accuracy = 1
|
||||
else:
|
||||
self.accuracy = fruits / totalFruits
|
||||
elif self.gameMode == 3:
|
||||
# mania
|
||||
totalPoints = self.c50*50+self.c100*100+self.cKatu*200+self.c300*300+self.cGeki*300
|
||||
totalHits = self.cMiss+self.c50+self.c100+self.c300+self.cGeki+self.cKatu
|
||||
self.accuracy = totalPoints / (totalHits * 300)
|
||||
else:
|
||||
# unknown gamemode
|
||||
self.accuracy = 0
|
||||
|
||||
def setRank(self, rank):
|
||||
"""
|
||||
Force a score rank
|
||||
|
||||
rank -- new score rank
|
||||
"""
|
||||
self.rank = rank
|
||||
|
||||
def setDataFromDB(self, scoreID, rank = None):
|
||||
"""
|
||||
Set this object's score data from db
|
||||
Sets playerUserID too
|
||||
|
||||
scoreID -- score ID
|
||||
rank -- rank in scoreboard. Optional.
|
||||
"""
|
||||
data = glob.db.fetch("SELECT scores.*, users.username FROM scores LEFT JOIN users ON users.id = scores.userid WHERE scores.id = %s LIMIT 1", [scoreID])
|
||||
if data is not None:
|
||||
self.setDataFromDict(data, rank)
|
||||
|
||||
def setDataFromDict(self, data, rank = None):
|
||||
"""
|
||||
Set this object's score data from dictionary
|
||||
Doesn't set playerUserID
|
||||
|
||||
data -- score dictionarty
|
||||
rank -- rank in scoreboard. Optional.
|
||||
"""
|
||||
#print(str(data))
|
||||
self.scoreID = data["id"]
|
||||
if "username" in data:
|
||||
self.playerName = userUtils.getClan(data["userid"])
|
||||
else:
|
||||
self.playerName = userUtils.getUsername(data["userid"])
|
||||
self.playerUserID = data["userid"]
|
||||
self.score = data["score"]
|
||||
self.maxCombo = data["max_combo"]
|
||||
self.gameMode = data["play_mode"]
|
||||
self.c50 = data["50_count"]
|
||||
self.c100 = data["100_count"]
|
||||
self.c300 = data["300_count"]
|
||||
self.cMiss = data["misses_count"]
|
||||
self.cKatu = data["katus_count"]
|
||||
self.cGeki = data["gekis_count"]
|
||||
self.fullCombo = True if data["full_combo"] == 1 else False
|
||||
self.mods = data["mods"]
|
||||
self.rank = rank if rank is not None else ""
|
||||
self.date = data["time"]
|
||||
self.fileMd5 = data["beatmap_md5"]
|
||||
self.completed = data["completed"]
|
||||
#if "pp" in data:
|
||||
self.pp = data["pp"]
|
||||
self.calculateAccuracy()
|
||||
|
||||
def setDataFromScoreData(self, scoreData):
|
||||
"""
|
||||
Set this object's score data from scoreData list (submit modular)
|
||||
|
||||
scoreData -- scoreData list
|
||||
"""
|
||||
if len(scoreData) >= 16:
|
||||
self.fileMd5 = scoreData[0]
|
||||
self.playerName = scoreData[1].strip()
|
||||
# %s%s%s = scoreData[2]
|
||||
self.c300 = int(scoreData[3])
|
||||
self.c100 = int(scoreData[4])
|
||||
self.c50 = int(scoreData[5])
|
||||
self.cGeki = int(scoreData[6])
|
||||
self.cKatu = int(scoreData[7])
|
||||
self.cMiss = int(scoreData[8])
|
||||
self.score = int(scoreData[9])
|
||||
self.maxCombo = int(scoreData[10])
|
||||
self.fullCombo = True if scoreData[11] == 'True' else False
|
||||
#self.rank = scoreData[12]
|
||||
self.mods = int(scoreData[13])
|
||||
self.passed = True if scoreData[14] == 'True' else False
|
||||
self.gameMode = int(scoreData[15])
|
||||
#self.playDateTime = int(scoreData[16])
|
||||
self.playDateTime = int(time.time())
|
||||
self.calculateAccuracy()
|
||||
#osuVersion = scoreData[17]
|
||||
|
||||
# Set completed status
|
||||
self.setCompletedStatus()
|
||||
|
||||
|
||||
def getData(self, pp=False):
|
||||
"""Return score row relative to this score for getscores"""
|
||||
return "{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|1\n".format(
|
||||
self.scoreID,
|
||||
self.playerName,
|
||||
int(self.pp) if pp else self.score,
|
||||
self.maxCombo,
|
||||
self.c50,
|
||||
self.c100,
|
||||
self.c300,
|
||||
self.cMiss,
|
||||
self.cKatu,
|
||||
self.cGeki,
|
||||
self.fullCombo,
|
||||
self.mods,
|
||||
self.playerUserID,
|
||||
self.rank,
|
||||
self.date)
|
||||
|
||||
def setCompletedStatus(self):
|
||||
"""
|
||||
Set this score completed status and rankedScoreIncrease
|
||||
"""
|
||||
self.completed = 0
|
||||
if self.passed == True and scoreUtils.isRankable(self.mods):
|
||||
# Get userID
|
||||
userID = userUtils.getID(self.playerName)
|
||||
|
||||
# Make sure we don't have another score identical to this one
|
||||
duplicate = glob.db.fetch("SELECT id FROM scores WHERE userid = %s AND beatmap_md5 = %s AND play_mode = %s AND score = %s LIMIT 1", [userID, self.fileMd5, self.gameMode, self.score])
|
||||
if duplicate is not None:
|
||||
# Found same score in db. Don't save this score.
|
||||
self.completed = -1
|
||||
return
|
||||
|
||||
# No duplicates found.
|
||||
# Get right "completed" value
|
||||
personalBest = glob.db.fetch("SELECT id, score FROM scores WHERE userid = %s AND beatmap_md5 = %s AND play_mode = %s AND completed = 3 LIMIT 1", [userID, self.fileMd5, self.gameMode])
|
||||
if personalBest is None:
|
||||
# This is our first score on this map, so it's our best score
|
||||
self.completed = 3
|
||||
self.rankedScoreIncrease = self.score
|
||||
self.oldPersonalBest = 0
|
||||
else:
|
||||
# Compare personal best's score with current score
|
||||
if self.score > personalBest["score"]:
|
||||
# New best score
|
||||
self.completed = 3
|
||||
self.rankedScoreIncrease = self.score-personalBest["score"]
|
||||
self.oldPersonalBest = personalBest["id"]
|
||||
else:
|
||||
self.completed = 2
|
||||
self.rankedScoreIncrease = 0
|
||||
self.oldPersonalBest = 0
|
||||
|
||||
log.debug("Completed status: {}".format(self.completed))
|
||||
|
||||
def saveScoreInDB(self):
|
||||
"""
|
||||
Save this score in DB (if passed and mods are valid)
|
||||
"""
|
||||
# Add this score
|
||||
if self.completed >= 2:
|
||||
query = "INSERT INTO scores (id, beatmap_md5, userid, score, max_combo, full_combo, mods, 300_count, 100_count, 50_count, katus_count, gekis_count, misses_count, time, play_mode, completed, accuracy, pp) VALUES (NULL, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);"
|
||||
self.scoreID = int(glob.db.execute(query, [self.fileMd5, userUtils.getID(self.playerName), self.score, self.maxCombo, 1 if self.fullCombo == True else 0, self.mods, self.c300, self.c100, self.c50, self.cKatu, self.cGeki, self.cMiss, self.playDateTime, self.gameMode, self.completed, self.accuracy * 100, self.pp]))
|
||||
|
||||
# Set old personal best to completed = 2
|
||||
if self.oldPersonalBest != 0:
|
||||
glob.db.execute("UPDATE scores SET completed = 2 WHERE id = %s", [self.oldPersonalBest])
|
||||
|
||||
def calculatePP(self, b = None):
|
||||
"""
|
||||
Calculate this score's pp value if completed == 3
|
||||
"""
|
||||
# Create beatmap object
|
||||
if b is None:
|
||||
b = beatmap.beatmap(self.fileMd5, 0)
|
||||
|
||||
# Calculate pp
|
||||
if b.rankedStatus >= rankedStatuses.RANKED and b.rankedStatus != rankedStatuses.LOVED 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)
|
||||
self.pp = calculator.pp
|
||||
else:
|
||||
self.pp = 0
|
240
objects/scoreboard.pyx
Normal file
240
objects/scoreboard.pyx
Normal file
@@ -0,0 +1,240 @@
|
||||
from objects import score
|
||||
from common.ripple import userUtils
|
||||
from constants import rankedStatuses
|
||||
from common.constants import mods as modsEnum
|
||||
from common.constants import privileges
|
||||
from objects import glob
|
||||
|
||||
|
||||
class scoreboard:
|
||||
def __init__(self, username, gameMode, beatmap, setScores = True, country = False, friends = False, mods = -1):
|
||||
"""
|
||||
Initialize a leaderboard object
|
||||
|
||||
username -- username of who's requesting the scoreboard. None if not known
|
||||
gameMode -- requested gameMode
|
||||
beatmap -- beatmap objecy relative to this leaderboard
|
||||
setScores -- if True, will get personal/top 50 scores automatically. Optional. Default: True
|
||||
"""
|
||||
self.scores = [] # list containing all top 50 scores objects. First object is personal best
|
||||
self.totalScores = 0
|
||||
self.personalBestRank = -1 # our personal best rank, -1 if not found yet
|
||||
self.username = username # username of who's requesting the scoreboard. None if not known
|
||||
self.userID = userUtils.getID(self.username) # username's userID
|
||||
self.gameMode = gameMode # requested gameMode
|
||||
self.beatmap = beatmap # beatmap objecy relative to this leaderboard
|
||||
self.country = country
|
||||
self.friends = friends
|
||||
self.mods = mods
|
||||
if setScores:
|
||||
self.setScores()
|
||||
|
||||
|
||||
def setScores(self):
|
||||
"""
|
||||
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
|
||||
self.scores = []
|
||||
self.scores.append(-1)
|
||||
|
||||
# Make sure the beatmap is ranked
|
||||
if self.beatmap.rankedStatus < rankedStatuses.RANKED:
|
||||
return
|
||||
|
||||
# Query parts
|
||||
cdef str select = ""
|
||||
cdef str joins = ""
|
||||
cdef str country = ""
|
||||
cdef str mods = ""
|
||||
cdef str friends = ""
|
||||
cdef str order = ""
|
||||
cdef str limit = ""
|
||||
|
||||
# Find personal best score
|
||||
if self.userID != 0:
|
||||
# Query parts
|
||||
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
|
||||
if personalBestScore is not None:
|
||||
s = score.score(personalBestScore["id"])
|
||||
self.scores[0] = s
|
||||
else:
|
||||
# No personal best
|
||||
self.scores[0] = -1
|
||||
|
||||
# Get top 50 scores
|
||||
select = "SELECT *"
|
||||
joins = "FROM scores STRAIGHT_JOIN users ON scores.userid = users.id STRAIGHT_JOIN users_stats ON users.id = users_stats.id WHERE scores.beatmap_md5 = %(beatmap_md5)s AND scores.play_mode = %(play_mode)s AND scores.completed = 3 AND (users.privileges & 1 > 0 OR users.id = %(userid)s)"
|
||||
|
||||
# Country ranking
|
||||
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)"
|
||||
else:
|
||||
country = ""
|
||||
|
||||
# Mods ranking (ignore auto, since we use it for pp sorting)
|
||||
if self.mods > -1:
|
||||
mods = "AND scores.mods = %(mods)s"
|
||||
else:
|
||||
mods = ""
|
||||
|
||||
# Friends ranking
|
||||
if self.friends:
|
||||
friends = "AND (scores.userid IN (SELECT user2 FROM users_relationships WHERE user1 = %(userid)s) OR scores.userid = %(userid)s)"
|
||||
else:
|
||||
friends = ""
|
||||
|
||||
order = "ORDER BY score DESC"
|
||||
|
||||
if isPremium: # Premium members can see up to 100 scores on leaderboards
|
||||
limit = "LIMIT 100"
|
||||
else:
|
||||
limit = "LIMIT 50"
|
||||
|
||||
# Build query, get params and run query
|
||||
query = buildQuery(locals())
|
||||
params = {"beatmap_md5": self.beatmap.fileMD5, "play_mode": self.gameMode, "userid": self.userID, "mods": self.mods}
|
||||
topScores = glob.db.fetchAll(query, params)
|
||||
|
||||
# Set data for all scores
|
||||
cdef int c = 1
|
||||
cdef dict topScore
|
||||
if topScores is not None:
|
||||
for topScore in topScores:
|
||||
# Create score object
|
||||
s = score.score(topScore["id"], setData=False)
|
||||
|
||||
# Set data and rank from topScores's row
|
||||
s.setDataFromDict(topScore)
|
||||
s.setRank(c)
|
||||
|
||||
# Check if this top 50 score is our personal best
|
||||
if s.playerName == self.username:
|
||||
self.personalBestRank = c
|
||||
|
||||
# Add this score to scores list and increment rank
|
||||
self.scores.append(s)
|
||||
c+=1
|
||||
|
||||
'''# If we have more than 50 scores, run query to get scores count
|
||||
if c >= 50:
|
||||
# Count all scores on this map
|
||||
select = "SELECT COUNT(*) AS count"
|
||||
limit = "LIMIT 1"
|
||||
|
||||
# Build query, get params and run query
|
||||
query = buildQuery(locals())
|
||||
count = glob.db.fetch(query, params)
|
||||
if count == None:
|
||||
self.totalScores = 0
|
||||
else:
|
||||
self.totalScores = count["count"]
|
||||
else:
|
||||
self.totalScores = c-1'''
|
||||
|
||||
# If personal best score was not in top 50, try to get it from cache
|
||||
if personalBestScore is not None and self.personalBestRank < 1:
|
||||
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
|
||||
if personalBestScore is not None and self.personalBestRank < 1:
|
||||
self.setPersonalBest()
|
||||
|
||||
# Cache our personal best rank so we can eventually use it later as
|
||||
# before personal best rank" in submit modular when building ranking panel
|
||||
if self.personalBestRank >= 1:
|
||||
glob.personalBestCache.set(self.userID, self.personalBestRank, self.beatmap.fileMD5)
|
||||
|
||||
def setPersonalBest(self):
|
||||
"""
|
||||
Set personal best rank ONLY
|
||||
Ikr, that query is HUGE but xd
|
||||
"""
|
||||
# Before running the HUGE query, make sure we have a score on that map
|
||||
cdef str query = "SELECT id FROM scores WHERE beatmap_md5 = %(md5)s AND userid = %(userid)s AND play_mode = %(mode)s AND completed = 3"
|
||||
# Mods
|
||||
if self.mods > -1:
|
||||
query += " AND scores.mods = %(mods)s"
|
||||
# Friends ranking
|
||||
if self.friends:
|
||||
query += " AND (scores.userid IN (SELECT user2 FROM users_relationships WHERE user1 = %(userid)s) OR scores.userid = %(userid)s)"
|
||||
# Sort and limit at the end
|
||||
query += " LIMIT 1"
|
||||
hasScore = glob.db.fetch(query, {"md5": self.beatmap.fileMD5, "userid": self.userID, "mode": self.gameMode, "mods": self.mods})
|
||||
if hasScore is None:
|
||||
return
|
||||
|
||||
# We have a score, run the huge query
|
||||
# Base query
|
||||
query = """SELECT COUNT(*) AS rank FROM scores STRAIGHT_JOIN users ON scores.userid = users.id STRAIGHT_JOIN users_stats ON users.id = users_stats.id WHERE scores.score >= (
|
||||
SELECT score FROM scores WHERE beatmap_md5 = %(md5)s AND play_mode = %(mode)s AND completed = 3 AND userid = %(userid)s LIMIT 1
|
||||
) AND scores.beatmap_md5 = %(md5)s AND scores.play_mode = %(mode)s AND scores.completed = 3 AND users.privileges & 1 > 0"""
|
||||
# Country
|
||||
if self.country:
|
||||
query += " AND users_stats.country = (SELECT country FROM users_stats WHERE id = %(userid)s LIMIT 1)"
|
||||
# Mods
|
||||
if self.mods > -1:
|
||||
query += " AND scores.mods = %(mods)s"
|
||||
# Friends
|
||||
if self.friends:
|
||||
query += " AND (scores.userid IN (SELECT user2 FROM users_relationships WHERE user1 = %(userid)s) OR scores.userid = %(userid)s)"
|
||||
# Sort and limit at the end
|
||||
query += " ORDER BY score DESC LIMIT 1"
|
||||
result = glob.db.fetch(query, {"md5": self.beatmap.fileMD5, "userid": self.userID, "mode": self.gameMode, "mods": self.mods})
|
||||
if result is not None:
|
||||
self.personalBestRank = result["rank"]
|
||||
|
||||
def getScoresData(self):
|
||||
"""
|
||||
Return scores data for getscores
|
||||
|
||||
return -- score data in getscores format
|
||||
"""
|
||||
data = ""
|
||||
|
||||
# Output personal best
|
||||
if self.scores[0] == -1:
|
||||
# We don't have a personal best score
|
||||
data += "\n"
|
||||
else:
|
||||
# Set personal best score rank
|
||||
self.setPersonalBest() # sets self.personalBestRank with the huge query
|
||||
self.scores[0].setRank(self.personalBestRank)
|
||||
data += self.scores[0].getData()
|
||||
|
||||
# Output top 50 scores
|
||||
for i in self.scores[1:]:
|
||||
data += i.getData(pp=self.mods > -1 and self.mods & modsEnum.AUTOPLAY > 0)
|
||||
|
||||
return data
|
Reference in New Issue
Block a user