feat(multiplayer): support countdown

This commit is contained in:
MingxuanGame
2025-08-05 17:21:45 +00:00
parent 0988f1fc0c
commit 0a80c5051c
6 changed files with 131 additions and 56 deletions

View File

@@ -1,11 +1,11 @@
from __future__ import annotations
from enum import IntEnum
from typing import Annotated, ClassVar, Literal
from typing import ClassVar, Literal
from app.models.signalr import SignalRMeta, SignalRUnionMessage, UserState
from app.models.signalr import SignalRUnionMessage, UserState
from pydantic import BaseModel, Field
from pydantic import BaseModel
class _UserActivity(SignalRUnionMessage): ...
@@ -100,12 +100,9 @@ UserActivity = (
class UserPresence(BaseModel):
activity: Annotated[
UserActivity | None, Field(default=None), SignalRMeta(use_upper_case=True)
]
status: Annotated[
OnlineStatus | None, Field(default=None), SignalRMeta(use_upper_case=True)
]
activity: UserActivity | None = None
status: OnlineStatus | None = None
@property
def pushable(self) -> bool:

View File

@@ -53,10 +53,14 @@ class MultiplayerRoomSettings(BaseModel):
auto_start_duration: timedelta = timedelta(seconds=0)
auto_skip: bool = False
@property
def auto_start_enabled(self) -> bool:
return self.auto_start_duration != timedelta(seconds=0)
class BeatmapAvailability(BaseModel):
state: DownloadState = DownloadState.UNKNOWN
progress: float | None = None
download_progress: float | None = None
class _MatchUserState(SignalRUnionMessage): ...
@@ -283,10 +287,12 @@ class PlaylistItem(BaseModel):
return copy
class _MultiplayerCountdown(BaseModel):
class _MultiplayerCountdown(SignalRUnionMessage):
id: int = 0
remaining: timedelta
is_exclusive: bool = False
time_remaining: timedelta
is_exclusive: Annotated[
bool, Field(default=True), SignalRMeta(member_ignore=True)
] = True
class MatchStartCountdown(_MultiplayerCountdown):
@@ -310,7 +316,7 @@ class MultiplayerRoomUser(BaseModel):
user_id: int
state: MultiplayerUserState = MultiplayerUserState.IDLE
availability: BeatmapAvailability = BeatmapAvailability(
state=DownloadState.UNKNOWN, progress=None
state=DownloadState.UNKNOWN, download_progress=None
)
mods: list[APIMod] = Field(default_factory=list)
match_state: MatchUserState | None = None
@@ -602,8 +608,8 @@ class CountdownInfo:
def __init__(self, countdown: MultiplayerCountdown):
self.countdown = countdown
self.duration = (
countdown.remaining
if countdown.remaining > timedelta(seconds=0)
countdown.time_remaining
if countdown.time_remaining > timedelta(seconds=0)
else timedelta(seconds=0)
)
@@ -776,13 +782,12 @@ class ServerMultiplayerRoom:
):
async def _countdown_task(self: "ServerMultiplayerRoom"):
await asyncio.sleep(info.duration.total_seconds())
await self.stop_countdown(countdown)
if on_complete is not None:
await on_complete(self)
await self.stop_countdown(countdown)
if countdown.is_exclusive:
await self.stop_all_countdowns()
countdown.id = await self.get_next_countdown_id()
info = CountdownInfo(countdown)
self.room.active_countdowns.append(info.countdown)
@@ -793,21 +798,14 @@ class ServerMultiplayerRoom:
info.task = asyncio.create_task(_countdown_task(self))
async def stop_countdown(self, countdown: MultiplayerCountdown):
info = next(
(
info
for info in self._tracked_countdown.values()
if info.countdown.id == countdown.id
),
None,
)
info = self._tracked_countdown.get(countdown.id)
if info is None:
return
if info.task is not None and not info.task.done():
info.task.cancel()
del self._tracked_countdown[countdown.id]
self.room.active_countdowns.remove(countdown)
await self.hub.send_match_event(self, CountdownStoppedEvent(id=countdown.id))
if info.task is not None and not info.task.done():
info.task.cancel()
async def stop_all_countdowns(self):
for countdown in list(self._tracked_countdown.values()):
@@ -817,19 +815,19 @@ class ServerMultiplayerRoom:
self.room.active_countdowns.clear()
class _MatchServerEvent(BaseModel): ...
class _MatchServerEvent(SignalRUnionMessage): ...
class CountdownStartedEvent(_MatchServerEvent):
countdown: MultiplayerCountdown
type: Literal[0] = Field(default=0, exclude=True)
union_type: ClassVar[Literal[0]] = 0
class CountdownStoppedEvent(_MatchServerEvent):
id: int
type: Literal[1] = Field(default=1, exclude=True)
union_type: ClassVar[Literal[1]] = 1
MatchServerEvent = CountdownStartedEvent | CountdownStoppedEvent

View File

@@ -13,7 +13,6 @@ from pydantic import (
class SignalRMeta:
member_ignore: bool = False # implement of IgnoreMember (msgpack) attribute
json_ignore: bool = False # implement of JsonIgnore (json) attribute
use_upper_case: bool = False # use upper CamelCase for field names
use_abbr: bool = True