Beispiel #1
0
    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
Beispiel #2
0
    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, ...), ...}}
Beispiel #3
0
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
Beispiel #4
0
    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, ...), ...}
Beispiel #5
0
    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
Beispiel #6
0
    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"])
Beispiel #7
0
    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
Beispiel #8
0
    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
Beispiel #9
0
    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
Beispiel #10
0
    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))
Beispiel #11
0
    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'])
Beispiel #12
0
    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
Beispiel #13
0
    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
Beispiel #14
0
    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, ...), ...}}
Beispiel #15
0
    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]
Beispiel #16
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:
Beispiel #17
0
            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;
Beispiel #18
0
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))
Beispiel #19
0
                            '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)):
Beispiel #20
0
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
Beispiel #21
0
    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
Beispiel #22
0
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}.')
Beispiel #23
0
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