Example #1
0
    async def update_stats(self, mode: osuModes, table: str, mode_vn: int) -> None:
        stats = self.stats[mode.value]
        mode_name = mode.name

        s = await glob.db.fetch(
            'SELECT {0}.acc, {0}.pp FROM {0} '
            'LEFT OUTER JOIN maps ON maps.md5 = {0}.md5 '
            'WHERE {0}.uid = %s AND {0}.mode = %s '
            'AND {0}.status = 2 AND maps.status IN (2, 3) '
            'ORDER BY {0}.pp DESC'.format(table),
            [self.id, mode_vn]
        )

        if not s:
            return # no scores xd

        t100 = s[:100]

        stats.acc = sum([row['acc'] for row in s[:50]]) / min(50, len(t100))
        weighted = sum([row['pp'] * 0.95 ** i for i, row in enumerate(t100)])
        bonus = 416.6667 * (1 - 0.9994 ** len(s))
        stats.pp = round(weighted + bonus)

        if not self.restricted:
            await glob.redis.zadd(f'asahi:leaderboard:{mode_name}', stats.pp, self.id)
            await glob.redis.zadd(f'asahi:leaderboard:{mode_name}:{self.country_iso}', stats.pp, self.id)

            r = await glob.redis.zrevrank(f'asahi:leaderboard:{mode_name}', self.id)
            cr = await glob.redis.zrevrank(f'asahi:leaderboard:{mode_name}:{self.country_iso}', self.id)

            stats.rank = 0
            stats.country_rank = 0

            if r is None:
                if stats.pp > 0:
                    stats.rank = 1
            else:
                stats.rank = r + 1

            if cr is None:
                if stats.pp > 0:
                    stats.country_rank = 1
            else:
                stats.country_rank = cr + 1

        await glob.db.execute(
            'UPDATE stats SET rscore_{0} = %s, acc_{0} = %s, pc_{0} = %s, tscore_{0} = %s,'
            ' pp_{0} = %s, mc_{0} = %s, pt_{0} = %s WHERE id = %s'.format(mode_name),
            [stats.rscore, stats.acc, stats.pc, stats.tscore,
             stats.pp, stats.max_combo, stats.playtime, self.id]
        )

        self.enqueue(writer.userStats(self))

        if not self.restricted:
            glob.players.enqueue(writer.userStats(self))
Example #2
0
async def update_action(user: Player, p: bytes) -> None:
    d = reader.handle_packet(p, (
        ('actionid', osuTypes.u8),
        ('info', osuTypes.string),
        ('md5', osuTypes.string),
        ('mods', osuTypes.u32),
        ('mode', osuTypes.u8),
        ('mid', osuTypes.i32)
    ))

    if d['actionid'] == 0 and d['mods'] & Mods.RELAX:
        d['info'] = 'on Relax'
    elif d['actionid'] == 0 and d['mods'] & Mods.AUTOPILOT:
        d['info'] = 'on Autopilot'

    user.action = d['actionid']
    user.info = d['info']
    user.map_md5 = d['md5']
    user.mods = d['mods']

    m = lbModes(d['mode'], d['mods'])
    user.mode = m.value
    user.mode_vn = m.as_vn

    user.map_id = d['mid']

    if d['actionid'] == 2:
        user.info += f' +{(Mods(user.mods))!r}' # ugly and i dont care!

    if not user.restricted:
        glob.players.enqueue(writer.userStats(user))
