示例#1
0
文件: web.py 项目: osuAtoka/gulag
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
示例#2
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
示例#3
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
示例#4
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
示例#5
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
示例#6
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
示例#7
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)):