Initial commit

This commit is contained in:
Josh
2018-12-09 00:15:56 -05:00
commit aad3c9bb54
125 changed files with 18177 additions and 0 deletions

0
objects/__init__.py Normal file
View File

340
objects/beatmap.pyx Normal file
View 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
View 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
View 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
View 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
View 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
View 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