Example #3
0
async def update_action(user: Player, p: bytes) -> None:
    d = reader.handle_packet(
        p,
        (
            ("actionid", osuTypes.u8),
            ("base_info", osuTypes.string),
            ("md5", osuTypes.string),
            ("mods", osuTypes.u32),
            ("mode", osuTypes.u8),
            ("mid", osuTypes.i32),
        ),
    )

    if d["actionid"] == 0 and d["mods"] & Mods.RELAX:
        d["base_info"] = "on Relax"
    elif d["actionid"] == 0 and d["mods"] & Mods.AUTOPILOT:
        d["base_info"] = "on Autopilot"

    user.action = d["actionid"]
    user.base_info = d["base_info"]
    user.map_md5 = d["md5"]
    user.mods = d["mods"]

    m = lbModes(d["mode"], d["mods"])
    user.mode = m.value
    user.mode_vn = m.as_vn

    user.map_id = d["mid"]

    if d["actionid"] == 2:
        user.base_info += f" +{(Mods(user.mods))!r}"  # ugly and i dont care!

    if not user.restricted:
        glob.players.enqueue(writer.userStats(user))
Example #4
0
async def scoreSubmit(request: Request) -> bytes:
    mpargs = request.args

    s = await Score.from_submission(mpargs['score'], mpargs['iv'],
                                    mpargs['pass'], mpargs['osuver'])

    if not s:
        return b'error: no'
    elif not s.user:
        return b''  # player not online, make client make resubmit attempts
    elif not s.map:
        return b'error: beatmap'  # map unsubmitted
    elif s.mods & Mods.UNRANKED:
        return b'error: no'

    if s.mode != s.user.mode or s.mods != s.user.mods:
        s.user.mode = s.mode.value
        s.user.mods = s.mods
        glob.players.enqueue(writer.userStats(s.user))

    # submit score and get id xd
    s.id = await glob.db.execute(
        f'INSERT INTO {s.mode.table} (md5, score, acc, pp, combo, mods, n300, '
        f'geki, n100, katu, n50, miss, grade, status, mode, time, uid, readable_mods, fc) VALUES '
        f'(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)',
        [
            s.map.md5, s.score, s.acc, s.pp, s.combo,
            int(s.mods), s.n300, s.geki, s.n100, s.katu, s.n50, s.miss,
            s.grade, s.status.value, s.mode.as_vn, s.time, s.user.id,
            s.readable_mods, s.fc
        ])

    if s.status == scoreStatuses.Best:
        # set any other best scores to submitted ones as they've been overwritten
        await glob.db.execute(
            f'UPDATE {s.mode.table} SET status = 1 WHERE status = 2 AND uid = %s AND md5 = %s AND mode = %s AND id != %s',
            [s.user.id, s.map.md5, s.mode.as_vn, s.id])

    # save replay if not a failed score
    if s.status != scoreStatuses.Failed:
        files = request.files
        replay = files.get('score')

        # i will make this auto-parse the replays one day when im not lazy
        if s.mods & Mods.RELAX:
            f = rx_path / f'{s.id}.osr'
        elif s.mods & Mods.AUTOPILOT:
            f = ap_path / f'{s.id}.osr'
        else:
            f = vn_path / f'{s.id}.osr'

        f.write_bytes(replay)

        if glob.config.anticheat and s.user.priv & Privileges.BypassAnticheat:
            loop = asyncio.get_event_loop()
            loop.create_task(s.analyse())

    cap = glob.config.pp_caps[s.mode.value]

    if cap is not None and s.pp >= cap \
        and s.map.status & mapStatuses.GIVE_PP and glob.config.anticheat \
        and not s.user.restricted and not s.user.priv & Privileges.Whitelisted: # ugly

        await s.user.restrict(
            reason=f'Exceeding PP cap ({s.pp:,}pp) on {s.map.name}',
            fr=glob.bot)

    # update stats EEEEEEE
    stats = s.user.stats[s.mode.value]
    old = copy.copy(
        stats)  # we need a copy of the old stats for submission chart

    elapsed = mpargs.get(
        'st' if s.passed else 'ft')  # timewarp check with this soon?

    if not elapsed and s.user.priv & Privileges.BypassAnticheat:
        await s.user.restrict(
            'Modified client', fr=glob.bot
        )  # its really only old version, but its supposed to be blocked on login. if it isnt present it must be modified to seem like a new version

    stats.playtime += int(elapsed) // 1000
    stats.tscore += s.score
    stats.pc += 1

    if s.status == scoreStatuses.Best and s.map.status >= mapStatuses.Ranked:
        add = s.score

        if s.old_best:
            add -= s.old_best.score

        if s.combo > old.max_combo:
            stats.max_combo = s.combo

        if s.map.status & mapStatuses.GIVE_PP:
            stats.rscore += add

    await s.user.update_stats(s.mode, s.mode.table, s.mode.as_vn)

    if not s.user.restricted:
        s.map.plays += 1
        if s.passed:
            s.map.passes += 1

        await glob.db.execute(
            'UPDATE maps SET plays = %s, passes = %s WHERE md5 = %s',
            [s.map.plays, s.map.passes, s.map.md5])

    # sub charts bruh
    if s.mods & Mods.GAME_CHANGING or s.status == scoreStatuses.Failed:
        log(
            f'[{s.mode!r}] {s.user.name} submitted a score on {s.map.name} ({s.status.name})',
            Ansi.LBLUE)
        return b'error: no'  # not actually erroring, score is already submitted we just want client to stop request as we cannot provide chart

    achievements = ''
    if s.map.status & mapStatuses.GIVE_PP and not s.user.restricted:  # TODO: hush-hush etc. achievements
        achs = []
        for ach in glob.achievements:
            if ach in s.user.achievements:
                continue

            if ach.cond(s):
                await s.user.unlock_ach(ach)
                achs.append(ach)

        achievements = '/'.join([a.format for a in achs])

    charts = []

    # could be done better
    def chart_format(name, b, a):
        return f'{name}Before:{b or ""}|{name}After:{a}'  # osu makes this so ugly man.

    # map info
    charts.append(f'beatmapId:{s.map.id}|'
                  f'beatmapSetId:{s.map.sid}|'
                  f'beatmapPlaycount:{s.map.plays}|'
                  f'beatmapPasscount:{s.map.passes}|'
                  f'approvedDate:{s.map.update}')

    # score-specific ranking
    if s.map.status >= mapStatuses.Ranked:
        charts.append('|'.join((
            'chartId:beatmap',
            f'chartUrl:{s.map.set_url}',
            'chartName:Current Score',
            *((  # wtaf
                chart_format('rank', s.old_best.rank, s.rank),
                chart_format('rankedScore', s.old_best.score, s.score),
                chart_format('totalScore', s.old_best.score, s.score),
                chart_format('maxCombo', s.old_best.combo, s.combo),
                chart_format('accuracy', round(s.old_best.acc, 2),
                             round(s.acc, 2)),
                chart_format('pp', s.old_best.pp, s.pp)) if s.old_best else
              (chart_format('rank', None, s.rank),
               chart_format('rankedScore', None, s.score),
               chart_format('totalScore', None, s.score),
               chart_format('maxCombo', None, s.combo),
               chart_format('accuracy', None, round(s.acc, 2)),
               chart_format('pp', None, s.pp))),
            f'onlineScoreId:{s.id}')))

    # overall user stats
    charts.append('|'.join(
        ('chartId:overall',
         f'chartUrl:https://{glob.config.domain}/u/{s.user.id}',
         'chartName:Overall Stats',
         *((chart_format('rank', old.rank, stats.rank),
            chart_format('rankedScore', old.rscore, stats.rscore),
            chart_format('totalScore', old.tscore, stats.tscore),
            chart_format('maxCombo', old.max_combo, stats.max_combo),
            chart_format('accuracy', round(old.acc, 2), round(stats.acc, 2)),
            chart_format('pp', old.pp, stats.pp)) if old else
           (chart_format('rank', None, stats.rank),
            chart_format('rankedScore', None, stats.rscore),
            chart_format('totalScore', None, stats.tscore),
            chart_format('maxCombo', None, stats.max_combo),
            chart_format('accuracy', None, round(stats.acc, 2)),
            chart_format('pp', None, stats.pp))),
         f'achievements-new:{achievements}')))

    if s.status == scoreStatuses.Best and s.map.status >= mapStatuses.Ranked:
        if s.rank == 1:  # announce #1 to announce channel cus they achieved #1
            loop = asyncio.get_event_loop()
            loop.create_task(s.announce_n1())

        # update lb cache
        lb = getattr(s.map, s.mode.leaderboard)

        if lb:
            threading.Thread(target=lb.set_user_pb, args=(
                s.user,
                s,
            )).start()

    s.user.last_score = s

    log(
        f'[{s.mode!r}] {s.user.name} submitted a score on {s.map.name} ({s.status.name})',
        Ansi.LBLUE)
    return '\n'.join(charts).encode()  # thank u osu
