async def getScores(p: Player, conn: AsyncConnection) -> Optional[bytes]: isdecimal_n = partial(_isdecimal, _negative=True) # make sure all int args are integral if not all(isdecimal_n(conn.args[k]) for k in ('mods', 'v', 'm', 'i')): return b'-1|false' map_md5 = conn.args['c'] mods = int(conn.args['mods']) mode = GameMode.from_params(int(conn.args['m']), mods) map_set_id = int(conn.args['i']) rank_type = RankingType(int(conn.args['v'])) # attempt to update their stats if their # gm/gm-affecting-mods change at all. if mode != p.status.mode: p.status.mods = mods p.status.mode = mode glob.players.enqueue(packets.userStats(p)) table = mode.sql_table scoring = 'pp' if mode >= GameMode.rx_std else 'score' if not (bmap := await Beatmap.from_md5(map_md5, map_set_id)): # couldn't find in db or at osu! api by md5. # check if we have the map in our db (by filename). filename = conn.args['f'].replace('+', ' ') if not (re := regexes.mapfile.match(unquote(filename))): log(f'Requested invalid file - {filename}.', Ansi.LRED) return
def update(self, action: int, info_text: str, map_md5: str, mods: int, mode: int, map_id: int) -> None: """Fully overwrite the class with new params.""" # osu! sends both map id and md5, but # we'll only need one since we fetch a # beatmap obj from cache/sql anyways.. self.action = Action(action) self.info_text = info_text self.map_md5 = map_md5 self.mods = Mods(mods) self.mode = GameMode.from_params(mode, self.mods) self.map_id = map_id
async def from_submission(cls, data: list[str]) -> "Score": """Create a score object from an osu! submission string.""" s = cls() """ parse the following format # 0 online_checksum # 1 n300 # 2 n100 # 3 n50 # 4 ngeki # 5 nkatu # 6 nmiss # 7 score # 8 max_combo # 9 perfect # 10 grade # 11 mods # 12 passed # 13 gamemode # 14 play_time # yyMMddHHmmss # 15 osu_version + (" " * client_flags) """ s.online_checksum = data[0] s.n300, s.n100, s.n50, s.ngeki, s.nkatu, s.nmiss, s.score, s.max_combo = map( int, data[1:9], ) s.perfect = data[9] == "True" _grade = data[10] # letter grade s.mods = Mods(int(data[11])) s.passed = data[12] == "True" s.mode = GameMode.from_params(int(data[13]), s.mods) # TODO: we might want to use data[14] to get more # accurate submission time (client side) but # we'd probably want to check if it's close. s.play_time = datetime.now() s.client_flags = ClientFlags(data[15].count(" ") & ~4) s.grade = Grade.from_str(_grade) if s.passed else Grade.F return s
async def from_sql(cls, scoreid: int, scores_table: str) -> Optional['Score']: """Create a score object from sql using it's scoreid.""" # XXX: perhaps in the future this should take a gamemode rather # than just the sql table? just faster on the current setup :P res = await glob.db.fetch( 'SELECT id, map_md5, userid, pp, score, ' 'max_combo, mods, acc, n300, n100, n50, ' 'nmiss, ngeki, nkatu, grade, perfect, ' 'status, mode, play_time, ' 'time_elapsed, client_flags, online_checksum ' f'FROM {scores_table} WHERE id = %s', [scoreid], _dict=False) if not res: return s = cls() s.id = res[0] s.bmap = await Beatmap.from_md5(res[1]) s.player = await glob.players.get_ensure(id=res[2]) (s.pp, s.score, s.max_combo, s.mods, s.acc, s.n300, s.n100, s.n50, s.nmiss, s.ngeki, s.nkatu, s.grade, s.perfect, s.status, mode_vn, s.play_time, s.time_elapsed, s.client_flags, s.online_checksum) = res[3:] # fix some types s.passed = s.status != 0 s.status = SubmissionStatus(s.status) s.grade = Grade.from_str(s.grade) s.mods = Mods(s.mods) s.mode = GameMode.from_params(mode_vn, s.mods) s.client_flags = ClientFlags(s.client_flags) if s.bmap: s.rank = await s.calc_lb_placement() return s
class Score: """\ Server side representation of an osu! score; any gamemode. Attributes ----------- id: `int` The score's unique ID. bmap: Optional[`Beatmap`] A beatmap obj representing the osu map. player: Optional[`Player`] A player obj of the player who submitted the score. pp: `float` The score's performance points. score: `int` The score's osu! score value. max_combo: `int` The maximum combo reached in the score. mods: `Mods` A bitwise value of the osu! mods used in the score. acc: `float` The accuracy of the score. n300: `int` The number of 300s in the score. n100: `int` The number of 100s in the score (150s if taiko). n50: `int` The number of 50s in the score. nmiss: `int` The number of misses in the score. ngeki: `int` The number of gekis in the score. nkatu: `int` The number of katus in the score. grade: `str` The letter grade in the score. rank: `int` The leaderboard placement of the score. passed: `bool` Whether the score completed the map. perfect: `bool` Whether the score is a full-combo. status: `SubmissionStatus` The submission status of the score. mode: `GameMode` The game mode of the score. play_time: `datetime` A datetime obj of the time of score submission. time_elapsed: `int` The total elapsed time of the play (in milliseconds). client_flags: `int` osu!'s old anticheat flags. prev_best: Optional[`Score`] The previous best score before this play was submitted. NOTE: just because a score has a `prev_best` attribute does mean the score is our best score on the map! the `status` value will always be accurate for any score. """ __slots__ = ( 'id', 'bmap', 'player', 'pp', 'sr', 'score', 'max_combo', 'mods', 'acc', 'n300', 'n100', 'n50', 'nmiss', 'ngeki', 'nkatu', 'grade', 'rank', 'passed', 'perfect', 'status', 'mode', 'play_time', 'time_elapsed', 'client_flags', 'prev_best' ) def __init__(self): self.id: Optional[int] = None self.bmap: Optional[Beatmap] = None self.player: Optional['Player'] = None # pp & star rating self.pp: Optional[float] = None self.sr: Optional[float] = None self.score: Optional[int] = None self.max_combo: Optional[int] = None self.mods: Optional[Mods] = None self.acc: Optional[float] = None # TODO: perhaps abstract these differently # since they're mode dependant? feels weird.. self.n300: Optional[int] = None self.n100: Optional[int] = None # n150 for taiko self.n50: Optional[int] = None self.nmiss: Optional[int] = None self.ngeki: Optional[int] = None self.nkatu: Optional[int] = None self.grade: Optional[Rank] = None self.rank: Optional[int] = None self.passed: Optional[bool] = None self.perfect: Optional[bool] = None self.status: Optional[SubmissionStatus] = None self.mode: Optional[GameMode] = None self.play_time: Optional[datetime] = None self.time_elapsed: Optional[datetime] = None # osu!'s client 'anticheat'. self.client_flags: Optional[ClientFlags] = None self.prev_best: Optional[Score] = None @classmethod async def from_sql(cls, scoreid: int, sql_table: str): """Create a score object from sql using it's scoreid.""" # XXX: perhaps in the future this should take a gamemode rather # than just the sql table? just faster on the current setup :P res = await glob.db.fetch( 'SELECT id, map_md5, userid, pp, score, ' 'max_combo, mods, acc, n300, n100, n50, ' 'nmiss, ngeki, nkatu, grade, perfect, ' 'status, mode, play_time, ' 'time_elapsed, client_flags ' f'FROM {sql_table} WHERE id = %s', [scoreid], _dict=False ) if not res: return s = cls() s.id = res[0] s.bmap = await Beatmap.from_md5(res[1]) s.player = await glob.players.get(id=res[2], sql=True) (s.pp, s.score, s.max_combo, s.mods, s.acc, s.n300, s.n100, s.n50, s.nmiss, s.ngeki, s.nkatu, s.grade, s.perfect, s.status, mode_vn, s.play_time, s.time_elapsed, s.client_flags) = res[3:] # fix some types s.passed = s.status != 0 s.status = SubmissionStatus(s.status) s.mods = Mods(s.mods) s.mode = GameMode.from_params(mode_vn, s.mods) s.client_flags = ClientFlags(s.client_flags) if s.bmap: s.rank = await s.calc_lb_placement() return s @classmethod async def from_submission(cls, data_b64: str, iv_b64: str, osu_ver: str, pw_md5: str) -> Optional['Score']: """Create a score object from an osu! submission string.""" iv = b64decode(iv_b64).decode('latin_1') data_aes = b64decode(data_b64).decode('latin_1') aes_key = f'osu!-scoreburgr---------{osu_ver}' aes = RijndaelCbc(aes_key, iv, ZeroPadding(32), 32) # score data is delimited by colons (:). data = aes.decrypt(data_aes).decode().split(':') if len(data) != 18: log('Received an invalid score submission.', Ansi.LRED) return s = cls() if len(map_md5 := data[0]) != 32: return pname = data[1].rstrip() # why does osu! make me rstrip lol # get the map & player for the score. s.bmap = await Beatmap.from_md5(map_md5) s.player = await glob.players.get_login(pname, pw_md5) if not s.player: # return the obj with an empty player to # determine whether the score faield to # be parsed vs. the user could not be found # logged in (we want to not send a reply to # the osu! client if they're simply not logged # in, so that it will retry once they login). return s # XXX: unused idx 2: online score checksum # perhaps will use to improve security at some point? # ensure all ints are safe to cast. if not all(map(lambda x: x.isdecimal(), data[3:11] + [data[13], data[15]])): log('Invalid parameter passed into submit-modular.', Ansi.LRED) return (s.n300, s.n100, s.n50, s.ngeki, s.nkatu, s.nmiss, s.score, s.max_combo) = map(int, data[3:11]) s.perfect = data[11] == 'True' _grade = data[12] # letter grade s.mods = Mods(int(data[13])) s.passed = data[14] == 'True' s.mode = GameMode.from_params(int(data[15]), s.mods) s.play_time = datetime.now() s.client_flags = data[17].count(' ') # TODO: use osu!ver? (osuver\s+) s.grade = _grade if s.passed else 'F' # all data read from submission. # now we can calculate things based on our data. s.calc_accuracy() if s.bmap: # ignore sr for now. s.pp, s.sr = await s.calc_diff() await s.calc_status() s.rank = await s.calc_lb_placement() else: s.pp = s.sr = 0.0 s.status = SubmissionStatus.SUBMITTED if s.passed \ else SubmissionStatus.FAILED return s
async def from_submission(cls, data_b64: str, iv_b64: str, osu_ver: str, pw_md5: str) -> Optional['Score']: """Create a score object from an osu! submission string.""" aes = RijndaelCbc(key=f'osu!-scoreburgr---------{osu_ver}', iv=b64decode(iv_b64), padding=Pkcs7Padding(32), block_size=32) # score data is delimited by colons (:). data = aes.decrypt(b64decode(data_b64)).decode().split(':') if len(data) != 18: log('Received an invalid score submission.', Ansi.LRED) return s = cls() if len(data[0]) != 32 or len(data[2]) != 32: return map_md5 = data[0] pname = data[1].rstrip() # rstrip 1 space if client has supporter s.online_checksum = data[2] # get the map & player for the score. s.bmap = await Beatmap.from_md5(map_md5) s.player = await glob.players.get_login(pname, pw_md5) if not s.player: # return the obj with an empty player to # determine whether the score failed to # be parsed vs. the user could not be found # logged in (we want to not send a reply to # the osu! client if they're simply not logged # in, so that it will retry once they login). return s # XXX: unused idx 2: online score checksum # perhaps will use to improve security at some point? # ensure all ints are safe to cast. if not all(map(str.isdecimal, data[3:11] + [data[13], data[15]])): log('Invalid parameter passed into submit-modular.', Ansi.LRED) return (s.n300, s.n100, s.n50, s.ngeki, s.nkatu, s.nmiss, s.score, s.max_combo) = map(int, data[3:11]) s.perfect = data[11] == 'True' _grade = data[12] # letter grade s.mods = Mods(int(data[13])) s.passed = data[14] == 'True' s.mode = GameMode.from_params(int(data[15]), s.mods) s.play_time = datetime.now() # TODO: use data[16] s.client_flags = ClientFlags(data[17].count(' ') & ~4) s.grade = Grade.from_str(_grade) if s.passed else Grade.F # all data read from submission. # now we can calculate things based on our data. s.calc_accuracy() if s.bmap: osu_file_path = BEATMAPS_PATH / f'{s.bmap.id}.osu' if await ensure_local_osu_file(osu_file_path, s.bmap.id, s.bmap.md5): s.pp, s.sr = s.calc_diff(osu_file_path) if s.passed: await s.calc_status() if s.bmap.status != RankedStatus.Pending: s.rank = await s.calc_lb_placement() else: s.status = SubmissionStatus.FAILED else: s.pp = s.sr = 0.0 if s.passed: s.status = SubmissionStatus.SUBMITTED else: s.status = SubmissionStatus.FAILED return s
'a', 'us', 'ha')) async def getScores(conn: AsyncConnection) -> Optional[bytes]: pname = unquote(conn.args['us']) phash = conn.args['ha'] if not (p := await glob.players.get_login(pname, phash)): return # make sure all int args are integral if not all(conn.args[k].isdecimal() for k in ('mods', 'v', 'm', 'i')): return b'-1|false' map_md5 = conn.args['c'] mods = int(conn.args['mods']) mode = GameMode.from_params(int(conn.args['m']), mods) map_set_id = int(conn.args['i']) rank_type = RankingType(int(conn.args['v'])) # attempt to update their stats if their # gm/gm-affecting-mods change at all. if mode != p.status.mode: p.status.mods = mods p.status.mode = mode glob.players.enqueue(await packets.userStats(p)) table = mode.sql_table scoring = 'pp' if mode >= GameMode.rx_std else 'score' if not (bmap := await Beatmap.from_md5(map_md5, map_set_id)):