Files
g0v0-server/app/models/room.py
jimmy-sketch d16a2ac1b5 feat(multiplayer): 增加房间用户添加功能并优化房间模型
- 新增 APIUser 模型用于表示房间内的用户
- 扩展 MultiplayerRoom 模型以支持更多房间相关功能
- 添加用户加入房间的路由和相关逻辑
- 优化 Room 模型,增加从 MultiplayerRoom 转换的方法
2025-07-27 15:04:30 +00:00

236 lines
6.5 KiB
Python

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.models.mods import APIMod
from app.models.user import APIUser
from pydantic import BaseModel
class RoomCategory(str, Enum):
NORMAL = "normal"
SPOTLIGHT = "spotlight"
FEATURED_ARTIST = "featured_artist"
DAILY_CHALLENGE = "daily_challenge"
class MatchType(str, Enum):
PLAYLISTS = "playlists"
HEAD_TO_HEAD = "head_to_head"
TEAM_VERSUS = "team_versus"
class QueueMode(str, Enum):
HOST_ONLY = "host_only"
ALL_PLAYERS = "all_players"
ALL_PLAYERS_ROUND_ROBIN = "all_players_round_robin"
class RoomAvailability(str, Enum):
PUBLIC = "public"
FRIENDS_ONLY = "friends_only"
INVITE_ONLY = "invite_only"
class RoomStatus(str, Enum):
IDLE = "idle"
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
ruleset_id: int
expired: bool
playlist_order: int | None
played_at: datetime | None
allowed_mods: list[APIMod] = []
required_mods: list[APIMod] = []
beatmap_id: int
beatmap: Beatmap | None
freestyle: bool
class RoomPlaylistItemStats(BaseModel):
count_active: int
count_total: int
ruleset_ids: list[int] = []
class RoomDifficultyRange(BaseModel):
min: float
max: float
class ItemAttemptsCount(BaseModel):
id: int
attempts: int
passed: bool
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: 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
@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)