Example #5
0
    if (md5 := args['c']) in glob.cache['unsub']:
        return b'-1|false'  # tell client map is unsub xd

    mods = int(args['mods'])
    mode = lbModes(int(args['m']), mods)
    lbm = int(args['v'])

    player = request.extras.get('player')

    if mode.value != player.mode or mods != player.mods:
        player.mode = mode.value
        player.mode_vn = mode.as_vn
        player.mods = mods

        if not player.restricted:
            glob.players.enqueue(writer.userStats(player))

    bmap = await Beatmap.from_md5(md5)

    if not bmap:
        file = args['f'].replace('+', '')
        if not (info := regexes.map_file.match(
                unquote(file))):  # once again osu why
            # invalid file? idfk
            glob.cache['unsub'].append(md5)
            return b'-1|false'

        exists = await glob.db.fetchval(
            'SELECT 1 FROM maps WHERE artist = %s AND title = %s AND diff = %s AND mapper = %s',
            [info['artist'], info['title'], info['diff'], info['mapper']])
Example #6
0
async def request_stats(user: Player, p: bytes) -> None:
    uids = (reader.handle_packet(p, (('uids', osuTypes.i32_list),)))['uids']

    for o in glob.players.unrestricted_users:
        if o.id != user.id and o.id in uids:
            user.enqueue(writer.userStats(o))
