Ejemplo n.º 1
0
    def from_parsed_match(
            cls, parsed_match: app.packets.MultiplayerMatch) -> "Match":
        obj = cls()
        obj.mods = Mods(parsed_match.mods)

        obj.name = parsed_match.name
        obj.passwd = parsed_match.passwd

        obj.map_name = parsed_match.map_name
        obj.map_id = parsed_match.map_id
        obj.map_md5 = parsed_match.map_md5

        for slot, status, team, mods in zip(
                obj.slots,
                parsed_match.slot_statuses,
                parsed_match.slot_teams,
                parsed_match.slot_mods or [0] * 16,
        ):
            slot.status = SlotStatus(status)
            slot.team = MatchTeams(team)
            slot.mods = Mods(mods)

        # TODO: validate there is no hole here?
        obj.host_id = parsed_match.host_id

        obj.mode = GameMode(parsed_match.mode)
        obj.win_condition = MatchWinConditions(parsed_match.win_condition)
        obj.team_type = MatchTeamTypes(parsed_match.team_type)
        obj.freemods = parsed_match.freemods

        obj.seed = parsed_match.seed

        return obj
Ejemplo n.º 2
0
async def api_get_global_leaderboard(
    sort: Literal["tscore", "rscore", "pp", "acc"] = "pp",
    mode_arg: int = Query(0, alias="mode", ge=0, le=7),
    limit: int = Query(25, ge=1, le=100),
    offset: int = Query(0, min=0, max=2_147_483_647),
    country: Optional[str] = Query(None, min_length=2, max_length=2),
    db_conn: databases.core.Connection = Depends(acquire_db_conn),
):
    mode = GameMode(mode_arg)

    query_conditions = ["s.mode = :mode", "u.priv & 1", f"s.{sort} > 0"]
    query_parameters: dict[str, object] = {"mode": mode}

    if country is not None:
        query_conditions.append("u.country = :country")
        query_parameters["country"] = country

    rows = await db_conn.fetch_all(
        "SELECT u.id as player_id, u.name, u.country, s.tscore, s.rscore, "
        "s.pp, s.plays, s.playtime, s.acc, s.max_combo, "
        "s.xh_count, s.x_count, s.sh_count, s.s_count, s.a_count, "
        "c.id as clan_id, c.name as clan_name, c.tag as clan_tag "
        "FROM stats s "
        "LEFT JOIN users u USING (id) "
        "LEFT JOIN clans c ON u.clan_id = c.id "
        f"WHERE {' AND '.join(query_conditions)} "
        f"ORDER BY s.{sort} DESC LIMIT :offset, :limit",
        query_parameters | {"offset": offset, "limit": limit},
    )

    return ORJSONResponse(
        {"status": "success", "leaderboard": [dict(row) for row in rows]},
    )
Ejemplo n.º 3
0
    async def from_sql(cls, score_id: 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
        row = await app.state.services.database.fetch_one(
            "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 = :score_id",
            {"score_id": score_id},
        )

        if not row:
            return

        s = cls()

        s.id = row[0]
        s.bmap = await Beatmap.from_md5(row[1])
        s.player = await app.state.sessions.players.from_cache_or_sql(id=row[2])

        s.sr = 0.0  # TODO

        (
            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,
        ) = row[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
Ejemplo n.º 4
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"])
Ejemplo n.º 5
0
async def api_get_player_most_played(
    user_id: Optional[int] = Query(None, alias="id", ge=3, le=2_147_483_647),
    username: Optional[str] = Query(None, alias="name", regex=regexes.USERNAME.pattern),
    mode_arg: int = Query(0, alias="mode", ge=0, le=7),
    limit: int = Query(25, ge=1, le=100),
    db_conn: databases.core.Connection = Depends(acquire_db_conn),
):
    """Return the most played beatmaps of a given player."""
    # NOTE: this will almost certainly not scale well, lol.

    if user_id is not None:
        p = await app.state.sessions.players.from_cache_or_sql(id=user_id)
    elif username is not None:
        p = await app.state.sessions.players.from_cache_or_sql(name=username)
    else:
        return ORJSONResponse(
            {"status": "Must provide either id or name."},
            status_code=status.HTTP_400_BAD_REQUEST,
        )

    if not p:
        return ORJSONResponse(
            {"status": "Player not found."},
            status_code=status.HTTP_404_NOT_FOUND,
        )

    # parse args (mode, limit)

    mode = GameMode(mode_arg)

    # fetch & return info from sql
    rows = await db_conn.fetch_all(
        "SELECT m.md5, m.id, m.set_id, m.status, "
        "m.artist, m.title, m.version, m.creator, COUNT(*) plays "
        f"FROM {mode.scores_table} s "
        "INNER JOIN maps m ON m.md5 = s.map_md5 "
        "WHERE s.userid = :user_id "
        "AND s.mode = :mode_vn "
        "GROUP BY s.map_md5 "
        "ORDER BY plays DESC "
        "LIMIT :limit",
        {"user_id": p.id, "mode_vn": mode.as_vanilla, "limit": limit},
    )

    return ORJSONResponse(
        {
            "status": "success",
            "maps": [dict(row) for row in rows],
        },
    )
Ejemplo n.º 6
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
Ejemplo n.º 7
0
    def __init__(self, map_set: "BeatmapSet", **kwargs: Any) -> None:
        self.set = map_set

        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, ...), ...}}
