async def from_md5_osuapi(cls, md5: str): params = {'k': glob.config.osu_api_key, 'h': md5} async with glob.http.get(f'https://old.ppy.sh/api/get_beatmaps', params=params) as resp: if not resp or resp.status != 200 or await resp.read() == b'[]': return # osu!api request failed. apidata = (await resp.json())[0] m = cls() m.md5 = md5 m.id = int(apidata['beatmap_id']) m.set_id = int(apidata['beatmapset_id']) m.status = RankedStatus.from_osuapi_status(int(apidata['approved'])) m.artist, m.title, m.version, m.creator = (apidata['artist'], apidata['title'], apidata['version'], apidata['creator']) date_format = '%Y-%m-%d %H:%M:%S' m.last_update = dt.strptime(apidata['last_update'], date_format) m.mode = GameMode(int(apidata['mode'])) m.bpm = float(apidata['bpm']) m.cs = float(apidata['diff_size']) m.od = float(apidata['diff_overall']) m.ar = float(apidata['diff_approach']) m.hp = float(apidata['diff_drain']) m.diff = float(apidata['difficultyrating']) res = await glob.db.fetch( 'SELECT last_update, status, frozen ' 'FROM maps WHERE id = %s', [apidata['beatmap_id']]) if res: # If a map with this ID exists, check if the api # data if newer than the data we have server-side; # the map may have been updated by its creator. # XXX: temp fix for local server if not res['last_update']: res['last_update'] = dt(1, 1, 1) #'0001-01-01 00:00:00' #old = dt.strptime(res['last_update'], date_format) if m.last_update > res['last_update']: if res['frozen'] and m.status != res['status']: # Keep the ranked status of maps through updates, # if we've specified to (by 'freezing' it). m.status = res['status'] m.frozen = res['frozen'] await m.save_to_sql() else: # New map, just save to DB. await m.save_to_sql() await plog(f'Retrieved {m.full} from the osu!api.', Ansi.LIGHT_GREEN) return m
def __init__(self, **kwargs) -> None: self.set: Optional[BeatmapSet] = None self.md5 = kwargs.get('md5', '') self.id = kwargs.get('id', 0) self.set_id = kwargs.get('set_id', 0) self.artist = kwargs.get('artist', '') self.title = kwargs.get('title', '') self.version = kwargs.get('version', '') # diff name self.creator = kwargs.get('creator', '') self.last_update = kwargs.get('last_update', DEFAULT_LAST_UPDATE) self.total_length = kwargs.get('total_length', 0) self.max_combo = kwargs.get('max_combo', 0) self.status = RankedStatus(kwargs.get('status', 0)) self.frozen = kwargs.get('frozen', False) == 1 self.plays = kwargs.get('plays', 0) self.passes = kwargs.get('passes', 0) self.mode = GameMode(kwargs.get('mode', 0)) self.bpm = kwargs.get('bpm', 0.0) self.cs = kwargs.get('cs', 0.0) self.od = kwargs.get('od', 0.0) self.ar = kwargs.get('ar', 0.0) self.hp = kwargs.get('hp', 0.0) self.diff = kwargs.get('diff', 0.0) self.filename = kwargs.get('filename', '') self.pp_cache = {0: {}, 1: {}, 2: {}, 3: {}} # {mode_vn: {mods: (acc/score: pp, ...), ...}}
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 __init__(self, **kwargs): self.md5 = kwargs.get('md5', '') self.id = kwargs.get('id', 0) self.set_id = kwargs.get('set_id', 0) self.artist = kwargs.get('artist', '') self.title = kwargs.get('title', '') self.version = kwargs.get('version', '') # diff name self.creator = kwargs.get('creator', '') self.last_update = kwargs.get('last_update', datetime(1970, 1, 1)) self.total_length = kwargs.get('total_length', 0) self.status = RankedStatus(kwargs.get('status', 0)) self.frozen = kwargs.get('frozen', False) == 1 self.plays = kwargs.get('plays', 0) self.passes = kwargs.get('passes', 0) self.mode = GameMode(kwargs.get('mode', 0)) self.bpm = kwargs.get('bpm', 0.0) self.cs = kwargs.get('cs', 0.0) self.od = kwargs.get('od', 0.0) self.ar = kwargs.get('ar', 0.0) self.hp = kwargs.get('hp', 0.0) self.diff = kwargs.get('diff', 0.00) self.pp_cache = {} # {mods: (acc: pp, ...), ...}
async def read_match(self) -> Match: """Read an osu! match from the internal buffer.""" m = Match() # ignore match id (i16) and inprogress (i8). self._buf = self._buf[3:] m.type = MatchTypes(await self.read_i8()) m.mods = Mods(await self.read_i32()) m.name = await self.read_string() m.passwd = await self.read_string() # TODO: don't do this, do it like everyone else.. # ignore the map's name, we're going # to get all it's info from the md5. await self.read_string() map_id = await self.read_i32() map_md5 = await self.read_string() m.bmap = await Beatmap.from_md5(map_md5) if not m.bmap and map_id != (1 << 32) - 1: # if they pick an unsubmitted map, # just give them vivid [insane] lol. vivid_md5 = '1cf5b2c2edfafd055536d2cefcb89c0e' m.bmap = await Beatmap.from_md5(vivid_md5) for slot in m.slots: slot.status = await self.read_i8() for slot in m.slots: slot.team = Teams(await self.read_i8()) for slot in m.slots: if slot.status & SlotStatus.has_player: # we don't need this, ignore it. self._buf = self._buf[4:] host_id = await self.read_i32() m.host = await glob.players.get_by_id(host_id) m.mode = GameMode(await self.read_i8()) m.match_scoring = MatchScoringTypes(await self.read_i8()) m.team_type = MatchTeamTypes(await self.read_i8()) m.freemods = await self.read_i8() == 1 # if we're in freemods mode, # read individual slot mods. if m.freemods: for slot in m.slots: slot.mods = Mods(await self.read_i32()) # read the seed (used for mania) m.seed = await self.read_i32() return m
def _parse_from_osuapi_resp(self, osuapi_resp: dict[str, Any]) -> None: """Change internal data with the data in osu!api format.""" # NOTE: `self` is not guaranteed to have any attributes # initialized when this is called. self.md5 = osuapi_resp["file_md5"] # self.id = int(osuapi_resp['beatmap_id']) self.set_id = int(osuapi_resp["beatmapset_id"]) self.artist, self.title, self.version, self.creator = ( osuapi_resp["artist"], osuapi_resp["title"], osuapi_resp["version"], osuapi_resp["creator"], ) self.filename = ( ("{artist} - {title} ({creator}) [{version}].osu") .format(**osuapi_resp) .translate(IGNORED_BEATMAP_CHARS) ) # quite a bit faster than using dt.strptime. _last_update = osuapi_resp["last_update"] self.last_update = datetime( year=int(_last_update[0:4]), month=int(_last_update[5:7]), day=int(_last_update[8:10]), hour=int(_last_update[11:13]), minute=int(_last_update[14:16]), second=int(_last_update[17:19]), ) self.total_length = int(osuapi_resp["total_length"]) if osuapi_resp["max_combo"] is not None: self.max_combo = int(osuapi_resp["max_combo"]) else: self.max_combo = 0 # if a map is 'frozen', we keeps it's status # even after an update from the osu!api. if not getattr(self, "frozen", False): osuapi_status = int(osuapi_resp["approved"]) self.status = RankedStatus.from_osuapi(osuapi_status) self.mode = GameMode(int(osuapi_resp["mode"])) if osuapi_resp["bpm"] is not None: self.bpm = float(osuapi_resp["bpm"]) else: self.bpm = 0.0 self.cs = float(osuapi_resp["diff_size"]) self.od = float(osuapi_resp["diff_overall"]) self.ar = float(osuapi_resp["diff_approach"]) self.hp = float(osuapi_resp["diff_drain"]) self.diff = float(osuapi_resp["difficultyrating"])
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 read_match(self) -> Match: """Read an osu! match from the internal buffer.""" m = Match() # ignore match id (i16) and inprogress (i8). self._buf = self._buf[3:] #m.type = MatchTypes(await self.read_i8()) if await self.read_i8() == 1: point_of_interest() # what is powerplay m.mods = Mods(await self.read_i32()) m.name = await self.read_string() m.passwd = await self.read_string() m.map_name = await self.read_string() m.map_id = await self.read_i32() m.map_md5 = await self.read_string() for slot in m.slots: slot.status = await self.read_i8() for slot in m.slots: slot.team = MatchTeams(await self.read_i8()) for slot in m.slots: if slot.status & SlotStatus.has_player: # we don't need this, ignore it. self._buf = self._buf[4:] host_id = await self.read_i32() m.host = await glob.players.get(id=host_id) m.mode = GameMode(await self.read_i8()) m.win_condition = MatchWinConditions(await self.read_i8()) m.team_type = MatchTeamTypes(await self.read_i8()) m.freemods = await self.read_i8() == 1 # if we're in freemods mode, # read individual slot mods. if m.freemods: for slot in m.slots: slot.mods = Mods(await self.read_i32()) # read the seed (used for mania) m.seed = await self.read_i32() return m
def read_match(self) -> Match: """Read an osu! match from the internal buffer.""" m = Match() # ignore match id (i16) and inprogress (i8). self.body_view = self.body_view[3:] self.read_i8() # powerplay unused m.mods = Mods(self.read_i32()) m.name = self.read_string() m.passwd = self.read_string() m.map_name = self.read_string() m.map_id = self.read_i32() m.map_md5 = self.read_string() for slot in m.slots: slot.status = SlotStatus(self.read_i8()) for slot in m.slots: slot.team = MatchTeams(self.read_i8()) for slot in m.slots: if slot.status & SlotStatus.has_player: # we don't need this, ignore it. self.body_view = self.body_view[4:] host_id = self.read_i32() m.host = glob.players.get(id=host_id) m.mode = GameMode(self.read_i8()) m.win_condition = MatchWinConditions(self.read_i8()) m.team_type = MatchTeamTypes(self.read_i8()) m.freemods = self.read_i8() == 1 # if we're in freemods mode, # read individual slot mods. if m.freemods: for slot in m.slots: slot.mods = Mods(self.read_i32()) # read the seed (used for mania) m.seed = self.read_i32() return m
async def handle(self, p: Player) -> None: # update the user's status. p.status.action = Action(self.action) p.status.info_text = self.info_text p.status.map_md5 = self.map_md5 p.status.mods = Mods(self.mods) if p.status.mods & Mods.RELAX: self.mode += 4 elif p.status.mods & Mods.AUTOPILOT: self.mode = 7 p.status.mode = GameMode(self.mode) p.status.map_id = self.map_id # broadcast it to all online players. glob.players.enqueue(packets.userStats(p))
def _parse_from_osuapi_resp(self, osuapi_resp: dict[str, object]) -> None: """Change internal data with the data in osu!api format.""" # NOTE: `self` is not guaranteed to have any attributes # initialized when this is called. self.md5 = osuapi_resp['file_md5'] #self.id = int(osuapi_resp['beatmap_id']) self.set_id = int(osuapi_resp['beatmapset_id']) self.artist, self.title, self.version, self.creator = ( osuapi_resp['artist'], osuapi_resp['title'], osuapi_resp['version'], osuapi_resp['creator']) self.filename = ( '{artist} - {title} ({creator}) [{version}].osu').format( **osuapi_resp).translate(IGNORED_BEATMAP_CHARS) # quite a bit faster than using dt.strptime. _last_update = osuapi_resp['last_update'] self.last_update = datetime(year=int(_last_update[0:4]), month=int(_last_update[5:7]), day=int(_last_update[8:10]), hour=int(_last_update[11:13]), minute=int(_last_update[14:16]), second=int(_last_update[17:19])) self.total_length = int(osuapi_resp['total_length']) if osuapi_resp['max_combo'] is not None: self.max_combo = int(osuapi_resp['max_combo']) else: self.max_combo = 0 # if a map is 'frozen', we keeps it's status # even after an update from the osu!api. if not getattr(self, 'frozen', False): self.status = RankedStatus.from_osuapi(int( osuapi_resp['approved'])) self.mode = GameMode(int(osuapi_resp['mode'])) self.bpm = float(osuapi_resp['bpm']) self.cs = float(osuapi_resp['diff_size']) self.od = float(osuapi_resp['diff_overall']) self.ar = float(osuapi_resp['diff_approach']) self.hp = float(osuapi_resp['diff_drain']) self.diff = float(osuapi_resp['difficultyrating'])
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
def __init__(self, **kwargs: Any) -> None: self.set: Optional[BeatmapSet] = None self.md5 = kwargs.get("md5", "") self.id = kwargs.get("id", 0) self.set_id = kwargs.get("set_id", 0) self.artist = kwargs.get("artist", "") self.title = kwargs.get("title", "") self.version = kwargs.get("version", "") # diff name self.creator = kwargs.get("creator", "") self.last_update = kwargs.get("last_update", DEFAULT_LAST_UPDATE) self.total_length = kwargs.get("total_length", 0) self.max_combo = kwargs.get("max_combo", 0) self.status = RankedStatus(kwargs.get("status", 0)) self.frozen = kwargs.get("frozen", False) == 1 self.plays = kwargs.get("plays", 0) self.passes = kwargs.get("passes", 0) self.mode = GameMode(kwargs.get("mode", 0)) self.bpm = kwargs.get("bpm", 0.0) self.cs = kwargs.get("cs", 0.0) self.od = kwargs.get("od", 0.0) self.ar = kwargs.get("ar", 0.0) self.hp = kwargs.get("hp", 0.0) self.diff = kwargs.get("diff", 0.0) self.filename = kwargs.get("filename", "") self.pp_cache = { 0: {}, 1: {}, 2: {}, 3: {}, } # {mode_vn: {mods: (acc/score: pp, ...), ...}}
def __init__(self, **kwargs): self.md5 = kwargs.pop('md5', '') self.id = kwargs.pop('id', 0) self.set_id = kwargs.pop('set_id', 0) self.artist = kwargs.pop('artist', '') self.title = kwargs.pop('title', '') self.version = kwargs.pop('version', '') self.creator = kwargs.pop('creator', '') self.last_update: dt = kwargs.pop('last_update', dt(1, 1, 1)) self.status = RankedStatus(kwargs.pop('status', 0)) self.frozen = kwargs.pop('frozen', False) self.mode = GameMode(kwargs.pop('mode', 0)) self.bpm = kwargs.pop('bpm', 0.0) self.cs = kwargs.pop('cs', 0.0) self.od = kwargs.pop('od', 0.0) self.ar = kwargs.pop('ar', 0.0) self.hp = kwargs.pop('hp', 0.0) self.diff = kwargs.pop('diff', 0.00) self.pp_values = [0.0, 0.0, 0.0, 0.0, 0.0]
bmap = apidata[0] m = cls() m.md5 = md5 m.id = int(bmap['beatmap_id']) m.set_id = int(bmap['beatmapset_id']) m.status = RankedStatus.from_osuapi(int(bmap['approved'])) m.artist, m.title, m.version, m.creator = ( bmap['artist'], bmap['title'], bmap['version'], bmap['creator'] ) m.last_update = datetime.strptime( bmap['last_update'], '%Y-%m-%d %H:%M:%S') m.mode = GameMode(int(bmap['mode'])) m.bpm = float(bmap['bpm']) m.cs = float(bmap['diff_size']) m.od = float(bmap['diff_overall']) m.ar = float(bmap['diff_approach']) m.hp = float(bmap['diff_drain']) m.diff = float(bmap['difficultyrating']) res = await glob.db.fetch( 'SELECT last_update, status, frozen ' 'FROM maps WHERE id = %s', [m.id] ) if res:
apidata = (await resp.json())[0] m = cls() m.md5 = md5 m.id = int(apidata['beatmap_id']) m.set_id = int(apidata['beatmapset_id']) m.status = RankedStatus.from_osuapi(int(apidata['approved'])) m.artist, m.title, m.version, m.creator = (apidata['artist'], apidata['title'], apidata['version'], apidata['creator']) date_format = '%Y-%m-%d %H:%M:%S' m.last_update = datetime.strptime(apidata['last_update'], date_format) m.mode = GameMode(int(apidata['mode'])) m.bpm = float(apidata['bpm']) m.cs = float(apidata['diff_size']) m.od = float(apidata['diff_overall']) m.ar = float(apidata['diff_approach']) m.hp = float(apidata['diff_drain']) m.diff = float(apidata['difficultyrating']) res = await glob.db.fetch( 'SELECT last_update, status, frozen ' 'FROM maps WHERE id = %s', [apidata['beatmap_id']]) if res: # if a map with this id exists, check if the api # data if newer than the data we have server-side;
async def submitModularSelector(req: AsyncRequest) -> Optional[bytes]: if not all(x in req.args for x in required_params_submitModular): await plog(f'submit-modular-selector req missing params.', Ansi.LIGHT_RED) return b'error: no' # Parse our score data into a score obj. s: Score = await Score.from_submission( req.args['score'], req.args['iv'], req.args['osuver'], req.args['pass'] ) if not s: await plog('Failed to parse a score - invalid format.', Ansi.LIGHT_RED) return b'error: no' elif not s.player: # Player is not online, return nothing so that their # client will retry submission when they log in. return elif not s.bmap: # Map does not exist, most likely unsubmitted. return b'error: no' elif s.bmap.status == RankedStatus.Pending: # XXX: Perhaps will accept in the future, return b'error: no' # not now though. table = 'scores_rx' if s.mods & Mods.RELAX else 'scores_vn' # Check for score duplicates # TODO: might need to improve? res = await glob.db.fetch( f'SELECT 1 FROM {table} WHERE game_mode = %s ' 'AND map_md5 = %s AND userid = %s AND mods = %s ' 'AND score = %s', [s.game_mode, s.bmap.md5, s.player.id, s.mods, s.score] ) if res: await plog(f'{s.player} submitted a duplicate score.', Ansi.LIGHT_YELLOW) return b'error: no' if req.args['i']: breakpoint() gm = GameMode(s.game_mode + (4 if s.mods & Mods.RELAX and s.game_mode != 3 else 0)) if not s.player.priv & Privileges.Whitelisted: # Get the PP cap for the current context. pp_cap = autorestrict_pp[gm][s.mods & Mods.FLASHLIGHT != 0] if s.pp > pp_cap: await plog(f'{s.player} restricted for submitting ' f'{s.pp:.2f} score on gm {s.game_mode}.', Ansi.LIGHT_RED) await s.player.restrict() return b'error: ban' if s.status == SubmissionStatus.BEST: # Our score is our best score. # Update any preexisting personal best # records with SubmissionStatus.SUBMITTED. await glob.db.execute( f'UPDATE {table} SET status = 1 ' 'WHERE status = 2 and map_md5 = %s ' 'AND userid = %s', [s.bmap.md5, s.player.id]) s.id = await glob.db.execute( f'INSERT INTO {table} VALUES (NULL, ' '%s, %s, %s, %s, %s, %s, ' '%s, %s, %s, %s, %s, %s, ' '%s, %s, %s, ' '%s, %s, %s' ')', [ s.bmap.md5, s.score, s.pp, s.acc, s.max_combo, s.mods, s.n300, s.n100, s.n50, s.nmiss, s.ngeki, s.nkatu, int(s.status), s.game_mode, s.play_time, s.client_flags, s.player.id, s.perfect ] ) if s.status != SubmissionStatus.FAILED: # All submitted plays should have a replay. # If not, they may be using a score submitter. if 'score' not in req.files or req.files['score'] == b'\r\n': await plog(f'{s.player} submitted a score without a replay!', Ansi.LIGHT_RED) await s.player.restrict() else: # Save our replay async with aiofiles.open(f'replays/{s.id}.osr', 'wb') as f: await f.write(req.files['score']) time_elapsed = req.args['st' if s.passed else 'ft'] if not isinstance(time_elapsed, int): return s.time_elapsed = time_elapsed / 1000 # Get the user's stats for current mode. stats = s.player.stats[gm] stats.playtime += s.time_elapsed stats.tscore += s.score if s.bmap.status in (RankedStatus.Ranked, RankedStatus.Approved): stats.rscore += s.score await glob.db.execute( 'UPDATE stats SET rscore_{0:sql} = %s, ' 'tscore_{0:sql} = %s, playtime_{0:sql} = %s ' 'WHERE id = %s'.format(gm), [ stats.rscore, stats.tscore, stats.playtime, s.player.id ] ) if s.status == SubmissionStatus.BEST and s.rank == 1 \ and (announce_chan := glob.channels.get('#announce')): # Announce the user's #1 score. prev_n1 = await glob.db.fetch( 'SELECT u.id, name FROM users u ' f'LEFT JOIN {table} s ON u.id = s.userid ' 'WHERE s.map_md5 = %s AND game_mode = %s ' 'ORDER BY pp DESC LIMIT 1, 1', [s.bmap.md5, s.game_mode] ) ann: List[str] = [f'{s.player.embed} achieved #1 on {s.bmap.embed}.'] if prev_n1: # If there was previously a score on the map, add old #1. ann.append('(Prev: [https://osu.ppy.sh/u/{id} {name}])'.format(**prev_n1)) await announce_chan.send(glob.bot, ' '.join(ann))
'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)):
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
def submitModularSelector(req: Request) -> Optional[bytes]: if not all(x in req.args for x in required_params_submitModular): printlog(f'submit-modular-selector req missing params.') return b'error: no' # TODO: make some kind of beatmap object. # We currently don't check the map's ranked status. # Parse our score data into a score obj. s: Score = Score.from_submission(req.args['score'], req.args['iv'], req.args['osuver'], req.args['pass']) if not s: printlog('Failed to parse a score - invalid format.', Ansi.YELLOW) return b'error: no' elif not s.player: # Player is not online, return nothing so that their # client will retry submission when they log in. return elif not s.bmap: # Map does not exist, most likely unsubmitted. return b'error: no' elif s.bmap.status == RankedStatus.Pending: # XXX: Perhaps will accept in the future, return b'error: no' # not now though. table = 'scores_rx' if s.mods & Mods.RELAX else 'scores_vn' # Check for score duplicates # TODO: might need to improve? res = glob.db.fetch( f'SELECT 1 FROM {table} WHERE game_mode = %s ' 'AND map_md5 = %s AND userid = %s AND mods = %s ' 'AND score = %s', [s.game_mode, s.bmap.md5, s.player.id, s.mods, s.score]) if res: printlog(f'{s.player} submitted a duplicate score.', Ansi.LIGHT_YELLOW) return b'error: no' if req.args['i']: breakpoint() gm = GameMode(s.game_mode + (4 if s.player.rx and s.game_mode != 3 else 0)) if not s.player.priv & Privileges.Whitelisted: # Get the PP cap for the current context. pp_cap = autorestrict_pp[gm][s.mods & Mods.FLASHLIGHT != 0] if s.pp > pp_cap: printlog( f'{s.player} restricted for submitting {s.pp:.2f} score on gm {s.game_mode}.', Ansi.LIGHT_RED) s.player.restrict() return b'error: ban' if s.status == SubmissionStatus.BEST: # Our score is our best score. # Update any preexisting personal best # records with SubmissionStatus.SUBMITTED. glob.db.execute( f'UPDATE {table} SET status = 1 ' 'WHERE status = 2 and map_md5 = %s ' 'AND userid = %s', [s.bmap.md5, s.player.id]) s.id = glob.db.execute( f'INSERT INTO {table} VALUES (NULL, ' '%s, %s, %s, %s, %s, %s, ' '%s, %s, %s, %s, %s, %s, ' '%s, %s, %s, ' '%s, %s, %s' ')', [ s.bmap.md5, s.score, s.pp, s.acc, s.max_combo, s.mods, s.n300, s.n100, s.n50, s.nmiss, s.ngeki, s.nkatu, int(s.status), s.game_mode, s.play_time, s.client_flags, s.player.id, s.perfect ]) if s.status != SubmissionStatus.FAILED: # All submitted plays should have a replay. # If not, they may be using a score submitter. if 'score' not in req.files or req.files['score'] == b'\r\n': printlog(f'{s.player} submitted a score without a replay!', Ansi.LIGHT_RED) s.player.restrict() else: # Save our replay with open(f'replays/{s.id}.osr', 'wb') as f: f.write(req.files['score']) s.player.stats[gm].tscore += s.score if s.bmap.status in {RankedStatus.Ranked, RankedStatus.Approved}: s.player.stats[gm].rscore += s.score glob.db.execute( 'UPDATE stats SET rscore_{0:sql} = %s, ' 'tscore_{0:sql} = %s WHERE id = %s'.format(gm), [s.player.stats[gm].rscore, s.player.stats[gm].tscore, s.player.id]) if s.status == SubmissionStatus.BEST and s.rank == 1: # Announce the user's #1 score. if announce_chan := glob.channels.get('#announce'): announce_chan.send( glob.bot, f'{s.player.embed} achieved #1 on {s.bmap.embed}.')
async def read_match(data: memoryview) -> tuple[Match, int]: """ Read an osu! match from `data`. """ m = Match() # Ignore match id (i32) & inprogress (i8). offset = 3 # Read match type (no idea what this is tbh). m.type = MatchTypes(data[offset]) offset += 1 # Read match mods. m.mods = Mods.from_bytes(data[offset:offset+4], 'little') offset += 4 # Read match name & password. m.name, offs = await read_string(data[offset:]) offset += offs m.passwd, offs = await read_string(data[offset:]) offset += offs # Ignore map's name. if data[offset] == 0x0b: offset += sum(await read_uleb128(data[offset + 1:])) offset += 1 # Read beatmap information (id & md5). map_id = int.from_bytes(data[offset:offset+4], 'little') offset += 4 map_md5, offs = await read_string(data[offset:]) offset += offs # Get beatmap object for map selected. m.bmap = await Beatmap.from_md5(map_md5) if not m.bmap and map_id != (1 << 32) - 1: # If they pick an unsubmitted map, # just give them Vivid [Insane] lol. vivid_md5 = '1cf5b2c2edfafd055536d2cefcb89c0e' m.bmap = await Beatmap.from_md5(vivid_md5) # Read slot statuses. for s in m.slots: s.status = data[offset] offset += 1 # Read slot teams. for s in m.slots: s.team = Teams(data[offset]) offset += 1 for s in m.slots: if s.status & SlotStatus.has_player: # Dont think we need this? offset += 4 # Read match host. user_id = int.from_bytes(data[offset:offset+4], 'little') m.host = await glob.players.get_by_id(user_id) offset += 4 # Read match mode, match scoring, # team type, and freemods. m.mode = GameMode(data[offset]) offset += 1 m.match_scoring = MatchScoringTypes(data[offset]) offset += 1 m.team_type = MatchTeamTypes(data[offset]) offset += 1 m.freemods = data[offset] == 1 offset += 1 # If we're in freemods mode, # read individual slot mods. if m.freemods: for s in m.slots: s.mods = Mods.from_bytes(data[offset:offset+4], 'little') offset += 4 # Read the seed from multi. # XXX: Used for mania random mod. m.seed = int.from_bytes(data[offset:offset+4], 'little') return m, offset + 4