Example #7
0
async def update_stats(user: Player, _) -> None:
    user.enqueue(writer.userStats(user))
Example #8
0
            # we want to check multi stuff before any cheats just in case
            await checks.multi_check()

            # this is probably confusing syntax.
            # client_check will restrict if a client is custom and if that's the case we want to ignore any update checks.
            # if an update check is made, this will send update required packet if thats what the function returns.
            # i should probably rename these funcs in the future
            if not await checks.client_check() and await checks.version_check():
                request.resp_headers['cho-token'] = 'no'
                return writer.versionUpdateForced() + writer.userID(-2)

        # start enqueueing login data to the client
        data = bytearray(writer.userID(p.id)) # initiate login by providing the user's id
        data += writer.protocolVersion(19) # no clue what this does
        data += writer.banchoPrivileges(p.client_priv | ClientPrivileges.Supporter)
        data += writer.userPresence(p) + writer.userStats(p) # provide user & other user's presence/stats (for f9 + user stats)
        data += writer.channelInfoEnd() # no clue what this does either
        data += writer.menuIcon() # set main menu icon
        data += writer.friends(p.friends) # send user friend list
        data += writer.silenceEnd(p.silence_end)

        # get channels from cache and send to user
        for chan in glob.channels.values():
            if chan.auto:
                p.join_chan(chan)
                data += writer.channelJoin(chan.name) # only join user to channel if the channel is meant for purpose

            data += writer.channelInfo(chan) # regardless of whether the channel should be auto-joined we should make the client aware of it

        # add user to cache?
        glob.players.append(p)