Ejemplo n.º 8
0
async def api_get_map_scores(
    scope: Literal["recent", "best"],
    map_id: Optional[int] = Query(None, alias="id", ge=0, le=2_147_483_647),
    map_md5: Optional[str] = Query(None, alias="md5", min_length=32, max_length=32),
    mods_arg: Optional[str] = Query(None, alias="mods"),
    mode_arg: int = Query(0, alias="mode", ge=0, le=7),
    limit: int = Query(50, ge=1, le=100),
    db_conn: databases.core.Connection = Depends(acquire_db_conn),
):
    """Return the top n scores on a given beatmap."""
    if map_id is not None:
        bmap = await Beatmap.from_bid(map_id)
    elif map_md5 is not None:
        bmap = await Beatmap.from_md5(map_md5)
    else:
        return ORJSONResponse(
            {"status": "Must provide either id or md5!"},
            status_code=status.HTTP_400_BAD_REQUEST,
        )

    if not bmap:
        return ORJSONResponse(
            {"status": "Map not found."},
            status_code=status.HTTP_404_NOT_FOUND,
        )

    # parse args (scope, mode, mods, limit)

    mode = GameMode(mode_arg)

    if mods_arg is not None:
        if mods_arg[0] in ("~", "="):  # weak/strong equality
            strong_equality = mods_arg[0] == "="
            mods_arg = mods_arg[1:]
        else:  # use strong as default
            strong_equality = True

        if mods_arg.isdecimal():
            # parse from int form
            mods = Mods(int(mods_arg))
        else:
            # parse from string form
            mods = Mods.from_modstr(mods_arg)
    else:
        mods = None

    # NOTE: userid will eventually become player_id,
    # along with everywhere else in the codebase.
    query = [
        "SELECT s.map_md5, s.score, s.pp, s.acc, s.max_combo, s.mods, "
        "s.n300, s.n100, s.n50, s.nmiss, s.ngeki, s.nkatu, s.grade, s.status, "
        "s.mode, s.play_time, s.time_elapsed, s.userid, s.perfect, "
        "u.name player_name, "
        "c.id clan_id, c.name clan_name, c.tag clan_tag "
        f"FROM {mode.scores_table} s "
        "INNER JOIN users u ON u.id = s.userid "
        "LEFT JOIN clans c ON c.id = u.clan_id "
        "WHERE s.map_md5 = :map_md5 "
        "AND s.mode = :mode_vn "
        "AND s.status = 2 "
        "AND u.priv & 1",
    ]
    params: dict[str, object] = {
        "map_md5": bmap.md5,
        "mode_vn": mode.as_vanilla,
    }

    if mods is not None:
        if strong_equality:  # type: ignore
            query.append("AND mods & :mods = :mods")
        else:
            query.append("AND mods & :mods != 0")

        params["mods"] = mods

    # unlike /get_player_scores, we'll sort by score/pp depending
    # on the mode played, since we want to replicated leaderboards.
    if scope == "best":
        sort = "pp" if mode >= GameMode.RELAX_OSU else "score"
    else:  # recent
        sort = "play_time"

    query.append(f"ORDER BY {sort} DESC LIMIT :limit")
    params["limit"] = limit

    rows = await db_conn.fetch_all(" ".join(query), params)

    return ORJSONResponse(
        {
            "status": "success",
            "scores": [dict(row) for row in rows],
        },
    )
