feat(score): support leaderboard for country/friends/team/selected mods
This commit is contained in:
@@ -14,6 +14,7 @@ from .lazer_user import (
|
|||||||
User,
|
User,
|
||||||
UserResp,
|
UserResp,
|
||||||
)
|
)
|
||||||
|
from .pp_best_score import PPBestScore
|
||||||
from .relationship import Relationship, RelationshipResp, RelationshipType
|
from .relationship import Relationship, RelationshipResp, RelationshipType
|
||||||
from .score import (
|
from .score import (
|
||||||
Score,
|
Score,
|
||||||
@@ -35,13 +36,13 @@ from .user_account_history import (
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Beatmap",
|
"Beatmap",
|
||||||
"BeatmapResp",
|
|
||||||
"Beatmapset",
|
"Beatmapset",
|
||||||
"BeatmapsetResp",
|
"BeatmapsetResp",
|
||||||
"BestScore",
|
"BestScore",
|
||||||
"DailyChallengeStats",
|
"DailyChallengeStats",
|
||||||
"DailyChallengeStatsResp",
|
"DailyChallengeStatsResp",
|
||||||
"OAuthToken",
|
"OAuthToken",
|
||||||
|
"PPBestScore",
|
||||||
"Relationship",
|
"Relationship",
|
||||||
"RelationshipResp",
|
"RelationshipResp",
|
||||||
"RelationshipType",
|
"RelationshipType",
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from app.models.score import GameMode
|
from app.models.score import GameMode, Rank
|
||||||
|
|
||||||
from .lazer_user import User
|
from .lazer_user import User
|
||||||
|
|
||||||
from sqlmodel import (
|
from sqlmodel import (
|
||||||
|
JSON,
|
||||||
BigInteger,
|
BigInteger,
|
||||||
Column,
|
Column,
|
||||||
Field,
|
Field,
|
||||||
Float,
|
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
Relationship,
|
Relationship,
|
||||||
SQLModel,
|
SQLModel,
|
||||||
@@ -20,7 +20,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class BestScore(SQLModel, table=True):
|
class BestScore(SQLModel, table=True):
|
||||||
__tablename__ = "best_scores" # pyright: ignore[reportAssignmentType]
|
__tablename__ = "total_score_best_scores" # pyright: ignore[reportAssignmentType]
|
||||||
user_id: int = Field(
|
user_id: int = Field(
|
||||||
sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), index=True)
|
sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), index=True)
|
||||||
)
|
)
|
||||||
@@ -29,13 +29,20 @@ class BestScore(SQLModel, table=True):
|
|||||||
)
|
)
|
||||||
beatmap_id: int = Field(foreign_key="beatmaps.id", index=True)
|
beatmap_id: int = Field(foreign_key="beatmaps.id", index=True)
|
||||||
gamemode: GameMode = Field(index=True)
|
gamemode: GameMode = Field(index=True)
|
||||||
pp: float = Field(
|
total_score: int = Field(
|
||||||
sa_column=Column(Float, default=0),
|
default=0, sa_column=Column(BigInteger, ForeignKey("scores.total_score"))
|
||||||
)
|
)
|
||||||
acc: float = Field(
|
mods: list[str] = Field(
|
||||||
sa_column=Column(Float, default=0),
|
default_factory=list,
|
||||||
|
sa_column=Column(JSON),
|
||||||
)
|
)
|
||||||
|
rank: Rank
|
||||||
|
|
||||||
user: User = Relationship()
|
user: User = Relationship()
|
||||||
score: "Score" = Relationship()
|
score: "Score" = Relationship(
|
||||||
|
sa_relationship_kwargs={
|
||||||
|
"foreign_keys": "[BestScore.score_id]",
|
||||||
|
"lazy": "joined",
|
||||||
|
}
|
||||||
|
)
|
||||||
beatmap: "Beatmap" = Relationship()
|
beatmap: "Beatmap" = Relationship()
|
||||||
|
|||||||
41
app/database/pp_best_score.py
Normal file
41
app/database/pp_best_score.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from app.models.score import GameMode
|
||||||
|
|
||||||
|
from .lazer_user import User
|
||||||
|
|
||||||
|
from sqlmodel import (
|
||||||
|
BigInteger,
|
||||||
|
Column,
|
||||||
|
Field,
|
||||||
|
Float,
|
||||||
|
ForeignKey,
|
||||||
|
Relationship,
|
||||||
|
SQLModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .beatmap import Beatmap
|
||||||
|
from .score import Score
|
||||||
|
|
||||||
|
|
||||||
|
class PPBestScore(SQLModel, table=True):
|
||||||
|
__tablename__ = "best_scores" # pyright: ignore[reportAssignmentType]
|
||||||
|
user_id: int = Field(
|
||||||
|
sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), index=True)
|
||||||
|
)
|
||||||
|
score_id: int = Field(
|
||||||
|
sa_column=Column(BigInteger, ForeignKey("scores.id"), primary_key=True)
|
||||||
|
)
|
||||||
|
beatmap_id: int = Field(foreign_key="beatmaps.id", index=True)
|
||||||
|
gamemode: GameMode = Field(index=True)
|
||||||
|
pp: float = Field(
|
||||||
|
sa_column=Column(Float, default=0),
|
||||||
|
)
|
||||||
|
acc: float = Field(
|
||||||
|
sa_column=Column(Float, default=0),
|
||||||
|
)
|
||||||
|
|
||||||
|
user: User = Relationship()
|
||||||
|
score: "Score" = Relationship()
|
||||||
|
beatmap: "Beatmap" = Relationship()
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from datetime import UTC, date, datetime
|
from datetime import UTC, date, datetime
|
||||||
|
import json
|
||||||
import math
|
import math
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ from app.calculator import (
|
|||||||
calculate_weighted_pp,
|
calculate_weighted_pp,
|
||||||
clamp,
|
clamp,
|
||||||
)
|
)
|
||||||
from app.models.beatmap import BeatmapRankStatus
|
from app.database.team import TeamMember
|
||||||
from app.models.model import UTCBaseModel
|
from app.models.model import UTCBaseModel
|
||||||
from app.models.mods import APIMod, mods_can_get_pp
|
from app.models.mods import APIMod, mods_can_get_pp
|
||||||
from app.models.score import (
|
from app.models.score import (
|
||||||
@@ -31,12 +32,18 @@ from .beatmapset import BeatmapsetResp
|
|||||||
from .best_score import BestScore
|
from .best_score import BestScore
|
||||||
from .lazer_user import User, UserResp
|
from .lazer_user import User, UserResp
|
||||||
from .monthly_playcounts import MonthlyPlaycounts
|
from .monthly_playcounts import MonthlyPlaycounts
|
||||||
|
from .pp_best_score import PPBestScore
|
||||||
|
from .relationship import (
|
||||||
|
Relationship as DBRelationship,
|
||||||
|
RelationshipType,
|
||||||
|
)
|
||||||
from .score_token import ScoreToken
|
from .score_token import ScoreToken
|
||||||
|
|
||||||
from redis import Redis
|
from redis import Redis
|
||||||
from sqlalchemy import Column, ColumnExpressionArgument, DateTime
|
from sqlalchemy import Column, ColumnExpressionArgument, DateTime
|
||||||
from sqlalchemy.ext.asyncio import AsyncAttrs
|
from sqlalchemy.ext.asyncio import AsyncAttrs
|
||||||
from sqlalchemy.orm import aliased
|
from sqlalchemy.orm import aliased
|
||||||
|
from sqlalchemy.sql.elements import ColumnElement
|
||||||
from sqlmodel import (
|
from sqlmodel import (
|
||||||
JSON,
|
JSON,
|
||||||
BigInteger,
|
BigInteger,
|
||||||
@@ -45,9 +52,10 @@ from sqlmodel import (
|
|||||||
Relationship,
|
Relationship,
|
||||||
SQLModel,
|
SQLModel,
|
||||||
col,
|
col,
|
||||||
false,
|
|
||||||
func,
|
func,
|
||||||
select,
|
select,
|
||||||
|
text,
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
from sqlmodel.sql._expression_select_cls import SelectOfScalar
|
from sqlmodel.sql._expression_select_cls import SelectOfScalar
|
||||||
@@ -156,9 +164,7 @@ class ScoreResp(ScoreBase):
|
|||||||
rank_country: int | None = None
|
rank_country: int | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def from_db(
|
async def from_db(cls, session: AsyncSession, score: Score) -> "ScoreResp":
|
||||||
cls, session: AsyncSession, score: Score, user: User | None = None
|
|
||||||
) -> "ScoreResp":
|
|
||||||
s = cls.model_validate(score.model_dump())
|
s = cls.model_validate(score.model_dump())
|
||||||
assert score.id
|
assert score.id
|
||||||
await score.awaitable_attrs.beatmap
|
await score.awaitable_attrs.beatmap
|
||||||
@@ -195,30 +201,30 @@ class ScoreResp(ScoreBase):
|
|||||||
s.maximum_statistics = {
|
s.maximum_statistics = {
|
||||||
HitResult.GREAT: score.beatmap.max_combo,
|
HitResult.GREAT: score.beatmap.max_combo,
|
||||||
}
|
}
|
||||||
if user:
|
s.user = await UserResp.from_db(
|
||||||
s.user = await UserResp.from_db(
|
score.user,
|
||||||
user,
|
session,
|
||||||
session,
|
include=["statistics", "team", "daily_challenge_user_stats"],
|
||||||
include=["statistics", "team", "daily_challenge_user_stats"],
|
ruleset=score.gamemode,
|
||||||
ruleset=score.gamemode,
|
)
|
||||||
)
|
|
||||||
s.rank_global = (
|
s.rank_global = (
|
||||||
await get_score_position_by_id(
|
await get_score_position_by_id(
|
||||||
session,
|
session,
|
||||||
score.map_md5,
|
score.beatmap_id,
|
||||||
score.id,
|
score.id,
|
||||||
mode=score.gamemode,
|
mode=score.gamemode,
|
||||||
user=user or score.user,
|
user=score.user,
|
||||||
)
|
)
|
||||||
or None
|
or None
|
||||||
)
|
)
|
||||||
s.rank_country = (
|
s.rank_country = (
|
||||||
await get_score_position_by_id(
|
await get_score_position_by_id(
|
||||||
session,
|
session,
|
||||||
score.map_md5,
|
score.beatmap_id,
|
||||||
score.id,
|
score.id,
|
||||||
score.gamemode,
|
score.gamemode,
|
||||||
user or score.user,
|
score.user,
|
||||||
|
type=LeaderboardType.COUNTRY,
|
||||||
)
|
)
|
||||||
or None
|
or None
|
||||||
)
|
)
|
||||||
@@ -228,134 +234,137 @@ class ScoreResp(ScoreBase):
|
|||||||
async def get_best_id(session: AsyncSession, score_id: int) -> None:
|
async def get_best_id(session: AsyncSession, score_id: int) -> None:
|
||||||
rownum = (
|
rownum = (
|
||||||
func.row_number()
|
func.row_number()
|
||||||
.over(partition_by=col(BestScore.user_id), order_by=col(BestScore.pp).desc())
|
.over(
|
||||||
|
partition_by=col(PPBestScore.user_id), order_by=col(PPBestScore.pp).desc()
|
||||||
|
)
|
||||||
.label("rn")
|
.label("rn")
|
||||||
)
|
)
|
||||||
subq = select(BestScore, rownum).subquery()
|
subq = select(PPBestScore, rownum).subquery()
|
||||||
stmt = select(subq.c.rn).where(subq.c.score_id == score_id)
|
stmt = select(subq.c.rn).where(subq.c.score_id == score_id)
|
||||||
result = await session.exec(stmt)
|
result = await session.exec(stmt)
|
||||||
return result.one_or_none()
|
return result.one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def _score_where(
|
||||||
|
type: LeaderboardType,
|
||||||
|
beatmap: int,
|
||||||
|
mode: GameMode,
|
||||||
|
mods: list[str] | None = None,
|
||||||
|
user: User | None = None,
|
||||||
|
) -> list[ColumnElement[bool]] | None:
|
||||||
|
wheres = [
|
||||||
|
col(BestScore.beatmap_id) == beatmap,
|
||||||
|
col(BestScore.gamemode) == mode,
|
||||||
|
]
|
||||||
|
|
||||||
|
if type == LeaderboardType.FRIENDS:
|
||||||
|
if user and user.is_supporter:
|
||||||
|
subq = (
|
||||||
|
select(DBRelationship.target_id)
|
||||||
|
.where(
|
||||||
|
DBRelationship.type == RelationshipType.FOLLOW,
|
||||||
|
DBRelationship.user_id == user.id,
|
||||||
|
)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
wheres.append(col(BestScore.user_id).in_(select(subq.c.target_id)))
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
elif type == LeaderboardType.COUNTRY:
|
||||||
|
if user and user.is_supporter:
|
||||||
|
wheres.append(
|
||||||
|
col(BestScore.user).has(col(User.country_code) == user.country_code)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
elif type == LeaderboardType.TEAM:
|
||||||
|
if user:
|
||||||
|
team_membership = await user.awaitable_attrs.team_membership
|
||||||
|
if team_membership:
|
||||||
|
team_id = team_membership.team_id
|
||||||
|
wheres.append(
|
||||||
|
col(BestScore.user).has(
|
||||||
|
col(User.team_membership).has(TeamMember.team_id == team_id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if mods:
|
||||||
|
if user and user.is_supporter:
|
||||||
|
wheres.append(
|
||||||
|
text(
|
||||||
|
"JSON_CONTAINS(total_score_best_scores.mods, :w)"
|
||||||
|
" AND JSON_CONTAINS(:w, total_score_best_scores.mods)"
|
||||||
|
) # pyright: ignore[reportArgumentType]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
return wheres
|
||||||
|
|
||||||
|
|
||||||
async def get_leaderboard(
|
async def get_leaderboard(
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
beatmap_md5: str,
|
beatmap: int,
|
||||||
mode: GameMode,
|
mode: GameMode,
|
||||||
type: LeaderboardType = LeaderboardType.GLOBAL,
|
type: LeaderboardType = LeaderboardType.GLOBAL,
|
||||||
mods: list[APIMod] | None = None,
|
mods: list[str] | None = None,
|
||||||
user: User | None = None,
|
user: User | None = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
) -> list[Score]:
|
) -> tuple[list[Score], Score | None]:
|
||||||
scores = []
|
wheres = await _score_where(type, beatmap, mode, mods, user)
|
||||||
if type == LeaderboardType.GLOBAL:
|
if wheres is None:
|
||||||
query = (
|
return [], None
|
||||||
select(Score)
|
query = (
|
||||||
.where(
|
select(BestScore)
|
||||||
col(Beatmap.beatmap_status).in_(
|
.where(*wheres)
|
||||||
[
|
.limit(limit)
|
||||||
BeatmapRankStatus.RANKED,
|
.order_by(col(BestScore.total_score).desc())
|
||||||
BeatmapRankStatus.LOVED,
|
)
|
||||||
BeatmapRankStatus.QUALIFIED,
|
if mods:
|
||||||
BeatmapRankStatus.APPROVED,
|
query = query.params(w=json.dumps(mods))
|
||||||
]
|
scores = [s.score for s in await session.exec(query)]
|
||||||
),
|
user_score = None
|
||||||
Score.map_md5 == beatmap_md5,
|
|
||||||
Score.gamemode == mode,
|
|
||||||
col(Score.passed).is_(True),
|
|
||||||
Score.mods == mods if user and user.is_supporter else false(),
|
|
||||||
)
|
|
||||||
.limit(limit)
|
|
||||||
.order_by(
|
|
||||||
col(Score.total_score).desc(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
result = await session.exec(query)
|
|
||||||
scores = list[Score](result.all())
|
|
||||||
elif type == LeaderboardType.FRIENDS and user and user.is_supporter:
|
|
||||||
# TODO
|
|
||||||
...
|
|
||||||
elif type == LeaderboardType.TEAM and user and user.team_membership:
|
|
||||||
team_id = user.team_membership.team_id
|
|
||||||
query = (
|
|
||||||
select(Score)
|
|
||||||
.join(Beatmap)
|
|
||||||
.where(
|
|
||||||
Score.map_md5 == beatmap_md5,
|
|
||||||
Score.gamemode == mode,
|
|
||||||
col(Score.passed).is_(True),
|
|
||||||
col(Score.user.team_membership).is_not(None),
|
|
||||||
Score.user.team_membership.team_id == team_id, # pyright: ignore[reportOptionalMemberAccess]
|
|
||||||
Score.mods == mods if user and user.is_supporter else false(),
|
|
||||||
)
|
|
||||||
.limit(limit)
|
|
||||||
.order_by(
|
|
||||||
col(Score.total_score).desc(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
result = await session.exec(query)
|
|
||||||
scores = list[Score](result.all())
|
|
||||||
if user:
|
if user:
|
||||||
user_score = (
|
self_query = (
|
||||||
await session.exec(
|
select(BestScore)
|
||||||
select(Score).where(
|
.where(BestScore.user_id == user.id)
|
||||||
Score.map_md5 == beatmap_md5,
|
.order_by(col(BestScore.total_score).desc())
|
||||||
Score.gamemode == mode,
|
.limit(1)
|
||||||
Score.user_id == user.id,
|
)
|
||||||
col(Score.passed).is_(True),
|
if mods:
|
||||||
|
self_query = self_query.where(
|
||||||
|
text(
|
||||||
|
"JSON_CONTAINS(total_score_best_scores.mods, :w)"
|
||||||
|
" AND JSON_CONTAINS(:w, total_score_best_scores.mods)"
|
||||||
)
|
)
|
||||||
)
|
).params(w=json.dumps(mods))
|
||||||
).first()
|
user_bs = (await session.exec(self_query)).first()
|
||||||
|
if user_bs:
|
||||||
|
user_score = user_bs.score
|
||||||
if user_score and user_score not in scores:
|
if user_score and user_score not in scores:
|
||||||
scores.append(user_score)
|
scores.append(user_score)
|
||||||
return scores
|
return scores, user_score
|
||||||
|
|
||||||
|
|
||||||
async def get_score_position_by_user(
|
async def get_score_position_by_user(
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
beatmap_md5: str,
|
beatmap: int,
|
||||||
user: User,
|
user: User,
|
||||||
mode: GameMode,
|
mode: GameMode,
|
||||||
type: LeaderboardType = LeaderboardType.GLOBAL,
|
type: LeaderboardType = LeaderboardType.GLOBAL,
|
||||||
mods: list[APIMod] | None = None,
|
mods: list[str] | None = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
where_clause = [
|
wheres = await _score_where(type, beatmap, mode, mods, user=user)
|
||||||
Score.map_md5 == beatmap_md5,
|
if wheres is None:
|
||||||
Score.gamemode == mode,
|
return 0
|
||||||
col(Score.passed).is_(True),
|
|
||||||
col(Beatmap.beatmap_status).in_(
|
|
||||||
[
|
|
||||||
BeatmapRankStatus.RANKED,
|
|
||||||
BeatmapRankStatus.LOVED,
|
|
||||||
BeatmapRankStatus.QUALIFIED,
|
|
||||||
BeatmapRankStatus.APPROVED,
|
|
||||||
]
|
|
||||||
),
|
|
||||||
]
|
|
||||||
if mods and user.is_supporter:
|
|
||||||
where_clause.append(Score.mods == mods)
|
|
||||||
else:
|
|
||||||
where_clause.append(false())
|
|
||||||
if type == LeaderboardType.FRIENDS and user.is_supporter:
|
|
||||||
# TODO
|
|
||||||
...
|
|
||||||
elif type == LeaderboardType.TEAM and user.team_membership:
|
|
||||||
team_id = user.team_membership.team_id
|
|
||||||
where_clause.append(
|
|
||||||
col(Score.user.team_membership).is_not(None),
|
|
||||||
)
|
|
||||||
where_clause.append(
|
|
||||||
Score.user.team_membership.team_id == team_id, # pyright: ignore[reportOptionalMemberAccess]
|
|
||||||
)
|
|
||||||
rownum = (
|
rownum = (
|
||||||
func.row_number()
|
func.row_number()
|
||||||
.over(
|
.over(
|
||||||
partition_by=Score.map_md5,
|
partition_by=col(BestScore.beatmap_id),
|
||||||
order_by=col(Score.total_score).desc(),
|
order_by=col(BestScore.total_score).desc(),
|
||||||
)
|
)
|
||||||
.label("row_number")
|
.label("row_number")
|
||||||
)
|
)
|
||||||
subq = select(Score, rownum).join(Beatmap).where(*where_clause).subquery()
|
subq = select(BestScore, rownum).join(Beatmap).where(*wheres).subquery()
|
||||||
stmt = select(subq.c.row_number).where(subq.c.user == user)
|
stmt = select(subq.c.row_number).where(subq.c.user_id == user.id)
|
||||||
result = await session.exec(stmt)
|
result = await session.exec(stmt)
|
||||||
s = result.one_or_none()
|
s = result.one_or_none()
|
||||||
return s if s else 0
|
return s if s else 0
|
||||||
@@ -363,57 +372,26 @@ async def get_score_position_by_user(
|
|||||||
|
|
||||||
async def get_score_position_by_id(
|
async def get_score_position_by_id(
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
beatmap_md5: str,
|
beatmap: int,
|
||||||
score_id: int,
|
score_id: int,
|
||||||
mode: GameMode,
|
mode: GameMode,
|
||||||
user: User | None = None,
|
user: User | None = None,
|
||||||
type: LeaderboardType = LeaderboardType.GLOBAL,
|
type: LeaderboardType = LeaderboardType.GLOBAL,
|
||||||
mods: list[APIMod] | None = None,
|
mods: list[str] | None = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
where_clause = [
|
wheres = await _score_where(type, beatmap, mode, mods, user=user)
|
||||||
Score.map_md5 == beatmap_md5,
|
if wheres is None:
|
||||||
Score.id == score_id,
|
return 0
|
||||||
Score.gamemode == mode,
|
|
||||||
col(Score.passed).is_(True),
|
|
||||||
col(Beatmap.beatmap_status).in_(
|
|
||||||
[
|
|
||||||
BeatmapRankStatus.RANKED,
|
|
||||||
BeatmapRankStatus.LOVED,
|
|
||||||
BeatmapRankStatus.QUALIFIED,
|
|
||||||
BeatmapRankStatus.APPROVED,
|
|
||||||
]
|
|
||||||
),
|
|
||||||
]
|
|
||||||
if mods and user and user.is_supporter:
|
|
||||||
where_clause.append(Score.mods == mods)
|
|
||||||
elif mods:
|
|
||||||
where_clause.append(false())
|
|
||||||
rownum = (
|
rownum = (
|
||||||
func.row_number()
|
func.row_number()
|
||||||
.over(
|
.over(
|
||||||
partition_by=[col(Score.user_id), col(Score.map_md5)],
|
partition_by=col(BestScore.beatmap_id),
|
||||||
order_by=col(Score.total_score).desc(),
|
order_by=col(BestScore.total_score).desc(),
|
||||||
)
|
)
|
||||||
.label("rownum")
|
.label("row_number")
|
||||||
)
|
)
|
||||||
subq = (
|
subq = select(BestScore, rownum).join(Beatmap).where(*wheres).subquery()
|
||||||
select(Score.user_id, Score.id, Score.total_score, rownum)
|
stmt = select(subq.c.row_number).where(subq.c.score_id == score_id)
|
||||||
.join(Beatmap)
|
|
||||||
.where(*where_clause)
|
|
||||||
.subquery()
|
|
||||||
)
|
|
||||||
best_scores = aliased(subq)
|
|
||||||
overall_rank = (
|
|
||||||
func.rank().over(order_by=best_scores.c.total_score.desc()).label("global_rank")
|
|
||||||
)
|
|
||||||
final_q = (
|
|
||||||
select(best_scores.c.id, overall_rank)
|
|
||||||
.select_from(best_scores)
|
|
||||||
.where(best_scores.c.rownum == 1)
|
|
||||||
.subquery()
|
|
||||||
)
|
|
||||||
|
|
||||||
stmt = select(final_q.c.global_rank).where(final_q.c.id == score_id)
|
|
||||||
result = await session.exec(stmt)
|
result = await session.exec(stmt)
|
||||||
s = result.one_or_none()
|
s = result.one_or_none()
|
||||||
return s if s else 0
|
return s if s else 0
|
||||||
@@ -424,16 +402,38 @@ async def get_user_best_score_in_beatmap(
|
|||||||
beatmap: int,
|
beatmap: int,
|
||||||
user: int,
|
user: int,
|
||||||
mode: GameMode | None = None,
|
mode: GameMode | None = None,
|
||||||
) -> Score | None:
|
) -> BestScore | None:
|
||||||
return (
|
return (
|
||||||
await session.exec(
|
await session.exec(
|
||||||
select(Score)
|
select(BestScore)
|
||||||
.where(
|
.where(
|
||||||
Score.gamemode == mode if mode is not None else True,
|
BestScore.gamemode == mode if mode is not None else true(),
|
||||||
Score.beatmap_id == beatmap,
|
BestScore.beatmap_id == beatmap,
|
||||||
Score.user_id == user,
|
BestScore.user_id == user,
|
||||||
)
|
)
|
||||||
.order_by(col(Score.total_score).desc())
|
.order_by(col(BestScore.total_score).desc())
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
|
||||||
|
# FIXME
|
||||||
|
async def get_user_best_score_with_mod_in_beatmap(
|
||||||
|
session: AsyncSession,
|
||||||
|
beatmap: int,
|
||||||
|
user: int,
|
||||||
|
mod: list[str],
|
||||||
|
mode: GameMode | None = None,
|
||||||
|
) -> BestScore | None:
|
||||||
|
return (
|
||||||
|
await session.exec(
|
||||||
|
select(BestScore)
|
||||||
|
.where(
|
||||||
|
BestScore.gamemode == mode if mode is not None else True,
|
||||||
|
BestScore.beatmap_id == beatmap,
|
||||||
|
BestScore.user_id == user,
|
||||||
|
# BestScore.mods == mod,
|
||||||
|
)
|
||||||
|
.order_by(col(BestScore.total_score).desc())
|
||||||
)
|
)
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
@@ -443,13 +443,13 @@ async def get_user_best_pp_in_beatmap(
|
|||||||
beatmap: int,
|
beatmap: int,
|
||||||
user: int,
|
user: int,
|
||||||
mode: GameMode,
|
mode: GameMode,
|
||||||
) -> BestScore | None:
|
) -> PPBestScore | None:
|
||||||
return (
|
return (
|
||||||
await session.exec(
|
await session.exec(
|
||||||
select(BestScore).where(
|
select(PPBestScore).where(
|
||||||
BestScore.beatmap_id == beatmap,
|
PPBestScore.beatmap_id == beatmap,
|
||||||
BestScore.user_id == user,
|
PPBestScore.user_id == user,
|
||||||
BestScore.gamemode == mode,
|
PPBestScore.gamemode == mode,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
).first()
|
).first()
|
||||||
@@ -459,12 +459,12 @@ async def get_user_best_pp(
|
|||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
user: int,
|
user: int,
|
||||||
limit: int = 200,
|
limit: int = 200,
|
||||||
) -> Sequence[BestScore]:
|
) -> Sequence[PPBestScore]:
|
||||||
return (
|
return (
|
||||||
await session.exec(
|
await session.exec(
|
||||||
select(BestScore)
|
select(PPBestScore)
|
||||||
.where(BestScore.user_id == user)
|
.where(PPBestScore.user_id == user)
|
||||||
.order_by(col(BestScore.pp).desc())
|
.order_by(col(PPBestScore.pp).desc())
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
)
|
)
|
||||||
).all()
|
).all()
|
||||||
@@ -474,9 +474,15 @@ async def process_user(
|
|||||||
session: AsyncSession, user: User, score: Score, ranked: bool = False
|
session: AsyncSession, user: User, score: Score, ranked: bool = False
|
||||||
):
|
):
|
||||||
assert user.id
|
assert user.id
|
||||||
|
assert score.id
|
||||||
|
mod_for_save = list({mod["acronym"] for mod in score.mods})
|
||||||
previous_score_best = await get_user_best_score_in_beatmap(
|
previous_score_best = await get_user_best_score_in_beatmap(
|
||||||
session, score.beatmap_id, user.id, score.gamemode
|
session, score.beatmap_id, user.id, score.gamemode
|
||||||
)
|
)
|
||||||
|
previous_score_best_mod = await get_user_best_score_with_mod_in_beatmap(
|
||||||
|
session, score.beatmap_id, user.id, mod_for_save, score.gamemode
|
||||||
|
)
|
||||||
|
print(previous_score_best, previous_score_best_mod)
|
||||||
add_to_db = False
|
add_to_db = False
|
||||||
mouthly_playcount = (
|
mouthly_playcount = (
|
||||||
await session.exec(
|
await session.exec(
|
||||||
@@ -493,7 +499,7 @@ async def process_user(
|
|||||||
)
|
)
|
||||||
add_to_db = True
|
add_to_db = True
|
||||||
statistics = None
|
statistics = None
|
||||||
for i in user.statistics:
|
for i in await user.awaitable_attrs.statistics:
|
||||||
if i.mode == score.gamemode.value:
|
if i.mode == score.gamemode.value:
|
||||||
statistics = i
|
statistics = i
|
||||||
break
|
break
|
||||||
@@ -506,7 +512,7 @@ async def process_user(
|
|||||||
statistics.total_score += score.total_score
|
statistics.total_score += score.total_score
|
||||||
difference = (
|
difference = (
|
||||||
score.total_score - previous_score_best.total_score
|
score.total_score - previous_score_best.total_score
|
||||||
if previous_score_best and previous_score_best.id != score.id
|
if previous_score_best
|
||||||
else score.total_score
|
else score.total_score
|
||||||
)
|
)
|
||||||
if difference > 0 and score.passed and ranked:
|
if difference > 0 and score.passed and ranked:
|
||||||
@@ -533,9 +539,41 @@ async def process_user(
|
|||||||
statistics.grade_sh -= 1
|
statistics.grade_sh -= 1
|
||||||
case Rank.A:
|
case Rank.A:
|
||||||
statistics.grade_a -= 1
|
statistics.grade_a -= 1
|
||||||
|
else:
|
||||||
|
previous_score_best = BestScore(
|
||||||
|
user_id=user.id,
|
||||||
|
beatmap_id=score.beatmap_id,
|
||||||
|
gamemode=score.gamemode,
|
||||||
|
score_id=score.id,
|
||||||
|
total_score=score.total_score,
|
||||||
|
rank=score.rank,
|
||||||
|
mods=mod_for_save,
|
||||||
|
)
|
||||||
|
session.add(previous_score_best)
|
||||||
|
|
||||||
statistics.ranked_score += difference
|
statistics.ranked_score += difference
|
||||||
statistics.level_current = calculate_score_to_level(statistics.ranked_score)
|
statistics.level_current = calculate_score_to_level(statistics.ranked_score)
|
||||||
statistics.maximum_combo = max(statistics.maximum_combo, score.max_combo)
|
statistics.maximum_combo = max(statistics.maximum_combo, score.max_combo)
|
||||||
|
if score.passed and ranked:
|
||||||
|
if previous_score_best_mod is not None:
|
||||||
|
previous_score_best_mod.mods = mod_for_save
|
||||||
|
previous_score_best_mod.score_id = score.id
|
||||||
|
previous_score_best_mod.rank = score.rank
|
||||||
|
previous_score_best_mod.total_score = score.total_score
|
||||||
|
elif (
|
||||||
|
previous_score_best is not None and previous_score_best.score_id != score.id
|
||||||
|
):
|
||||||
|
session.add(
|
||||||
|
BestScore(
|
||||||
|
user_id=user.id,
|
||||||
|
beatmap_id=score.beatmap_id,
|
||||||
|
gamemode=score.gamemode,
|
||||||
|
score_id=score.id,
|
||||||
|
total_score=score.total_score,
|
||||||
|
rank=score.rank,
|
||||||
|
mods=mod_for_save,
|
||||||
|
)
|
||||||
|
)
|
||||||
statistics.play_count += 1
|
statistics.play_count += 1
|
||||||
mouthly_playcount.playcount += 1
|
mouthly_playcount.playcount += 1
|
||||||
statistics.play_time += int((score.ended_at - score.started_at).total_seconds())
|
statistics.play_time += int((score.ended_at - score.started_at).total_seconds())
|
||||||
@@ -623,7 +661,7 @@ async def process_score(
|
|||||||
)
|
)
|
||||||
if previous_pp_best is None or score.pp > previous_pp_best.pp:
|
if previous_pp_best is None or score.pp > previous_pp_best.pp:
|
||||||
assert score.id
|
assert score.id
|
||||||
best_score = BestScore(
|
best_score = PPBestScore(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
score_id=score.id,
|
score_id=score.id,
|
||||||
beatmap_id=beatmap_id,
|
beatmap_id=beatmap_id,
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ class HitResultInt(IntEnum):
|
|||||||
|
|
||||||
class LeaderboardType(Enum):
|
class LeaderboardType(Enum):
|
||||||
GLOBAL = "global"
|
GLOBAL = "global"
|
||||||
FRIENDS = "friends"
|
FRIENDS = "friend"
|
||||||
COUNTRY = "country"
|
COUNTRY = "country"
|
||||||
TEAM = "team"
|
TEAM = "team"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from app.database import Beatmap, Score, ScoreResp, ScoreToken, ScoreTokenResp, User
|
from app.database import Beatmap, Score, ScoreResp, ScoreToken, ScoreTokenResp, User
|
||||||
from app.database.score import process_score, process_user
|
from app.database.score import get_leaderboard, process_score, process_user
|
||||||
from app.dependencies.database import get_db, get_redis
|
from app.dependencies.database import get_db, get_redis
|
||||||
from app.dependencies.fetcher import get_fetcher
|
from app.dependencies.fetcher import get_fetcher
|
||||||
from app.dependencies.user import get_current_user
|
from app.dependencies.user import get_current_user
|
||||||
@@ -9,6 +9,7 @@ from app.models.beatmap import BeatmapRankStatus
|
|||||||
from app.models.score import (
|
from app.models.score import (
|
||||||
INT_TO_MODE,
|
INT_TO_MODE,
|
||||||
GameMode,
|
GameMode,
|
||||||
|
LeaderboardType,
|
||||||
Rank,
|
Rank,
|
||||||
SoloScoreSubmissionInfo,
|
SoloScoreSubmissionInfo,
|
||||||
)
|
)
|
||||||
@@ -19,7 +20,7 @@ from fastapi import Depends, Form, HTTPException, Query
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from redis import Redis
|
from redis import Redis
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
from sqlmodel import col, select, true
|
from sqlmodel import col, select
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
@@ -33,44 +34,26 @@ class BeatmapScores(BaseModel):
|
|||||||
)
|
)
|
||||||
async def get_beatmap_scores(
|
async def get_beatmap_scores(
|
||||||
beatmap: int,
|
beatmap: int,
|
||||||
|
mode: GameMode,
|
||||||
legacy_only: bool = Query(None), # TODO:加入对这个参数的查询
|
legacy_only: bool = Query(None), # TODO:加入对这个参数的查询
|
||||||
mode: GameMode | None = Query(None),
|
mods: list[str] = Query(default_factory=set, alias="mods[]"),
|
||||||
# mods: List[APIMod] = Query(None), # TODO:加入指定MOD的查询
|
type: LeaderboardType = Query(LeaderboardType.GLOBAL),
|
||||||
type: str = Query(None),
|
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
|
limit: int = Query(50, ge=1, le=200),
|
||||||
):
|
):
|
||||||
if legacy_only:
|
if legacy_only:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=404, detail="this server only contains lazer scores"
|
status_code=404, detail="this server only contains lazer scores"
|
||||||
)
|
)
|
||||||
|
|
||||||
all_scores = (
|
all_scores, user_score = await get_leaderboard(
|
||||||
await db.exec(
|
db, beatmap, mode, type=type, user=current_user, limit=limit, mods=mods
|
||||||
Score.select_clause_unique(
|
)
|
||||||
Score.beatmap_id == beatmap,
|
|
||||||
col(Score.passed).is_(True),
|
|
||||||
Score.gamemode == mode if mode is not None else true(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
user_score = (
|
|
||||||
await db.exec(
|
|
||||||
Score.select_clause_unique(
|
|
||||||
Score.beatmap_id == beatmap,
|
|
||||||
Score.user_id == current_user.id,
|
|
||||||
col(Score.passed).is_(True),
|
|
||||||
Score.gamemode == mode if mode is not None else true(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
return BeatmapScores(
|
return BeatmapScores(
|
||||||
scores=[await ScoreResp.from_db(db, score, score.user) for score in all_scores],
|
scores=[await ScoreResp.from_db(db, score) for score in all_scores],
|
||||||
userScore=await ScoreResp.from_db(db, user_score, user_score.user)
|
userScore=await ScoreResp.from_db(db, user_score) if user_score else None,
|
||||||
if user_score
|
|
||||||
else None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -116,7 +99,7 @@ async def get_user_beatmap_score(
|
|||||||
else:
|
else:
|
||||||
return BeatmapUserScore(
|
return BeatmapUserScore(
|
||||||
position=user_score.position if user_score.position is not None else 0,
|
position=user_score.position if user_score.position is not None else 0,
|
||||||
score=await ScoreResp.from_db(db, user_score, user_score.user),
|
score=await ScoreResp.from_db(db, user_score),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -149,9 +132,7 @@ async def get_user_all_beatmap_scores(
|
|||||||
)
|
)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
return [
|
return [await ScoreResp.from_db(db, score) for score in all_user_scores]
|
||||||
await ScoreResp.from_db(db, score, current_user) for score in all_user_scores
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
@@ -243,4 +224,4 @@ async def submit_solo_score(
|
|||||||
await process_user(db, current_user, score, ranked)
|
await process_user(db, current_user, score, ranked)
|
||||||
score = (await db.exec(select(Score).where(Score.id == score_id))).first()
|
score = (await db.exec(select(Score).where(Score.id == score_id))).first()
|
||||||
assert score is not None
|
assert score is not None
|
||||||
return await ScoreResp.from_db(db, score, current_user)
|
return await ScoreResp.from_db(db, score)
|
||||||
|
|||||||
Reference in New Issue
Block a user