refactor(multiplayer): 完善服务端房间模型
This commit is contained in:
@@ -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)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user