refactor(multiplayer): 完善服务端房间模型

This commit is contained in:
jimmy-sketch
2025-07-29 06:17:56 +00:00
parent 094a441d73
commit 605ad934cc

View File

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