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

@@ -19,12 +19,14 @@ from app.models.multiplayer_hub import (
GameplayAbortReason,
MatchRequest,
MatchServerEvent,
MatchStartCountdown,
MultiplayerClientState,
MultiplayerRoom,
MultiplayerRoomSettings,
MultiplayerRoomUser,
PlaylistItem,
ServerMultiplayerRoom,
ServerShuttingDownCountdown,
StartMatchCountdownRequest,
StopCountdownRequest,
)
@@ -160,7 +162,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
availability = user.availability
if (
availability.state == beatmap_availability.state
and availability.progress == beatmap_availability.progress
and availability.download_progress == beatmap_availability.download_progress
):
return
user.availability = beatmap_availability
@@ -512,6 +514,25 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
async def update_room_state(self, room: ServerMultiplayerRoom):
match room.room.state:
case MultiplayerRoomState.OPEN:
if room.room.settings.auto_start_enabled:
if (
not room.queue.current_item.expired
and any(
u.state == MultiplayerUserState.READY
for u in room.room.users
)
and not any(
isinstance(countdown, MatchStartCountdown)
for countdown in room.room.active_countdowns
)
):
await room.start_countdown(
MatchStartCountdown(
time_remaining=room.room.settings.auto_start_duration
),
self.start_match,
)
case MultiplayerRoomState.WAITING_FOR_LOAD:
played_count = len(
[True for user in room.room.users if user.state.is_playing]
@@ -610,7 +631,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
)
await room.start_countdown(
ForceGameplayStartCountdown(
remaining=timedelta(seconds=GAMEPLAY_LOAD_TIMEOUT)
time_remaining=timedelta(seconds=GAMEPLAY_LOAD_TIMEOUT)
),
self.start_gameplay,
)
@@ -885,15 +906,34 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
raise InvokeException("You are not in this room")
if isinstance(request, StartMatchCountdownRequest):
# TODO: countdown
...
if room.host and room.host.user_id != user.user_id:
raise InvokeException("You are not the host of this room")
if room.state != MultiplayerRoomState.OPEN:
raise InvokeException("Cannot start a countdown during ongoing play")
await server_room.start_countdown(
MatchStartCountdown(time_remaining=request.duration),
self.start_match,
)
elif isinstance(request, StopCountdownRequest):
...
countdown = next(
(c for c in room.active_countdowns if c.id == request.id),
None,
)
if countdown is None:
return
if (
isinstance(countdown, MatchStartCountdown)
and room.settings.auto_start_enabled
) or isinstance(
countdown, (ForceGameplayStartCountdown | ServerShuttingDownCountdown)
):
raise InvokeException("Cannot stop the requested countdown")
await server_room.stop_countdown(countdown)
else:
await server_room.match_type_handler.handle_request(user, request)
async def InvitePlayer(self, client: Client, user_id: int):
print(f"Inviting player... {client.user_id} {user_id}")
store = self.get_or_create_state(client)
if store.room_id == 0:
raise InvokeException("You are not in a room")

View File

@@ -15,7 +15,7 @@ from typing import (
)
from app.models.signalr import SignalRMeta, SignalRUnionMessage
from app.utils import camel_to_snake, snake_to_camel
from app.utils import camel_to_snake, snake_to_camel, snake_to_pascal
import msgpack_lazer_api as m
from pydantic import BaseModel
@@ -98,7 +98,7 @@ class MsgpackProtocol:
elif issubclass(typ, datetime.datetime):
return [v, 0]
elif issubclass(typ, datetime.timedelta):
return int(v.total_seconds())
return int(v.total_seconds() * 10_000_000)
elif isinstance(v, dict):
return {
cls.serialize_msgpack(k): cls.serialize_msgpack(value)
@@ -216,8 +216,8 @@ class MsgpackProtocol:
elif inspect.isclass(typ) and issubclass(typ, datetime.datetime):
return v[0]
elif inspect.isclass(typ) and issubclass(typ, datetime.timedelta):
return datetime.timedelta(seconds=int(v))
elif isinstance(v, list):
return datetime.timedelta(seconds=int(v / 10_000_000))
elif get_origin(typ) is list:
return [cls.validate_object(item, get_args(typ)[0]) for item in v]
elif inspect.isclass(typ) and issubclass(typ, Enum):
list_ = list(typ)
@@ -300,10 +300,10 @@ class MsgpackProtocol:
class JSONProtocol:
@classmethod
def serialize_to_json(cls, v: Any, dict_key: bool = False):
def serialize_to_json(cls, v: Any, dict_key: bool = False, in_union: bool = False):
typ = v.__class__
if issubclass(typ, BaseModel):
return cls.serialize_model(v)
return cls.serialize_model(v, in_union)
elif isinstance(v, dict):
return {
cls.serialize_to_json(k, True): cls.serialize_to_json(value)
@@ -327,22 +327,28 @@ class JSONProtocol:
return v
@classmethod
def serialize_model(cls, v: BaseModel) -> dict[str, Any]:
def serialize_model(cls, v: BaseModel, in_union: bool = False) -> dict[str, Any]:
d = {}
is_union = issubclass(v.__class__, SignalRUnionMessage)
for field, info in v.__class__.model_fields.items():
metadata = next(
(m for m in info.metadata if isinstance(m, SignalRMeta)), None
)
if metadata and metadata.json_ignore:
continue
d[
name = (
snake_to_camel(
field,
metadata.use_upper_case if metadata else False,
metadata.use_abbr if metadata else True,
)
] = cls.serialize_to_json(getattr(v, field))
if issubclass(v.__class__, SignalRUnionMessage):
if not is_union
else snake_to_pascal(
field,
metadata.use_abbr if metadata else True,
)
)
d[name] = cls.serialize_to_json(getattr(v, field), in_union=is_union)
if is_union and not in_union:
return {
"$dtype": v.__class__.__name__,
"$value": d,
@@ -360,11 +366,12 @@ class JSONProtocol:
)
if metadata and metadata.json_ignore:
continue
value = v.get(
snake_to_camel(
field, not from_union, metadata.use_abbr if metadata else True
)
name = (
snake_to_camel(field, metadata.use_abbr if metadata else True)
if not from_union
else snake_to_pascal(field, metadata.use_abbr if metadata else True)
)
value = v.get(name)
anno = typ.model_fields[field].annotation
if anno is None:
d[field] = value
@@ -433,7 +440,7 @@ class JSONProtocol:
return datetime.timedelta(minutes=int(parts[0]), seconds=int(parts[1]))
elif len(parts) == 1:
return datetime.timedelta(seconds=int(parts[0]))
elif isinstance(v, list):
elif get_origin(typ) is list:
return [cls.validate_object(item, get_args(typ)[0]) for item in v]
elif inspect.isclass(typ) and issubclass(typ, Enum):
list_ = list(typ)