Ejemplo n.º 9
0
async def api_get_player_scores(
    scope: Literal["recent", "best"],
    user_id: Optional[int] = Query(None, alias="id", ge=3, le=2_147_483_647),
    username: Optional[str] = Query(None, alias="name", regex=regexes.USERNAME.pattern),
    mods_arg: Optional[str] = Query(None, alias="mods"),
    mode_arg: int = Query(0, alias="mode", ge=0, le=7),
    limit: int = Query(25, ge=1, le=100),
    include_loved: bool = False,
    include_failed: bool = True,
):
    """Return a list of a given user's recent/best scores."""
    if username and user_id:
        return ORJSONResponse(
            {"status": "Must provide either id OR name!"},
            status_code=status.HTTP_400_BAD_REQUEST,
        )

    if username:
        player = await app.state.sessions.players.from_cache_or_sql(name=username)
    elif user_id:
        player = await app.state.sessions.players.from_cache_or_sql(id=user_id)
    else:
        return ORJSONResponse(
            {"status": "Must provide either id OR name!"},
            status_code=status.HTTP_400_BAD_REQUEST,
        )

    if not player:
        return ORJSONResponse(
            {"status": "Player not found."},
            status_code=status.HTTP_404_NOT_FOUND,
        )

    # parse args (scope, mode, mods, limit)

    mode = GameMode(mode_arg)

    if mods_arg is not None:
        if mods_arg[0] in ("~", "="):  # weak/strong equality
            strong_equality = mods_arg[0] == "="
            mods_arg = mods_arg[1:]
        else:  # use strong as default
            strong_equality = True

        if mods_arg.isdecimal():
            # parse from int form
            mods = Mods(int(mods_arg))
        else:
            # parse from string form
            mods = Mods.from_modstr(mods_arg)
    else:
        mods = None

    # build sql query & fetch info

    query = [
        "SELECT t.id, t.map_md5, t.score, t.pp, t.acc, t.max_combo, "
        "t.mods, t.n300, t.n100, t.n50, t.nmiss, t.ngeki, t.nkatu, t.grade, "
        "t.status, t.mode, t.play_time, t.time_elapsed, t.perfect "
        f"FROM {mode.scores_table} t "
        "INNER JOIN maps b ON t.map_md5 = b.md5 "
        "WHERE t.userid = :user_id AND t.mode = :mode_vn",
    ]

    params: dict[str, object] = {
        "user_id": player.id,
        "mode_vn": mode.as_vanilla,
    }

    if mods is not None:
        if strong_equality:  # type: ignore
            query.append("AND t.mods & :mods = :mods")
        else:
            query.append("AND t.mods & :mods != 0")

        params["mods"] = mods

    if scope == "best":
        allowed_statuses = [2, 3]

        if include_loved:
            allowed_statuses.append(5)

        query.append("AND t.status = 2 AND b.status IN :statuses")
        params["statuses"] = allowed_statuses
        sort = "t.pp"
    else:
        if not include_failed:
            query.append("AND t.status != 0")

        sort = "t.play_time"

    query.append(f"ORDER BY {sort} DESC LIMIT :limit")
    params["limit"] = limit

    rows = [
        dict(row)
        for row in await app.state.services.database.fetch_all(" ".join(query), params)
    ]

    # fetch & return info from sql
    for row in rows:
        bmap = await Beatmap.from_md5(row.pop("map_md5"))
        row["beatmap"] = bmap.as_dict if bmap else None

    player_info = {
        "id": player.id,
        "name": player.name,
        "clan": {
            "id": player.clan.id,
            "name": player.clan.name,
            "tag": player.clan.tag,
        }
        if player.clan
        else None,
    }

    return ORJSONResponse(
        {
            "status": "success",
            "scores": rows,
            "player": player_info,
        },
    )