feat(multiplayer): 增加房间用户添加功能并优化房间模型
- 新增 APIUser 模型用于表示房间内的用户 - 扩展 MultiplayerRoom 模型以支持更多房间相关功能 - 添加用户加入房间的路由和相关逻辑 - 优化 Room 模型,增加从 MultiplayerRoom 转换的方法
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from app.database.beatmap import Beatmap
|
from app.database.beatmap import Beatmap
|
||||||
from app.database.user import User
|
|
||||||
from app.models.mods import APIMod
|
from app.models.mods import APIMod
|
||||||
|
from app.models.user import APIUser
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@@ -40,6 +41,75 @@ class RoomStatus(str, Enum):
|
|||||||
PLAYING = "playing"
|
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):
|
class PlaylistItem(BaseModel):
|
||||||
id: int | None
|
id: int | None
|
||||||
owner_id: int
|
owner_id: int
|
||||||
@@ -75,18 +145,23 @@ class PlaylistAggregateScore(BaseModel):
|
|||||||
playlist_item_attempts: list[ItemAttemptsCount]
|
playlist_item_attempts: list[ItemAttemptsCount]
|
||||||
|
|
||||||
|
|
||||||
|
class MultiplayerCountdown(BaseModel):
|
||||||
|
id: int
|
||||||
|
timeRemaining: timedelta
|
||||||
|
|
||||||
|
|
||||||
class Room(BaseModel):
|
class Room(BaseModel):
|
||||||
id: int | None
|
id: int | None
|
||||||
name: str = ""
|
name: str = ""
|
||||||
password: str | None
|
password: str | None
|
||||||
has_password: bool = False
|
has_password: bool = False
|
||||||
host: User | None
|
host: APIUser
|
||||||
category: RoomCategory = RoomCategory.NORMAL
|
category: RoomCategory = RoomCategory.NORMAL
|
||||||
duration: int | None
|
duration: int | None
|
||||||
starts_at: datetime | None
|
starts_at: datetime | None
|
||||||
ends_at: datetime | None
|
ends_at: datetime | None
|
||||||
participant_count: int = 0
|
participant_count: int = 0
|
||||||
recent_participants: list[User] = []
|
recent_participants: list[APIUser] = []
|
||||||
max_attempts: int | None
|
max_attempts: int | None
|
||||||
playlist: list[PlaylistItem] = []
|
playlist: list[PlaylistItem] = []
|
||||||
playlist_item_stats: RoomPlaylistItemStats | None
|
playlist_item_stats: RoomPlaylistItemStats | None
|
||||||
@@ -101,3 +176,60 @@ class Room(BaseModel):
|
|||||||
status: RoomStatus = RoomStatus.IDLE
|
status: RoomStatus = RoomStatus.IDLE
|
||||||
# availability 字段在当前序列化中未包含,但可能在某些场景下需要
|
# availability 字段在当前序列化中未包含,但可能在某些场景下需要
|
||||||
availability: RoomAvailability | None
|
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)
|
||||||
|
|||||||
@@ -209,3 +209,7 @@ class User(BaseModel):
|
|||||||
replays_watched_counts: list[dict] = []
|
replays_watched_counts: list[dict] = []
|
||||||
team: Team | None = None
|
team: Team | None = None
|
||||||
user_achievements: list[UserAchievement] = []
|
user_achievements: list[UserAchievement] = []
|
||||||
|
|
||||||
|
|
||||||
|
class APIUser(BaseModel):
|
||||||
|
id: int
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from app.database.room import RoomIndex
|
from app.database.room import RoomIndex
|
||||||
from app.dependencies.database import get_db, get_redis
|
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
|
from .api_router import router
|
||||||
|
|
||||||
@@ -27,9 +31,37 @@ async def get_all_rooms(
|
|||||||
for room_index in all_room_ids:
|
for room_index in all_room_ids:
|
||||||
dumped_room = redis.get(str(room_index.id))
|
dumped_room = redis.get(str(room_index.id))
|
||||||
if dumped_room:
|
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:
|
if actual_room.status == status and actual_room.category == category:
|
||||||
roomsList.append(actual_room)
|
roomsList.append(actual_room)
|
||||||
return roomsList
|
return roomsList
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=500, detail="Redis Error")
|
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")
|
||||||
|
|||||||
Reference in New Issue
Block a user