From 605ad934ccc623843478b42086073c8ed0fffc35 Mon Sep 17 00:00:00 2001 From: jimmy-sketch Date: Tue, 29 Jul 2025 06:17:56 +0000 Subject: [PATCH] =?UTF-8?q?refactor(multiplayer):=20=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=AB=AF=E6=88=BF=E9=97=B4=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/room.py | 297 ++++++++++++++++++++++++++------------------- 1 file changed, 169 insertions(+), 128 deletions(-) diff --git a/app/models/room.py b/app/models/room.py index 793635e..f8a4a73 100644 --- a/app/models/room.py +++ b/app/models/room.py @@ -2,13 +2,16 @@ from __future__ import annotations from datetime import datetime, timedelta from enum import Enum -from typing import Any -from app.database.beatmap import Beatmap +from app.database.beatmap import Beatmap, BeatmapResp +from app.database.user import User as DBUser +from app.fetcher import Fetcher from app.models.mods import APIMod -from app.models.user import APIUser +from app.models.user import User +from app.utils import convert_db_user_to_api_user from pydantic import BaseModel +from sqlmodel.ext.asyncio.session import AsyncSession class RoomCategory(str, Enum): @@ -61,57 +64,15 @@ class MultiplayerUserState(str, Enum): class DownloadState(str, Enum): - UNKONWN = "unkown" + UNKOWN = "unkown" NOT_DOWNLOADED = "not_downloaded" DOWNLOADING = "downloading" IMPORTING = "importing" LOCALLY_AVAILABLE = "locally_available" -class BeatmapAvailability(BaseModel): - state: DownloadState - downloadProgress: float | None - - -class MultiplayerPlaylistItem(BaseModel): - id: int = 0 - ownerId: int = 0 - beatmapId: int = 0 - beatmapChecksum: str = "" - rulesetId: int = 0 - requiredMods: list[APIMod] = [] - allowedMods: list[APIMod] = [] - expired: bool = False - playlistOrder: int = 0 - playedAt: datetime | None - starRating: float = 0.0 - freestyle: bool = False - - -class MultiplayerRoomSettings(BaseModel): - name: str = "Unnamed room" - playlistItemId: int = 0 - password: str = "" - matchType: MatchType = MatchType.HEAD_TO_HEAD - queueMode: QueueMode = QueueMode.HOST_ONLY - autoStartDuration: timedelta = timedelta(0) - autoSkip: bool = False - - -class MultiplayerRoomUser(BaseModel): - id: int - state: MultiplayerUserState = MultiplayerUserState.IDLE - beatmapAvailability: BeatmapAvailability = BeatmapAvailability( - state=DownloadState.UNKONWN, downloadProgress=None - ) - mods: list[APIMod] = [] - matchState: dict[str, Any] | None - rulesetId: int | None - beatmapId: int | None - - class PlaylistItem(BaseModel): - id: int | None + id: int owner_id: int ruleset_id: int expired: bool @@ -120,9 +81,12 @@ class PlaylistItem(BaseModel): allowed_mods: list[APIMod] = [] required_mods: list[APIMod] = [] beatmap_id: int - beatmap: Beatmap | None + beatmap: BeatmapResp | None freestyle: bool + class Config: + exclude_none = True + class RoomPlaylistItemStats(BaseModel): count_active: int @@ -145,91 +109,168 @@ class PlaylistAggregateScore(BaseModel): playlist_item_attempts: list[ItemAttemptsCount] -class MultiplayerCountdown(BaseModel): - id: int - timeRemaining: timedelta +class MultiplayerRoomSettings(BaseModel): + Name: str = "Unnamed Room" + PlaylistItemId: int + Password: str = "" + MatchType: MatchType + QueueMode: QueueMode + AutoStartDuration: timedelta + AutoSkip: bool -class Room(BaseModel): - id: int | None - name: str = "" - password: str | None - has_password: bool = False - host: APIUser - category: RoomCategory = RoomCategory.NORMAL - duration: int | None - starts_at: datetime | None - ends_at: datetime | None - participant_count: int = 0 - recent_participants: list[APIUser] = [] - max_attempts: int | None - playlist: list[PlaylistItem] = [] - playlist_item_stats: RoomPlaylistItemStats | None - difficulty_range: RoomDifficultyRange | None - type: MatchType = MatchType.PLAYLISTS - queue_mode: QueueMode = QueueMode.HOST_ONLY - auto_skip: bool = False - auto_start_duration: int = 0 - current_user_score: PlaylistAggregateScore | None - current_playlist_item: PlaylistItem | None - channel_id: int = 0 - status: RoomStatus = RoomStatus.IDLE - # availability 字段在当前序列化中未包含,但可能在某些场景下需要 - availability: RoomAvailability | None +class BeatmapAvailability(BaseModel): + State: DownloadState + DownloadProgress: float | None + + +class MatchUserState(BaseModel): + class Config: + extra = "allow" + + +class TeamVersusState(MatchUserState): + TeamId: int + + +MatchUserStateType = TeamVersusState | MatchUserState + + +class MultiplayerRoomUser(BaseModel): + UserID: int + State: MultiplayerUserState = MultiplayerUserState.IDLE + BeatmapAvailability: BeatmapAvailability + Mods: list[APIMod] = [] + MatchUserState: MatchUserStateType | None + RulesetId: int | None + BeatmapId: int | None + User: User | None @classmethod - def from_MultiplayerRoom(cls, room: MultiplayerRoom): - r = cls.model_validate(room.model_dump()) - r.id = room.roomId - r.name = room.settings.name - r.password = room.settings.password - r.has_password = bool(room.settings.password) - if room.host: - r.host.id = room.host.id - r.type = room.settings.matchType - r.queue_mode = room.settings.queueMode - r.auto_start_duration = room.settings.autoStartDuration.seconds - r.auto_skip = room.settings.autoSkip - r.channel_id = room.channelId - if room.state == MultiplayerRoomState.OPEN: - r.status = RoomStatus.IDLE - elif ( - room.state == MultiplayerRoomState.WAITING_FOR_LOAD - or room.state == MultiplayerRoomState.PLAYING - ): - r.status = RoomStatus.PLAYING - elif room.state == MultiplayerRoomState.CLOSED: - r.status = RoomStatus.IDLE - r.ends_at = datetime.utcnow() - playlist_items = [] - for multiplayer_item in room.playlist: - playlist_item = PlaylistItem( - id=multiplayer_item.id, - owner_id=multiplayer_item.ownerId, - ruleset_id=multiplayer_item.rulesetId, - expired=multiplayer_item.expired, - playlist_order=multiplayer_item.playlistOrder, - played_at=multiplayer_item.playedAt, - freestyle=multiplayer_item.freestyle, - beatmap_id=multiplayer_item.beatmapId, - beatmap=None, + async def from_id(cls, id: int, db: AsyncSession): + actualUser = ( + await db.exec( + DBUser.all_select_clause().where( + DBUser.id == id, + ) ) - playlist_items.append(playlist_item) - r.playlist = playlist_items - r.participant_count = len(playlist_items) - return r + ).first() + user = ( + await convert_db_user_to_api_user(actualUser) + if actualUser is not None + else None + ) + return MultiplayerRoomUser( + UserID=id, + MatchUserState=None, + BeatmapAvailability=BeatmapAvailability( + State=DownloadState.UNKOWN, DownloadProgress=None + ), + RulesetId=None, + BeatmapId=None, + User=user, + ) + + +class MatchRoomState(BaseModel): + class Config: + extra = "allow" + + +class MultiPlayerTeam(BaseModel): + id: int = 0 + name: str = "" + + +class TeamVersusRoomState(BaseModel): + teams: list[MultiPlayerTeam] = [] + + class Config: + pass + + @classmethod + def create_default(cls): + return cls( + teams=[ + MultiPlayerTeam(id=0, name="Team Red"), + MultiPlayerTeam(id=1, name="Team Blue"), + ] + ) + + +MatchRoomStateType = TeamVersusRoomState | MatchRoomState + + +class MultiPlayerListItem(BaseModel): + id: int + OwnerID: int + BeatmapID: int + BeatmapChecksum: str = "" + RulesetID: int + RequierdMods: list[APIMod] + AllowedMods: list[APIMod] + Expired: bool + PlaylistOrder: int | None + PlayedAt: datetime | None + StarRating: float + Freestyle: bool + + @classmethod + async def from_apiItem(cls, item: PlaylistItem, db: AsyncSession, fetcher: Fetcher): + s = cls.model_validate(item.model_dump()) + s.id = item.id + s.OwnerID = item.owner_id + if item.beatmap is None: # 从客户端接受的一定没有这字段 + cur_beatmap = await Beatmap.get_or_fetch( + db, fetcher=fetcher, bid=item.beatmap_id + ) + s.BeatmapID = cur_beatmap.id if cur_beatmap.id is not None else 0 + s.BeatmapChecksum = cur_beatmap.checksum + s.StarRating = cur_beatmap.difficulty_rating + s.RulesetID = item.ruleset_id + s.RequierdMods = item.required_mods + s.AllowedMods = item.allowed_mods + s.Expired = item.expired + s.PlaylistOrder = item.playlist_order if item.playlist_order is not None else 0 + s.PlayedAt = item.played_at + s.Freestyle = item.freestyle + + +class MultiplayerCountdown(BaseModel): + id: int = 0 + time_remaining: timedelta = timedelta(seconds=0) + is_exclusive: bool = True + + class Config: + extra = "allow" + + +class MatchStartCountdown(MultiplayerCountdown): + pass + + +class ForceGameplayStartCountdown(MultiplayerCountdown): + pass + + +class ServerShuttingCountdown(MultiplayerCountdown): + pass + + +MultiplayerCountdownType = ( + MatchStartCountdown + | ForceGameplayStartCountdown + | ServerShuttingCountdown + | MultiplayerCountdown +) class MultiplayerRoom(BaseModel): - roomId: int - state: MultiplayerRoomState = MultiplayerRoomState.OPEN - settings: MultiplayerRoomSettings = MultiplayerRoomSettings() - users: list[MultiplayerRoomUser] = [] - host: MultiplayerRoomUser | None - matchState: dict[str, Any] | None - playlist: list[MultiplayerPlaylistItem] = [] - activeCountdowns: list[MultiplayerCountdown] = [] - channelId: int = 0 - - def __init__(self, roomId: int, **data): - super().__init__(roomId=roomId, **data) + RoomId: int + State: MultiplayerRoomState + Users: list[MultiplayerRoomUser] + Host: MultiplayerRoomUser + MatchState: MatchRoomState | None + Playlist: list[MultiPlayerListItem] + ActivecCountDowns: list[MultiplayerCountdownType] + ChannelID: int