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 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, BeatmapResp
from app.database.user import User as DBUser
from app.fetcher import Fetcher
from app.models.mods import APIMod 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 pydantic import BaseModel
from sqlmodel.ext.asyncio.session import AsyncSession
class RoomCategory(str, Enum): class RoomCategory(str, Enum):
@@ -61,57 +64,15 @@ class MultiplayerUserState(str, Enum):
class DownloadState(str, Enum): class DownloadState(str, Enum):
UNKONWN = "unkown" UNKOWN = "unkown"
NOT_DOWNLOADED = "not_downloaded" NOT_DOWNLOADED = "not_downloaded"
DOWNLOADING = "downloading" DOWNLOADING = "downloading"
IMPORTING = "importing" IMPORTING = "importing"
LOCALLY_AVAILABLE = "locally_available" 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
owner_id: int owner_id: int
ruleset_id: int ruleset_id: int
expired: bool expired: bool
@@ -120,9 +81,12 @@ class PlaylistItem(BaseModel):
allowed_mods: list[APIMod] = [] allowed_mods: list[APIMod] = []
required_mods: list[APIMod] = [] required_mods: list[APIMod] = []
beatmap_id: int beatmap_id: int
beatmap: Beatmap | None beatmap: BeatmapResp | None
freestyle: bool freestyle: bool
class Config:
exclude_none = True
class RoomPlaylistItemStats(BaseModel): class RoomPlaylistItemStats(BaseModel):
count_active: int count_active: int
@@ -145,91 +109,168 @@ class PlaylistAggregateScore(BaseModel):
playlist_item_attempts: list[ItemAttemptsCount] playlist_item_attempts: list[ItemAttemptsCount]
class MultiplayerCountdown(BaseModel): class MultiplayerRoomSettings(BaseModel):
id: int Name: str = "Unnamed Room"
timeRemaining: timedelta PlaylistItemId: int
Password: str = ""
MatchType: MatchType
QueueMode: QueueMode
AutoStartDuration: timedelta
AutoSkip: bool
class Room(BaseModel): class BeatmapAvailability(BaseModel):
id: int | None State: DownloadState
name: str = "" DownloadProgress: float | None
password: str | None
has_password: bool = False
host: APIUser class MatchUserState(BaseModel):
category: RoomCategory = RoomCategory.NORMAL class Config:
duration: int | None extra = "allow"
starts_at: datetime | None
ends_at: datetime | None
participant_count: int = 0 class TeamVersusState(MatchUserState):
recent_participants: list[APIUser] = [] TeamId: int
max_attempts: int | None
playlist: list[PlaylistItem] = []
playlist_item_stats: RoomPlaylistItemStats | None MatchUserStateType = TeamVersusState | MatchUserState
difficulty_range: RoomDifficultyRange | None
type: MatchType = MatchType.PLAYLISTS
queue_mode: QueueMode = QueueMode.HOST_ONLY class MultiplayerRoomUser(BaseModel):
auto_skip: bool = False UserID: int
auto_start_duration: int = 0 State: MultiplayerUserState = MultiplayerUserState.IDLE
current_user_score: PlaylistAggregateScore | None BeatmapAvailability: BeatmapAvailability
current_playlist_item: PlaylistItem | None Mods: list[APIMod] = []
channel_id: int = 0 MatchUserState: MatchUserStateType | None
status: RoomStatus = RoomStatus.IDLE RulesetId: int | None
# availability 字段在当前序列化中未包含,但可能在某些场景下需要 BeatmapId: int | None
availability: RoomAvailability | None User: User | None
@classmethod @classmethod
def from_MultiplayerRoom(cls, room: MultiplayerRoom): async def from_id(cls, id: int, db: AsyncSession):
r = cls.model_validate(room.model_dump()) actualUser = (
r.id = room.roomId await db.exec(
r.name = room.settings.name DBUser.all_select_clause().where(
r.password = room.settings.password DBUser.id == id,
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) ).first()
r.playlist = playlist_items user = (
r.participant_count = len(playlist_items) await convert_db_user_to_api_user(actualUser)
return r 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): class MultiplayerRoom(BaseModel):
roomId: int RoomId: int
state: MultiplayerRoomState = MultiplayerRoomState.OPEN State: MultiplayerRoomState
settings: MultiplayerRoomSettings = MultiplayerRoomSettings() Users: list[MultiplayerRoomUser]
users: list[MultiplayerRoomUser] = [] Host: MultiplayerRoomUser
host: MultiplayerRoomUser | None MatchState: MatchRoomState | None
matchState: dict[str, Any] | None Playlist: list[MultiPlayerListItem]
playlist: list[MultiplayerPlaylistItem] = [] ActivecCountDowns: list[MultiplayerCountdownType]
activeCountdowns: list[MultiplayerCountdown] = [] ChannelID: int
channelId: int = 0
def __init__(self, roomId: int, **data):
super().__init__(roomId=roomId, **data)