diff --git a/app/models/room.py b/app/models/room.py index 9ca24d2..793635e 100644 --- a/app/models/room.py +++ b/app/models/room.py @@ -1,11 +1,12 @@ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timedelta from enum import Enum +from typing import Any from app.database.beatmap import Beatmap -from app.database.user import User from app.models.mods import APIMod +from app.models.user import APIUser from pydantic import BaseModel @@ -40,6 +41,75 @@ class RoomStatus(str, Enum): PLAYING = "playing" +class MultiplayerRoomState(str, Enum): + OPEN = "open" + WAITING_FOR_LOAD = "waiting_for_load" + PLAYING = "playing" + CLOSED = "closed" + + +class MultiplayerUserState(str, Enum): + IDLE = "idle" + READY = "ready" + WAITING_FOR_LOAD = "waiting_for_load" + LOADED = "loaded" + READY_FOR_GAMEPLAY = "ready_for_gameplay" + PLAYING = "playing" + FINISHED_PLAY = "finished_play" + RESULTS = "results" + SPECTATING = "spectating" + + +class DownloadState(str, Enum): + UNKONWN = "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 owner_id: int @@ -75,18 +145,23 @@ class PlaylistAggregateScore(BaseModel): playlist_item_attempts: list[ItemAttemptsCount] +class MultiplayerCountdown(BaseModel): + id: int + timeRemaining: timedelta + + class Room(BaseModel): id: int | None name: str = "" password: str | None has_password: bool = False - host: User | None + host: APIUser category: RoomCategory = RoomCategory.NORMAL duration: int | None starts_at: datetime | None ends_at: datetime | None participant_count: int = 0 - recent_participants: list[User] = [] + recent_participants: list[APIUser] = [] max_attempts: int | None playlist: list[PlaylistItem] = [] playlist_item_stats: RoomPlaylistItemStats | None @@ -101,3 +176,60 @@ class Room(BaseModel): status: RoomStatus = RoomStatus.IDLE # availability 字段在当前序列化中未包含,但可能在某些场景下需要 availability: RoomAvailability | 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, + ) + playlist_items.append(playlist_item) + r.playlist = playlist_items + r.participant_count = len(playlist_items) + return r + + +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) diff --git a/app/models/user.py b/app/models/user.py index 42251e9..0f5dab9 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -209,3 +209,7 @@ class User(BaseModel): replays_watched_counts: list[dict] = [] team: Team | None = None user_achievements: list[UserAchievement] = [] + + +class APIUser(BaseModel): + id: int diff --git a/app/router/room.py b/app/router/room.py index ed540fc..ebd8326 100644 --- a/app/router/room.py +++ b/app/router/room.py @@ -2,7 +2,11 @@ from __future__ import annotations from app.database.room import RoomIndex from app.dependencies.database import get_db, get_redis -from app.models.room import Room +from app.models.room import ( + MultiplayerRoom, + MultiplayerRoomUser, + Room, +) from .api_router import router @@ -27,9 +31,37 @@ async def get_all_rooms( for room_index in all_room_ids: dumped_room = redis.get(str(room_index.id)) if dumped_room: - actual_room = Room.model_validate_json(str(dumped_room)) + actual_room = MultiplayerRoom.model_validate_json(str(dumped_room)) + actual_room = Room.from_MultiplayerRoom(actual_room) if actual_room.status == status and actual_room.category == category: roomsList.append(actual_room) return roomsList else: raise HTTPException(status_code=500, detail="Redis Error") + + +@router.put("/rooms/{room}/users/{user}", tags=["rooms"], response_model=Room) +async def add_user_to_room( + room: int, user: int, password: str, db: AsyncSession = Depends(dependency=get_db) +): + redis = get_redis() + if redis: + dumped_room = redis.get(str(room)) + if not dumped_room: + raise HTTPException(status_code=404, detail="房间不存在") + actual_room = MultiplayerRoom.model_validate_json(str(dumped_room)) + + # 验证密码 + if password != actual_room.settings.password: + raise HTTPException(status_code=403, detail="Invalid password") + + # 继续处理加入房间的逻辑 + actual_room.users.append( + MultiplayerRoomUser( + id=user, matchState=None, rulesetId=None, beatmapId=None + ) + ) + actual_room = Room.from_MultiplayerRoom(actual_room) + return actual_room + else: + raise HTTPException(status_code=500, detail="Redis Error")