Example #9
0
async def scoreSubmit(request: Request) -> bytes:
    mpargs = request.args

    s = await Score.from_submission(
        mpargs["score"],
        mpargs["iv"],
        mpargs["pass"],
        mpargs["osuver"],
    )

    if not s:
        return b"error: no"
    elif not s.user:
        return b""  # player not online, make client make resubmit attempts
    elif not s.map:
        return b"error: beatmap"  # map unsubmitted
    elif s.mods & Mods.UNRANKED:
        return b"error: no"

    if s.mode != s.user.mode or s.mods != s.user.mods:
        s.user.mode = s.mode.value
        s.user.mods = s.mods
        glob.players.enqueue(writer.userStats(s.user))

    # submit score and get id xd
    s.id = await glob.db.execute(
        f"INSERT INTO {s.mode.table} (md5, score, acc, pp, combo, mods, n300, "
        f"geki, n100, katu, n50, miss, grade, status, mode, time, uid, readable_mods, fc) VALUES "
        f"(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
        [
            s.map.md5,
            s.score,
            s.acc,
            s.pp,
            s.combo,
            int(s.mods),
            s.n300,
            s.geki,
            s.n100,
            s.katu,
            s.n50,
            s.miss,
            s.grade,
            s.status.value,
            s.mode.as_vn,
            s.time,
            s.user.id,
            s.readable_mods,
            s.fc,
        ],
    )

    if s.status == scoreStatuses.Best:
        # set any other best scores to submitted ones as they've been overwritten
        await glob.db.execute(
            f"UPDATE {s.mode.table} SET status = 1 WHERE status = 2 AND uid = %s AND md5 = %s AND mode = %s AND id != %s",
            [s.user.id, s.map.md5, s.mode.as_vn, s.id],
        )

    # save replay if not a failed score
    if s.status != scoreStatuses.Failed:
        files = request.files

        try:
            replay = files["score"]
        except KeyError:
            await s.player.restrict("Missing replay file", fr=glob.bot)
            return b"error: ban"

        if replay == b"\r\n":
            await s.player.restrict("Missing replay file", fr=glob.bot)
            return b"error: ban"

        # i will make this auto-parse the replays one day when im not lazy
        if s.mods & Mods.RELAX:
            f = rx_path / f"{s.id}.osr"
        elif s.mods & Mods.AUTOPILOT:
            f = ap_path / f"{s.id}.osr"
        else:
            f = vn_path / f"{s.id}.osr"

        f.write_bytes(replay)

        if glob.config.anticheat and not s.user.priv & Privileges.BypassAnticheat:
            loop = asyncio.get_event_loop()
            loop.create_task(s.analyse())

    cap = glob.config.pp_caps[s.mode.value]

    if (cap is not None and s.pp >= cap and s.map.status & mapStatuses.GIVE_PP
            and glob.config.anticheat and not s.user.restricted
            and not s.user.priv & Privileges.Whitelisted):  # ugly

        await s.user.restrict(
            reason=f"Exceeding PP cap ({s.pp:,}pp) on {s.map.name}",
            fr=glob.bot,
        )

    # update stats EEEEEEE
    stats = s.user.stats[s.mode.value]
    old = copy.copy(
        stats)  # we need a copy of the old stats for submission chart

    elapsed = mpargs["st" if s.passed else "ft"]

    if not elapsed and s.user.priv & Privileges.BypassAnticheat:
        await s.user.restrict(
            "Modified client",
            fr=glob.bot,
        )  # its really only old version, but its supposed to be blocked on login. if it isnt present it must be modified to seem like a new version

    stats.playtime += int(elapsed) // 1000
    stats.tscore += s.score
    stats.pc += 1

    if s.status == scoreStatuses.Best and s.map.status >= mapStatuses.Ranked:
        add = s.score

        if s.old_best:
            add -= s.old_best.score

        if s.combo > old.max_combo:
            stats.max_combo = s.combo

        if s.map.status & mapStatuses.GIVE_PP:
            stats.rscore += add

    await s.user.update_stats(s.mode, s.mode.table, s.mode.as_vn)

    if not s.user.restricted:
        s.map.plays += 1
        if s.passed:
            s.map.passes += 1

        await glob.db.execute(
            "UPDATE maps SET plays = %s, passes = %s WHERE md5 = %s",
            [s.map.plays, s.map.passes, s.map.md5],
        )

    # sub charts bruh
    if s.status == scoreStatuses.Failed:
        info(
            f"[{s.mode!r}] {s.user.name} submitted a score on {s.map.name} ({s.status.name})",
        )
        return b"error: no"  # not actually erroring, score is already submitted we just want client to stop request as we cannot provide chart

    achievements = ""
    if (s.map.status & mapStatuses.GIVE_PP
            and not s.user.restricted):  # TODO: hush-hush etc. achievements
        achs = []
        for ach in glob.achievements:
            if ach in s.user.achievements:
                continue

            if ach.cond(s):
                await s.user.unlock_ach(ach)
                achs.append(ach)

        achievements = "/".join([a.format for a in achs])

    charts = []

    # could be done better
    def chart_format(name, b, a):
        return f'{name}Before:{b or ""}|{name}After:{a}'  # osu makes this so ugly man.

    # map info
    charts.append(
        f"beatmapId:{s.map.id}|"
        f"beatmapSetId:{s.map.sid}|"
        f"beatmapPlaycount:{s.map.plays}|"
        f"beatmapPasscount:{s.map.passes}|"
        f"approvedDate:{s.map.update}", )

    # score-specific ranking
    if s.map.status >= mapStatuses.Ranked:
        charts.append(
            "|".join(
                (
                    "chartId:beatmap",
                    f"chartUrl:{s.map.set_url}",
                    "chartName:Current Score",
                    *((  # wtaf
                        chart_format("rank", s.old_best.rank, s.rank),
                        chart_format("rankedScore", s.old_best.score, s.score),
                        chart_format("totalScore", s.old_best.score, s.score),
                        chart_format("maxCombo", s.old_best.combo, s.combo),
                        chart_format(
                            "accuracy",
                            round(s.old_best.acc, 2),
                            round(s.acc, 2),
                        ),
                        chart_format("pp", s.old_best.pp, s.pp),
                    ) if s.old_best else (
                        chart_format("rank", None, s.rank),
                        chart_format("rankedScore", None, s.score),
                        chart_format("totalScore", None, s.score),
                        chart_format("maxCombo", None, s.combo),
                        chart_format("accuracy", None, round(s.acc, 2)),
                        chart_format("pp", None, s.pp),
                    )),
                    f"onlineScoreId:{s.id}",
                ), ), )

    # overall user stats
    charts.append(
        "|".join((
            "chartId:overall",
            f"chartUrl:https://{glob.config.domain}/u/{s.user.id}",
            "chartName:Overall Stats",
            *((
                chart_format("rank", old.rank, stats.rank),
                chart_format("rankedScore", old.rscore, stats.rscore),
                chart_format("totalScore", old.tscore, stats.tscore),
                chart_format("maxCombo", old.max_combo, stats.max_combo),
                chart_format(
                    "accuracy",
                    round(old.acc, 2),
                    round(stats.acc, 2),
                ),
                chart_format("pp", old.pp, stats.pp),
            ) if old else (
                chart_format("rank", None, stats.rank),
                chart_format("rankedScore", None, stats.rscore),
                chart_format("totalScore", None, stats.tscore),
                chart_format("maxCombo", None, stats.max_combo),
                chart_format("accuracy", None, round(stats.acc, 2)),
                chart_format("pp", None, stats.pp),
            )),
            f"achievements-new:{achievements}",
        ), ), )

    if s.status == scoreStatuses.Best and s.map.status >= mapStatuses.Ranked:
        if s.rank == 1:  # announce #1 to announce channel cus they achieved #1
            await s.announce_n1()

        # update lb cache
        lb = getattr(s.map, s.mode.leaderboard)

        if lb:
            threading.Thread(
                target=lb.set_user_pb,
                args=(
                    s.user,
                    s,
                ),
            ).start()

    s.user.last_score = s

    info(
        f"[{s.mode!r}] {s.user.name} submitted a score on {s.map.name} ({s.status.name})",
    )
    return "\n".join(charts).encode()  # thank u osu