feat(multiplayer): 增加房间用户添加功能并优化房间模型

- 新增 APIUser 模型用于表示房间内的用户
- 扩展 MultiplayerRoom 模型以支持更多房间相关功能
- 添加用户加入房间的路由和相关逻辑
- 优化 Room 模型,增加从 MultiplayerRoom 转换的方法
This commit is contained in:
jimmy-sketch
2025-07-27 15:04:30 +00:00
parent d08df157e7
commit d16a2ac1b5
3 changed files with 174 additions and 6 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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")