refactor(project): make pyright & ruff happy
This commit is contained in:
@@ -145,14 +145,9 @@ class Hub[TState: UserState]:
|
||||
connection: WebSocket,
|
||||
) -> Client:
|
||||
if connection_token in self.clients:
|
||||
raise ValueError(
|
||||
f"Client with connection token {connection_token} already exists."
|
||||
)
|
||||
raise ValueError(f"Client with connection token {connection_token} already exists.")
|
||||
if connection_token in self.waited_clients:
|
||||
if (
|
||||
self.waited_clients[connection_token]
|
||||
< time.time() - settings.signalr_negotiate_timeout
|
||||
):
|
||||
if self.waited_clients[connection_token] < time.time() - settings.signalr_negotiate_timeout:
|
||||
raise TimeoutError(f"Connection {connection_id} has waited too long.")
|
||||
del self.waited_clients[connection_token]
|
||||
client = Client(connection_id, connection_token, connection, protocol)
|
||||
@@ -196,9 +191,7 @@ class Hub[TState: UserState]:
|
||||
try:
|
||||
await client.send_packet(packet)
|
||||
except WebSocketDisconnect as e:
|
||||
logger.info(
|
||||
f"Client {client.connection_id} disconnected: {e.code}, {e.reason}"
|
||||
)
|
||||
logger.info(f"Client {client.connection_id} disconnected: {e.code}, {e.reason}")
|
||||
await self.remove_client(client)
|
||||
except RuntimeError as e:
|
||||
if "disconnect message" in str(e):
|
||||
@@ -216,9 +209,7 @@ class Hub[TState: UserState]:
|
||||
tasks.append(self.call_noblock(client, method, *args))
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
async def broadcast_group_call(
|
||||
self, group_id: str, method: str, *args: Any
|
||||
) -> None:
|
||||
async def broadcast_group_call(self, group_id: str, method: str, *args: Any) -> None:
|
||||
tasks = []
|
||||
for client in self.groups.get(group_id, []):
|
||||
tasks.append(self.call_noblock(client, method, *args))
|
||||
@@ -241,9 +232,7 @@ class Hub[TState: UserState]:
|
||||
self.tasks.add(task)
|
||||
task.add_done_callback(self.tasks.discard)
|
||||
except WebSocketDisconnect as e:
|
||||
logger.info(
|
||||
f"Client {client.connection_id} disconnected: {e.code}, {e.reason}"
|
||||
)
|
||||
logger.info(f"Client {client.connection_id} disconnected: {e.code}, {e.reason}")
|
||||
except RuntimeError as e:
|
||||
if "disconnect message" in str(e):
|
||||
logger.info(f"Client {client.connection_id} closed the connection.")
|
||||
@@ -251,12 +240,8 @@ class Hub[TState: UserState]:
|
||||
logger.exception(f"RuntimeError in client {client.connection_id}: {e}")
|
||||
except CloseConnection as e:
|
||||
if not e.from_client:
|
||||
await client.send_packet(
|
||||
ClosePacket(error=e.message, allow_reconnect=e.allow_reconnect)
|
||||
)
|
||||
logger.info(
|
||||
f"Client {client.connection_id} closed the connection: {e.message}"
|
||||
)
|
||||
await client.send_packet(ClosePacket(error=e.message, allow_reconnect=e.allow_reconnect))
|
||||
logger.info(f"Client {client.connection_id} closed the connection: {e.message}")
|
||||
except Exception:
|
||||
logger.exception(f"Error in client {client.connection_id}")
|
||||
|
||||
@@ -273,15 +258,9 @@ class Hub[TState: UserState]:
|
||||
result = await self.invoke_method(client, packet.target, args)
|
||||
except InvokeException as e:
|
||||
error = e.message
|
||||
logger.debug(
|
||||
f"Client {client.connection_token} call {packet.target}"
|
||||
f" failed: {error}"
|
||||
)
|
||||
logger.debug(f"Client {client.connection_token} call {packet.target} failed: {error}")
|
||||
except Exception:
|
||||
logger.exception(
|
||||
f"Error invoking method {packet.target} for "
|
||||
f"client {client.connection_id}"
|
||||
)
|
||||
logger.exception(f"Error invoking method {packet.target} for client {client.connection_id}")
|
||||
error = "Unknown error occured in server"
|
||||
if packet.invocation_id is not None:
|
||||
await client.send_packet(
|
||||
@@ -303,9 +282,7 @@ class Hub[TState: UserState]:
|
||||
for name, param in signature.parameters.items():
|
||||
if name == "self" or param.annotation is Client:
|
||||
continue
|
||||
call_params.append(
|
||||
client.protocol.validate_object(args.pop(0), param.annotation)
|
||||
)
|
||||
call_params.append(client.protocol.validate_object(args.pop(0), param.annotation))
|
||||
return await method_(client, *call_params)
|
||||
|
||||
async def call(self, client: Client, method: str, *args: Any) -> Any:
|
||||
|
||||
@@ -13,7 +13,7 @@ from app.database.playlist_best_score import PlaylistBestScore
|
||||
from app.database.playlists import Playlist
|
||||
from app.database.room import Room
|
||||
from app.database.score import Score
|
||||
from app.dependencies.database import get_redis, with_db
|
||||
from app.dependencies.database import with_db
|
||||
from app.log import logger
|
||||
from app.models.metadata_hub import (
|
||||
TOTAL_SCORE_DISTRIBUTION_BINS,
|
||||
@@ -44,13 +44,8 @@ class MetadataHub(Hub[MetadataClientState]):
|
||||
self._today = datetime.now(UTC).date()
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
def get_daily_challenge_stats(
|
||||
self, daily_challenge_room: int
|
||||
) -> MultiplayerRoomStats:
|
||||
if (
|
||||
self._daily_challenge_stats is None
|
||||
or self._today != datetime.now(UTC).date()
|
||||
):
|
||||
def get_daily_challenge_stats(self, daily_challenge_room: int) -> MultiplayerRoomStats:
|
||||
if self._daily_challenge_stats is None or self._today != datetime.now(UTC).date():
|
||||
self._daily_challenge_stats = MultiplayerRoomStats(
|
||||
room_id=daily_challenge_room,
|
||||
playlist_item_stats={},
|
||||
@@ -65,9 +60,7 @@ class MetadataHub(Hub[MetadataClientState]):
|
||||
def room_watcher_group(room_id: int) -> str:
|
||||
return f"metadata:multiplayer-room-watchers:{room_id}"
|
||||
|
||||
def broadcast_tasks(
|
||||
self, user_id: int, store: MetadataClientState | None
|
||||
) -> set[Coroutine]:
|
||||
def broadcast_tasks(self, user_id: int, store: MetadataClientState | None) -> set[Coroutine]:
|
||||
if store is not None and not store.pushable:
|
||||
return set()
|
||||
data = store.for_push if store else None
|
||||
@@ -96,18 +89,15 @@ class MetadataHub(Hub[MetadataClientState]):
|
||||
|
||||
# Use centralized offline status management
|
||||
from app.service.online_status_manager import online_status_manager
|
||||
|
||||
await online_status_manager.set_user_offline(user_id)
|
||||
|
||||
if state.pushable:
|
||||
await asyncio.gather(*self.broadcast_tasks(user_id, None))
|
||||
|
||||
|
||||
async with with_db() as session:
|
||||
async with session.begin():
|
||||
user = (
|
||||
await session.exec(
|
||||
select(User).where(User.id == int(state.connection_id))
|
||||
)
|
||||
).one()
|
||||
user = (await session.exec(select(User).where(User.id == int(state.connection_id)))).one()
|
||||
user.last_visit = datetime.now(UTC)
|
||||
await session.commit()
|
||||
|
||||
@@ -124,6 +114,7 @@ class MetadataHub(Hub[MetadataClientState]):
|
||||
|
||||
# Use centralized online status management
|
||||
from app.service.online_status_manager import online_status_manager
|
||||
|
||||
await online_status_manager.set_user_online(user_id, "metadata")
|
||||
|
||||
# CRITICAL FIX: Set online status IMMEDIATELY upon connection
|
||||
@@ -143,20 +134,14 @@ class MetadataHub(Hub[MetadataClientState]):
|
||||
).all()
|
||||
tasks = []
|
||||
for friend_id in friends:
|
||||
self.groups.setdefault(
|
||||
self.friend_presence_watchers_group(friend_id), set()
|
||||
).add(client)
|
||||
if (
|
||||
friend_state := self.state.get(friend_id)
|
||||
) and friend_state.pushable:
|
||||
self.groups.setdefault(self.friend_presence_watchers_group(friend_id), set()).add(client)
|
||||
if (friend_state := self.state.get(friend_id)) and friend_state.pushable:
|
||||
tasks.append(
|
||||
self.broadcast_group_call(
|
||||
self.friend_presence_watchers_group(friend_id),
|
||||
"FriendPresenceUpdated",
|
||||
friend_id,
|
||||
friend_state.for_push
|
||||
if friend_state.pushable
|
||||
else None,
|
||||
friend_state.for_push if friend_state.pushable else None,
|
||||
)
|
||||
)
|
||||
await asyncio.gather(*tasks)
|
||||
@@ -177,7 +162,7 @@ class MetadataHub(Hub[MetadataClientState]):
|
||||
room_id=daily_challenge_room.id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# CRITICAL FIX: Immediately broadcast the user's online status to all watchers
|
||||
# This ensures the user appears as "currently online" right after connection
|
||||
# Similar to the C# implementation's immediate broadcast logic
|
||||
@@ -185,7 +170,7 @@ class MetadataHub(Hub[MetadataClientState]):
|
||||
if online_presence_tasks:
|
||||
await asyncio.gather(*online_presence_tasks)
|
||||
logger.info(f"[MetadataHub] Broadcasted online status for user {user_id} to watchers")
|
||||
|
||||
|
||||
# Also send the user's own presence update to confirm online status
|
||||
await self.call_noblock(
|
||||
client,
|
||||
@@ -213,9 +198,7 @@ class MetadataHub(Hub[MetadataClientState]):
|
||||
)
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
async def UpdateActivity(
|
||||
self, client: Client, activity: UserActivity | None
|
||||
) -> None:
|
||||
async def UpdateActivity(self, client: Client, activity: UserActivity | None) -> None:
|
||||
user_id = int(client.connection_id)
|
||||
store = self.get_or_create_state(client)
|
||||
store.activity = activity
|
||||
@@ -246,15 +229,16 @@ class MetadataHub(Hub[MetadataClientState]):
|
||||
]
|
||||
)
|
||||
self.add_to_group(client, self.online_presence_watchers_group())
|
||||
logger.info(f"[MetadataHub] Client {client.connection_id} now watching user presence, sent {len([s for s in self.state.values() if s.pushable])} online users")
|
||||
logger.info(
|
||||
f"[MetadataHub] Client {client.connection_id} now watching user presence, "
|
||||
f"sent {len([s for s in self.state.values() if s.pushable])} online users"
|
||||
)
|
||||
|
||||
async def EndWatchingUserPresence(self, client: Client) -> None:
|
||||
self.remove_from_group(client, self.online_presence_watchers_group())
|
||||
|
||||
async def notify_room_score_processed(self, event: MultiplayerRoomScoreSetEvent):
|
||||
await self.broadcast_group_call(
|
||||
self.room_watcher_group(event.room_id), "MultiplayerRoomScoreSet", event
|
||||
)
|
||||
await self.broadcast_group_call(self.room_watcher_group(event.room_id), "MultiplayerRoomScoreSet", event)
|
||||
|
||||
async def BeginWatchingMultiplayerRoom(self, client: Client, room_id: int):
|
||||
self.add_to_group(client, self.room_watcher_group(room_id))
|
||||
@@ -289,9 +273,7 @@ class MetadataHub(Hub[MetadataClientState]):
|
||||
PlaylistBestScore.room_id == stats.room_id,
|
||||
PlaylistBestScore.playlist_id == playlist_id,
|
||||
PlaylistBestScore.score_id > last_processed_score_id,
|
||||
col(PlaylistBestScore.score).has(
|
||||
col(Score.passed).is_(True)
|
||||
),
|
||||
col(PlaylistBestScore.score).has(col(Score.passed).is_(True)),
|
||||
)
|
||||
)
|
||||
).all()
|
||||
@@ -311,17 +293,13 @@ class MetadataHub(Hub[MetadataClientState]):
|
||||
)
|
||||
totals[bin_index] += 1
|
||||
|
||||
item.cumulative_score += sum(
|
||||
score.total_score for score in scores
|
||||
)
|
||||
item.cumulative_score += sum(score.total_score for score in scores)
|
||||
|
||||
for j in range(TOTAL_SCORE_DISTRIBUTION_BINS):
|
||||
item.total_score_distribution[j] += totals.get(j, 0)
|
||||
|
||||
if scores:
|
||||
item.last_processed_score_id = max(
|
||||
score.score_id for score in scores
|
||||
)
|
||||
item.last_processed_score_id = max(score.score_id for score in scores)
|
||||
|
||||
async def EndWatchingMultiplayerRoom(self, client: Client, room_id: int):
|
||||
self.remove_from_group(client, self.room_watcher_group(room_id))
|
||||
|
||||
@@ -114,9 +114,7 @@ class MultiplayerEventLogger:
|
||||
)
|
||||
await self.log_event(event)
|
||||
|
||||
async def game_started(
|
||||
self, room_id: int, playlist_item_id: int, details: MatchStartedEventDetail
|
||||
):
|
||||
async def game_started(self, room_id: int, playlist_item_id: int, details: MatchStartedEventDetail):
|
||||
event = MultiplayerEvent(
|
||||
room_id=room_id,
|
||||
playlist_item_id=playlist_item_id,
|
||||
@@ -166,6 +164,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
|
||||
# Use centralized offline status management
|
||||
from app.service.online_status_manager import online_status_manager
|
||||
|
||||
await online_status_manager.set_user_offline(user_id)
|
||||
|
||||
if state.room_id != 0 and state.room_id in self.rooms:
|
||||
@@ -173,9 +172,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
room = server_room.room
|
||||
user = next((u for u in room.users if u.user_id == user_id), None)
|
||||
if user is not None:
|
||||
await self.make_user_leave(
|
||||
self.get_client_by_id(str(user_id)), server_room, user
|
||||
)
|
||||
await self.make_user_leave(self.get_client_by_id(str(user_id)), server_room, user)
|
||||
|
||||
async def on_client_connect(self, client: Client) -> None:
|
||||
"""Track online users when connecting to multiplayer hub"""
|
||||
@@ -183,6 +180,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
|
||||
# Use centralized online status management
|
||||
from app.service.online_status_manager import online_status_manager
|
||||
|
||||
await online_status_manager.set_user_online(client.user_id, "multiplayer")
|
||||
|
||||
def _ensure_in_room(self, client: Client) -> ServerMultiplayerRoom:
|
||||
@@ -212,9 +210,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
type=room.settings.match_type,
|
||||
queue_mode=room.settings.queue_mode,
|
||||
auto_skip=room.settings.auto_skip,
|
||||
auto_start_duration=int(
|
||||
room.settings.auto_start_duration.total_seconds()
|
||||
),
|
||||
auto_start_duration=int(room.settings.auto_start_duration.total_seconds()),
|
||||
host_id=client.user_id,
|
||||
status=RoomStatus.IDLE,
|
||||
)
|
||||
@@ -231,26 +227,20 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
await session.commit()
|
||||
await session.refresh(channel)
|
||||
await session.refresh(db_room)
|
||||
room.channel_id = channel.channel_id # pyright: ignore[reportAttributeAccessIssue]
|
||||
room.channel_id = channel.channel_id
|
||||
db_room.channel_id = channel.channel_id
|
||||
|
||||
item = room.playlist[0]
|
||||
item.owner_id = client.user_id
|
||||
room.room_id = db_room.id
|
||||
starts_at = db_room.starts_at or datetime.now(UTC)
|
||||
beatmap_exists = await session.exec(
|
||||
select(exists().where(col(Beatmap.id) == item.beatmap_id))
|
||||
)
|
||||
beatmap_exists = await session.exec(select(exists().where(col(Beatmap.id) == item.beatmap_id)))
|
||||
if not beatmap_exists.one():
|
||||
fetcher = await get_fetcher()
|
||||
try:
|
||||
await Beatmap.get_or_fetch(
|
||||
session, fetcher, bid=item.beatmap_id
|
||||
)
|
||||
await Beatmap.get_or_fetch(session, fetcher, bid=item.beatmap_id)
|
||||
except HTTPError:
|
||||
raise InvokeException(
|
||||
"Failed to fetch beatmap, please retry later"
|
||||
)
|
||||
raise InvokeException("Failed to fetch beatmap, please retry later")
|
||||
await Playlist.add_to_db(item, room.room_id, session)
|
||||
|
||||
server_room = ServerMultiplayerRoom(
|
||||
@@ -262,9 +252,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
self.rooms[room.room_id] = server_room
|
||||
await server_room.set_handler()
|
||||
await self.event_logger.room_created(room.room_id, client.user_id)
|
||||
return await self.JoinRoomWithPassword(
|
||||
client, room.room_id, room.settings.password
|
||||
)
|
||||
return await self.JoinRoomWithPassword(client, room.room_id, room.settings.password)
|
||||
|
||||
async def JoinRoom(self, client: Client, room_id: int):
|
||||
return self.JoinRoomWithPassword(client, room_id, "")
|
||||
@@ -350,9 +338,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
beatmap_availability,
|
||||
)
|
||||
|
||||
async def ChangeBeatmapAvailability(
|
||||
self, client: Client, beatmap_availability: BeatmapAvailability
|
||||
):
|
||||
async def ChangeBeatmapAvailability(self, client: Client, beatmap_availability: BeatmapAvailability):
|
||||
server_room = self._ensure_in_room(client)
|
||||
room = server_room.room
|
||||
user = next((u for u in room.users if u.user_id == client.user_id), None)
|
||||
@@ -371,10 +357,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
user = next((u for u in room.users if u.user_id == client.user_id), None)
|
||||
if user is None:
|
||||
raise InvokeException("You are not in this room")
|
||||
logger.info(
|
||||
f"[MultiplayerHub] {client.user_id} adding "
|
||||
f"beatmap {item.beatmap_id} to room {room.room_id}"
|
||||
)
|
||||
logger.info(f"[MultiplayerHub] {client.user_id} adding beatmap {item.beatmap_id} to room {room.room_id}")
|
||||
await server_room.queue.add_item(
|
||||
item,
|
||||
user,
|
||||
@@ -388,10 +371,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
if user is None:
|
||||
raise InvokeException("You are not in this room")
|
||||
|
||||
logger.info(
|
||||
f"[MultiplayerHub] {client.user_id} editing "
|
||||
f"item {item.id} in room {room.room_id}"
|
||||
)
|
||||
logger.info(f"[MultiplayerHub] {client.user_id} editing item {item.id} in room {room.room_id}")
|
||||
await server_room.queue.edit_item(
|
||||
item,
|
||||
user,
|
||||
@@ -405,10 +385,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
if user is None:
|
||||
raise InvokeException("You are not in this room")
|
||||
|
||||
logger.info(
|
||||
f"[MultiplayerHub] {client.user_id} removing "
|
||||
f"item {item_id} from room {room.room_id}"
|
||||
)
|
||||
logger.info(f"[MultiplayerHub] {client.user_id} removing item {item_id} from room {room.room_id}")
|
||||
await server_room.queue.remove_item(
|
||||
item_id,
|
||||
user,
|
||||
@@ -424,9 +401,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
type=room.room.settings.match_type,
|
||||
queue_mode=room.room.settings.queue_mode,
|
||||
auto_skip=room.room.settings.auto_skip,
|
||||
auto_start_duration=int(
|
||||
room.room.settings.auto_start_duration.total_seconds()
|
||||
),
|
||||
auto_start_duration=int(room.room.settings.auto_start_duration.total_seconds()),
|
||||
host_id=room.room.host.user_id if room.room.host else None,
|
||||
)
|
||||
)
|
||||
@@ -456,9 +431,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
item_id,
|
||||
)
|
||||
|
||||
async def playlist_changed(
|
||||
self, room: ServerMultiplayerRoom, item: PlaylistItem, beatmap_changed: bool
|
||||
):
|
||||
async def playlist_changed(self, room: ServerMultiplayerRoom, item: PlaylistItem, beatmap_changed: bool):
|
||||
if item.id == room.room.settings.playlist_item_id:
|
||||
await self.validate_styles(room)
|
||||
await self.unready_all_users(room, beatmap_changed)
|
||||
@@ -468,9 +441,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
item,
|
||||
)
|
||||
|
||||
async def ChangeUserStyle(
|
||||
self, client: Client, beatmap_id: int | None, ruleset_id: int | None
|
||||
):
|
||||
async def ChangeUserStyle(self, client: Client, beatmap_id: int | None, ruleset_id: int | None):
|
||||
server_room = self._ensure_in_room(client)
|
||||
room = server_room.room
|
||||
user = next((u for u in room.users if u.user_id == client.user_id), None)
|
||||
@@ -496,9 +467,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
)
|
||||
async with with_db() as session:
|
||||
try:
|
||||
beatmap = await Beatmap.get_or_fetch(
|
||||
session, fetcher, bid=room.queue.current_item.beatmap_id
|
||||
)
|
||||
beatmap = await Beatmap.get_or_fetch(session, fetcher, bid=room.queue.current_item.beatmap_id)
|
||||
except HTTPError:
|
||||
raise InvokeException("Current item beatmap not found")
|
||||
beatmap_ids = (
|
||||
@@ -518,11 +487,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
if beatmap_id is not None and user_beatmap is None:
|
||||
beatmap_id = None
|
||||
beatmap_ruleset = user_beatmap[1] if user_beatmap else beatmap.mode
|
||||
if (
|
||||
ruleset_id is not None
|
||||
and beatmap_ruleset != GameMode.OSU
|
||||
and ruleset_id != beatmap_ruleset
|
||||
):
|
||||
if ruleset_id is not None and beatmap_ruleset != GameMode.OSU and ruleset_id != beatmap_ruleset:
|
||||
ruleset_id = None
|
||||
await self.change_user_style(
|
||||
beatmap_id,
|
||||
@@ -532,9 +497,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
)
|
||||
|
||||
for user in room.room.users:
|
||||
is_valid, valid_mods = room.queue.current_item.validate_user_mods(
|
||||
user, user.mods
|
||||
)
|
||||
is_valid, valid_mods = room.queue.current_item.validate_user_mods(user, user.mods)
|
||||
if not is_valid:
|
||||
await self.change_user_mods(valid_mods, room, user)
|
||||
|
||||
@@ -553,34 +516,24 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
raise InvokeException("Current item does not allow free user styles.")
|
||||
|
||||
async with with_db() as session:
|
||||
item_beatmap = await session.get(
|
||||
Beatmap, room.queue.current_item.beatmap_id
|
||||
)
|
||||
item_beatmap = await session.get(Beatmap, room.queue.current_item.beatmap_id)
|
||||
if item_beatmap is None:
|
||||
raise InvokeException("Item beatmap not found")
|
||||
|
||||
user_beatmap = (
|
||||
item_beatmap
|
||||
if beatmap_id is None
|
||||
else await session.get(Beatmap, beatmap_id)
|
||||
)
|
||||
user_beatmap = item_beatmap if beatmap_id is None else await session.get(Beatmap, beatmap_id)
|
||||
|
||||
if user_beatmap is None:
|
||||
raise InvokeException("Invalid beatmap selected.")
|
||||
|
||||
if user_beatmap.beatmapset_id != item_beatmap.beatmapset_id:
|
||||
raise InvokeException(
|
||||
"Selected beatmap is not from the same beatmap set."
|
||||
)
|
||||
raise InvokeException("Selected beatmap is not from the same beatmap set.")
|
||||
|
||||
if (
|
||||
ruleset_id is not None
|
||||
and user_beatmap.mode != GameMode.OSU
|
||||
and ruleset_id != int(user_beatmap.mode)
|
||||
):
|
||||
raise InvokeException(
|
||||
"Selected ruleset is not supported for the given beatmap."
|
||||
)
|
||||
raise InvokeException("Selected ruleset is not supported for the given beatmap.")
|
||||
|
||||
user.beatmap_id = beatmap_id
|
||||
user.ruleset_id = ruleset_id
|
||||
@@ -608,16 +561,10 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
room: ServerMultiplayerRoom,
|
||||
user: MultiplayerRoomUser,
|
||||
):
|
||||
is_valid, valid_mods = room.queue.current_item.validate_user_mods(
|
||||
user, new_mods
|
||||
)
|
||||
is_valid, valid_mods = room.queue.current_item.validate_user_mods(user, new_mods)
|
||||
if not is_valid:
|
||||
incompatible_mods = [
|
||||
mod["acronym"] for mod in new_mods if mod not in valid_mods
|
||||
]
|
||||
raise InvokeException(
|
||||
f"Incompatible mods were selected: {','.join(incompatible_mods)}"
|
||||
)
|
||||
incompatible_mods = [mod["acronym"] for mod in new_mods if mod not in valid_mods]
|
||||
raise InvokeException(f"Incompatible mods were selected: {','.join(incompatible_mods)}")
|
||||
|
||||
if user.mods == valid_mods:
|
||||
return
|
||||
@@ -640,16 +587,12 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
match new:
|
||||
case MultiplayerUserState.IDLE:
|
||||
if old.is_playing:
|
||||
raise InvokeException(
|
||||
"Cannot return to idle without aborting gameplay."
|
||||
)
|
||||
raise InvokeException("Cannot return to idle without aborting gameplay.")
|
||||
case MultiplayerUserState.READY:
|
||||
if old != MultiplayerUserState.IDLE:
|
||||
raise InvokeException(f"Cannot change state from {old} to {new}")
|
||||
if room.queue.current_item.expired:
|
||||
raise InvokeException(
|
||||
"Cannot ready up while all items have been played."
|
||||
)
|
||||
raise InvokeException("Cannot ready up while all items have been played.")
|
||||
case MultiplayerUserState.WAITING_FOR_LOAD:
|
||||
raise InvokeException(f"Cannot change state from {old} to {new}")
|
||||
case MultiplayerUserState.LOADED:
|
||||
@@ -688,9 +631,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
MultiplayerRoomState.PLAYING,
|
||||
)
|
||||
):
|
||||
raise InvokeException(
|
||||
f"Cannot change state from {old} to {new}"
|
||||
)
|
||||
raise InvokeException(f"Cannot change state from {old} to {new}")
|
||||
case _:
|
||||
raise InvokeException(f"Invalid state transition from {old} to {new}")
|
||||
|
||||
@@ -713,9 +654,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
if not user.state.is_playing:
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"[MultiplayerHub] User {user.user_id} changing state from {user.state} to {state}"
|
||||
)
|
||||
logger.info(f"[MultiplayerHub] User {user.user_id} changing state from {user.state} to {state}")
|
||||
|
||||
await self.validate_user_stare(
|
||||
server_room,
|
||||
@@ -737,10 +676,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
user: MultiplayerRoomUser,
|
||||
state: MultiplayerUserState,
|
||||
):
|
||||
logger.info(
|
||||
f"[MultiplayerHub] {user.user_id}'s state "
|
||||
f"changed from {user.state} to {state}"
|
||||
)
|
||||
logger.info(f"[MultiplayerHub] {user.user_id}'s state changed from {user.state} to {state}")
|
||||
user.state = state
|
||||
await self.broadcast_group_call(
|
||||
self.group_id(room.room.room_id),
|
||||
@@ -760,23 +696,17 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
|
||||
# If switching to spectating during gameplay, immediately request load
|
||||
if room_state == MultiplayerRoomState.WAITING_FOR_LOAD:
|
||||
logger.info(
|
||||
f"[MultiplayerHub] Spectator {user.user_id} joining during load phase"
|
||||
)
|
||||
logger.info(f"[MultiplayerHub] Spectator {user.user_id} joining during load phase")
|
||||
await self.call_noblock(client, "LoadRequested")
|
||||
|
||||
elif room_state == MultiplayerRoomState.PLAYING:
|
||||
logger.info(
|
||||
f"[MultiplayerHub] Spectator {user.user_id} joining during active gameplay"
|
||||
)
|
||||
logger.info(f"[MultiplayerHub] Spectator {user.user_id} joining during active gameplay")
|
||||
await self.call_noblock(client, "LoadRequested")
|
||||
|
||||
|
||||
# Also sync the spectator with current game state
|
||||
await self._send_current_gameplay_state_to_spectator(client, room)
|
||||
|
||||
async def _send_current_gameplay_state_to_spectator(
|
||||
self, client: Client, room: ServerMultiplayerRoom
|
||||
):
|
||||
async def _send_current_gameplay_state_to_spectator(self, client: Client, room: ServerMultiplayerRoom):
|
||||
"""
|
||||
Send current gameplay state information to a newly joined spectator.
|
||||
This helps spectators sync with ongoing gameplay.
|
||||
@@ -794,27 +724,20 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
room_user.user_id,
|
||||
room_user.state,
|
||||
)
|
||||
|
||||
|
||||
# If the room is in OPEN state but we have users in RESULTS state,
|
||||
# this means the game just finished and we should send ResultsReady
|
||||
if (room.room.state == MultiplayerRoomState.OPEN and
|
||||
any(u.state == MultiplayerUserState.RESULTS for u in room.room.users)):
|
||||
logger.debug(
|
||||
f"[MultiplayerHub] Sending ResultsReady to new spectator {client.user_id}"
|
||||
)
|
||||
if room.room.state == MultiplayerRoomState.OPEN and any(
|
||||
u.state == MultiplayerUserState.RESULTS for u in room.room.users
|
||||
):
|
||||
logger.debug(f"[MultiplayerHub] Sending ResultsReady to new spectator {client.user_id}")
|
||||
await self.call_noblock(client, "ResultsReady")
|
||||
|
||||
logger.debug(
|
||||
f"[MultiplayerHub] Sent current gameplay state to spectator {client.user_id}"
|
||||
)
|
||||
logger.debug(f"[MultiplayerHub] Sent current gameplay state to spectator {client.user_id}")
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[MultiplayerHub] Failed to send gameplay state to spectator {client.user_id}: {e}"
|
||||
)
|
||||
logger.error(f"[MultiplayerHub] Failed to send gameplay state to spectator {client.user_id}: {e}")
|
||||
|
||||
async def _send_room_state_to_new_user(
|
||||
self, client: Client, room: ServerMultiplayerRoom
|
||||
):
|
||||
async def _send_room_state_to_new_user(self, client: Client, room: ServerMultiplayerRoom):
|
||||
"""
|
||||
Send complete room state to a newly joined user.
|
||||
Critical for spectators joining ongoing games.
|
||||
@@ -847,28 +770,21 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
|
||||
# Critical fix: If room is OPEN but has users in RESULTS state,
|
||||
# send ResultsReady to new joiners (including spectators)
|
||||
if (room.room.state == MultiplayerRoomState.OPEN and
|
||||
any(u.state == MultiplayerUserState.RESULTS for u in room.room.users)):
|
||||
logger.info(
|
||||
f"[MultiplayerHub] Sending ResultsReady to newly joined user {client.user_id}"
|
||||
)
|
||||
if room.room.state == MultiplayerRoomState.OPEN and any(
|
||||
u.state == MultiplayerUserState.RESULTS for u in room.room.users
|
||||
):
|
||||
logger.info(f"[MultiplayerHub] Sending ResultsReady to newly joined user {client.user_id}")
|
||||
await self.call_noblock(client, "ResultsReady")
|
||||
|
||||
# Critical addition: Send current playing users to SpectatorHub for cross-hub sync
|
||||
# This ensures spectators can watch multiplayer players properly
|
||||
await self._sync_with_spectator_hub(client, room)
|
||||
|
||||
logger.debug(
|
||||
f"[MultiplayerHub] Sent complete room state to new user {client.user_id}"
|
||||
)
|
||||
logger.debug(f"[MultiplayerHub] Sent complete room state to new user {client.user_id}")
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[MultiplayerHub] Failed to send room state to user {client.user_id}: {e}"
|
||||
)
|
||||
logger.error(f"[MultiplayerHub] Failed to send room state to user {client.user_id}: {e}")
|
||||
|
||||
async def _sync_with_spectator_hub(
|
||||
self, client: Client, room: ServerMultiplayerRoom
|
||||
):
|
||||
async def _sync_with_spectator_hub(self, client: Client, room: ServerMultiplayerRoom):
|
||||
"""
|
||||
Sync with SpectatorHub to ensure cross-hub spectating works properly.
|
||||
This is crucial for users watching multiplayer players from other pages.
|
||||
@@ -893,13 +809,16 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
f"[MultiplayerHub] Synced spectator state for user {room_user.user_id} "
|
||||
f"to new client {client.user_id}"
|
||||
)
|
||||
|
||||
|
||||
# Critical addition: Notify SpectatorHub about users in RESULTS state
|
||||
elif room_user.state == MultiplayerUserState.RESULTS:
|
||||
# Create a synthetic finished state for cross-hub spectating
|
||||
try:
|
||||
from app.models.spectator_hub import SpectatedUserState, SpectatorState
|
||||
|
||||
from app.models.spectator_hub import (
|
||||
SpectatedUserState,
|
||||
SpectatorState,
|
||||
)
|
||||
|
||||
finished_state = SpectatorState(
|
||||
beatmap_id=room.queue.current_item.beatmap_id,
|
||||
ruleset_id=room_user.ruleset_id or 0,
|
||||
@@ -919,9 +838,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
f"to client {client.user_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
f"[MultiplayerHub] Failed to create synthetic finished state: {e}"
|
||||
)
|
||||
logger.debug(f"[MultiplayerHub] Failed to create synthetic finished state: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"[MultiplayerHub] Failed to sync with SpectatorHub: {e}")
|
||||
@@ -933,75 +850,55 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
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 any(u.state == MultiplayerUserState.READY for u in room.room.users)
|
||||
and not any(
|
||||
isinstance(countdown, MatchStartCountdown)
|
||||
for countdown in room.room.active_countdowns
|
||||
isinstance(countdown, MatchStartCountdown) for countdown in room.room.active_countdowns
|
||||
)
|
||||
):
|
||||
await room.start_countdown(
|
||||
MatchStartCountdown(
|
||||
time_remaining=room.room.settings.auto_start_duration
|
||||
),
|
||||
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]
|
||||
)
|
||||
played_count = len([True for user in room.room.users if user.state.is_playing])
|
||||
ready_count = len(
|
||||
[
|
||||
True
|
||||
for user in room.room.users
|
||||
if user.state == MultiplayerUserState.READY_FOR_GAMEPLAY
|
||||
]
|
||||
[True for user in room.room.users if user.state == MultiplayerUserState.READY_FOR_GAMEPLAY]
|
||||
)
|
||||
if played_count == ready_count:
|
||||
await self.start_gameplay(room)
|
||||
case MultiplayerRoomState.PLAYING:
|
||||
if all(
|
||||
u.state != MultiplayerUserState.PLAYING for u in room.room.users
|
||||
):
|
||||
if all(u.state != MultiplayerUserState.PLAYING for u in room.room.users):
|
||||
any_user_finished_playing = False
|
||||
|
||||
|
||||
# Handle finished players first
|
||||
for u in filter(
|
||||
lambda u: u.state == MultiplayerUserState.FINISHED_PLAY,
|
||||
room.room.users,
|
||||
):
|
||||
any_user_finished_playing = True
|
||||
await self.change_user_state(
|
||||
room, u, MultiplayerUserState.RESULTS
|
||||
)
|
||||
|
||||
await self.change_user_state(room, u, MultiplayerUserState.RESULTS)
|
||||
|
||||
# Critical fix: Handle spectators who should also see results
|
||||
# Move spectators to RESULTS state so they can see the results screen
|
||||
for u in filter(
|
||||
lambda u: u.state == MultiplayerUserState.SPECTATING,
|
||||
room.room.users,
|
||||
):
|
||||
logger.debug(
|
||||
f"[MultiplayerHub] Moving spectator {u.user_id} to RESULTS state"
|
||||
)
|
||||
await self.change_user_state(
|
||||
room, u, MultiplayerUserState.RESULTS
|
||||
)
|
||||
|
||||
logger.debug(f"[MultiplayerHub] Moving spectator {u.user_id} to RESULTS state")
|
||||
await self.change_user_state(room, u, MultiplayerUserState.RESULTS)
|
||||
|
||||
await self.change_room_state(room, MultiplayerRoomState.OPEN)
|
||||
|
||||
|
||||
# Send ResultsReady to all room members
|
||||
await self.broadcast_group_call(
|
||||
self.group_id(room.room.room_id),
|
||||
"ResultsReady",
|
||||
)
|
||||
|
||||
|
||||
# Critical addition: Notify SpectatorHub about finished games
|
||||
# This ensures cross-hub spectating works properly
|
||||
await self._notify_spectator_hub_game_ended(room)
|
||||
|
||||
|
||||
if any_user_finished_playing:
|
||||
await self.event_logger.game_completed(
|
||||
room.room.room_id,
|
||||
@@ -1014,13 +911,8 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
)
|
||||
await room.queue.finish_current_item()
|
||||
|
||||
async def change_room_state(
|
||||
self, room: ServerMultiplayerRoom, state: MultiplayerRoomState
|
||||
):
|
||||
logger.debug(
|
||||
f"[MultiplayerHub] Room {room.room.room_id} state "
|
||||
f"changed from {room.room.state} to {state}"
|
||||
)
|
||||
async def change_room_state(self, room: ServerMultiplayerRoom, state: MultiplayerRoomState):
|
||||
logger.debug(f"[MultiplayerHub] Room {room.room.room_id} state changed from {room.room.state} to {state}")
|
||||
room.room.state = state
|
||||
await self.broadcast_group_call(
|
||||
self.group_id(room.room.room_id),
|
||||
@@ -1064,10 +956,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
u
|
||||
for u in room.room.users
|
||||
if u.availability.state == DownloadState.LOCALLY_AVAILABLE
|
||||
and (
|
||||
u.state == MultiplayerUserState.READY
|
||||
or u.state == MultiplayerUserState.IDLE
|
||||
)
|
||||
and (u.state == MultiplayerUserState.READY or u.state == MultiplayerUserState.IDLE)
|
||||
]
|
||||
for u in ready_users:
|
||||
await self.change_user_state(room, u, MultiplayerUserState.WAITING_FOR_LOAD)
|
||||
@@ -1080,9 +969,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
"LoadRequested",
|
||||
)
|
||||
await room.start_countdown(
|
||||
ForceGameplayStartCountdown(
|
||||
time_remaining=timedelta(seconds=GAMEPLAY_LOAD_TIMEOUT)
|
||||
),
|
||||
ForceGameplayStartCountdown(time_remaining=timedelta(seconds=GAMEPLAY_LOAD_TIMEOUT)),
|
||||
self.start_gameplay,
|
||||
)
|
||||
await self.event_logger.game_started(
|
||||
@@ -1133,9 +1020,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
else:
|
||||
await room.queue.finish_current_item()
|
||||
|
||||
async def send_match_event(
|
||||
self, room: ServerMultiplayerRoom, event: MatchServerEvent
|
||||
):
|
||||
async def send_match_event(self, room: ServerMultiplayerRoom, event: MatchServerEvent):
|
||||
await self.broadcast_group_call(
|
||||
self.group_id(room.room.room_id),
|
||||
"MatchEvent",
|
||||
@@ -1183,24 +1068,16 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
await self.end_room(room)
|
||||
return
|
||||
await self.update_room_state(room)
|
||||
if (
|
||||
len(room.room.users) != 0
|
||||
and room.room.host
|
||||
and room.room.host.user_id == user.user_id
|
||||
):
|
||||
if len(room.room.users) != 0 and room.room.host and room.room.host.user_id == user.user_id:
|
||||
next_host = room.room.users[0]
|
||||
await self.set_host(room, next_host)
|
||||
|
||||
if kicked:
|
||||
if client:
|
||||
await self.call_noblock(client, "UserKicked", user)
|
||||
await self.broadcast_group_call(
|
||||
self.group_id(room.room.room_id), "UserKicked", user
|
||||
)
|
||||
await self.broadcast_group_call(self.group_id(room.room.room_id), "UserKicked", user)
|
||||
else:
|
||||
await self.broadcast_group_call(
|
||||
self.group_id(room.room.room_id), "UserLeft", user
|
||||
)
|
||||
await self.broadcast_group_call(self.group_id(room.room.room_id), "UserLeft", user)
|
||||
|
||||
async def end_room(self, room: ServerMultiplayerRoom):
|
||||
assert room.room.host
|
||||
@@ -1214,9 +1091,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
type=room.room.settings.match_type,
|
||||
queue_mode=room.room.settings.queue_mode,
|
||||
auto_skip=room.room.settings.auto_skip,
|
||||
auto_start_duration=int(
|
||||
room.room.settings.auto_start_duration.total_seconds()
|
||||
),
|
||||
auto_start_duration=int(room.room.settings.auto_start_duration.total_seconds()),
|
||||
host_id=room.room.host.user_id,
|
||||
)
|
||||
)
|
||||
@@ -1262,10 +1137,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
)
|
||||
target_client = self.get_client_by_id(str(user.user_id))
|
||||
await self.make_user_leave(target_client, server_room, user, kicked=True)
|
||||
logger.info(
|
||||
f"[MultiplayerHub] {user.user_id} was kicked from room {room.room_id}"
|
||||
f"by {client.user_id}"
|
||||
)
|
||||
logger.info(f"[MultiplayerHub] {user.user_id} was kicked from room {room.room_id}by {client.user_id}")
|
||||
|
||||
async def set_host(self, room: ServerMultiplayerRoom, user: MultiplayerRoomUser):
|
||||
room.room.host = user
|
||||
@@ -1289,10 +1161,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
new_host.user_id,
|
||||
)
|
||||
await self.set_host(server_room, new_host)
|
||||
logger.info(
|
||||
f"[MultiplayerHub] {client.user_id} transferred host to {new_host.user_id}"
|
||||
f" in room {room.room_id}"
|
||||
)
|
||||
logger.info(f"[MultiplayerHub] {client.user_id} transferred host to {new_host.user_id} in room {room.room_id}")
|
||||
|
||||
async def AbortGameplay(self, client: Client):
|
||||
server_room = self._ensure_in_room(client)
|
||||
@@ -1316,10 +1185,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
room = server_room.room
|
||||
self._ensure_host(client, server_room)
|
||||
|
||||
if (
|
||||
room.state != MultiplayerRoomState.PLAYING
|
||||
and room.state != MultiplayerRoomState.WAITING_FOR_LOAD
|
||||
):
|
||||
if room.state != MultiplayerRoomState.PLAYING and room.state != MultiplayerRoomState.WAITING_FOR_LOAD:
|
||||
raise InvokeException("Cannot abort a match that hasn't started.")
|
||||
|
||||
await asyncio.gather(
|
||||
@@ -1335,13 +1201,9 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
GameplayAbortReason.HOST_ABORTED,
|
||||
)
|
||||
await self.update_room_state(server_room)
|
||||
logger.info(
|
||||
f"[MultiplayerHub] {client.user_id} aborted match in room {room.room_id}"
|
||||
)
|
||||
logger.info(f"[MultiplayerHub] {client.user_id} aborted match in room {room.room_id}")
|
||||
|
||||
async def change_user_match_state(
|
||||
self, room: ServerMultiplayerRoom, user: MultiplayerRoomUser
|
||||
):
|
||||
async def change_user_match_state(self, room: ServerMultiplayerRoom, user: MultiplayerRoomUser):
|
||||
await self.broadcast_group_call(
|
||||
self.group_id(room.room.room_id),
|
||||
"MatchUserStateChanged",
|
||||
@@ -1402,10 +1264,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
)
|
||||
if countdown is None:
|
||||
return
|
||||
if (
|
||||
isinstance(countdown, MatchStartCountdown)
|
||||
and room.settings.auto_start_enabled
|
||||
) or isinstance(
|
||||
if (isinstance(countdown, MatchStartCountdown) and room.settings.auto_start_enabled) or isinstance(
|
||||
countdown, (ForceGameplayStartCountdown | ServerShuttingDownCountdown)
|
||||
):
|
||||
raise InvokeException("Cannot stop the requested countdown")
|
||||
@@ -1447,25 +1306,16 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
raise InvokeException("User already invited")
|
||||
if db_user.is_restricted:
|
||||
raise InvokeException("User is restricted")
|
||||
if (
|
||||
inviter_relationship
|
||||
and inviter_relationship.type == RelationshipType.BLOCK
|
||||
):
|
||||
if inviter_relationship and inviter_relationship.type == RelationshipType.BLOCK:
|
||||
raise InvokeException("Cannot perform action due to user being blocked")
|
||||
if (
|
||||
target_relationship
|
||||
and target_relationship.type == RelationshipType.BLOCK
|
||||
):
|
||||
if target_relationship and target_relationship.type == RelationshipType.BLOCK:
|
||||
raise InvokeException("Cannot perform action due to user being blocked")
|
||||
if (
|
||||
db_user.pm_friends_only
|
||||
and target_relationship is not None
|
||||
and target_relationship.type != RelationshipType.FOLLOW
|
||||
):
|
||||
raise InvokeException(
|
||||
"Cannot perform action "
|
||||
"because user has disabled non-friend communications"
|
||||
)
|
||||
raise InvokeException("Cannot perform action because user has disabled non-friend communications")
|
||||
|
||||
target_client = self.get_client_by_id(str(user_id))
|
||||
if target_client is None:
|
||||
@@ -1478,9 +1328,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
room.settings.password,
|
||||
)
|
||||
|
||||
async def unready_all_users(
|
||||
self, room: ServerMultiplayerRoom, reset_beatmap_availability: bool
|
||||
):
|
||||
async def unready_all_users(self, room: ServerMultiplayerRoom, reset_beatmap_availability: bool):
|
||||
await asyncio.gather(
|
||||
*[
|
||||
self.change_user_state(
|
||||
@@ -1512,8 +1360,8 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
"""
|
||||
try:
|
||||
# Import here to avoid circular imports
|
||||
from app.signalr.hub import SpectatorHubs
|
||||
from app.models.spectator_hub import SpectatedUserState, SpectatorState
|
||||
from app.signalr.hub import SpectatorHubs
|
||||
|
||||
# For each user who finished the game, notify SpectatorHub
|
||||
for room_user in room.room.users:
|
||||
@@ -1534,13 +1382,9 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
room_user.user_id,
|
||||
finished_state,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"[MultiplayerHub] Notified SpectatorHub that user {room_user.user_id} finished game"
|
||||
)
|
||||
|
||||
logger.debug(f"[MultiplayerHub] Notified SpectatorHub that user {room_user.user_id} finished game")
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
f"[MultiplayerHub] Failed to notify SpectatorHub about game end: {e}"
|
||||
)
|
||||
logger.debug(f"[MultiplayerHub] Failed to notify SpectatorHub about game end: {e}")
|
||||
# This is not critical, so we don't raise the exception
|
||||
|
||||
@@ -31,7 +31,7 @@ from app.models.spectator_hub import (
|
||||
StoreClientState,
|
||||
StoreScore,
|
||||
)
|
||||
from app.utils import unix_timestamp_to_windows
|
||||
from app.utils import bg_tasks, unix_timestamp_to_windows
|
||||
|
||||
from .hub import Client, Hub
|
||||
|
||||
@@ -111,20 +111,14 @@ async def save_replay(
|
||||
last_time = 0
|
||||
for frame in frames:
|
||||
time = round(frame.time)
|
||||
frame_strs.append(
|
||||
f"{time - last_time}|{frame.mouse_x or 0.0}"
|
||||
f"|{frame.mouse_y or 0.0}|{frame.button_state}"
|
||||
)
|
||||
frame_strs.append(f"{time - last_time}|{frame.mouse_x or 0.0}|{frame.mouse_y or 0.0}|{frame.button_state}")
|
||||
last_time = time
|
||||
frame_strs.append("-12345|0|0|0")
|
||||
|
||||
compressed = lzma.compress(
|
||||
",".join(frame_strs).encode("ascii"), format=lzma.FORMAT_ALONE
|
||||
)
|
||||
compressed = lzma.compress(",".join(frame_strs).encode("ascii"), format=lzma.FORMAT_ALONE)
|
||||
data.extend(struct.pack("<i", len(compressed)))
|
||||
data.extend(compressed)
|
||||
data.extend(struct.pack("<q", score.id))
|
||||
assert score.id
|
||||
score_info = LegacyReplaySoloScoreInfo(
|
||||
online_id=score.id,
|
||||
mods=score.mods,
|
||||
@@ -135,16 +129,12 @@ async def save_replay(
|
||||
user_id=score.user_id,
|
||||
total_score_without_mods=score.total_score_without_mods,
|
||||
)
|
||||
compressed = lzma.compress(
|
||||
json.dumps(score_info).encode(), format=lzma.FORMAT_ALONE
|
||||
)
|
||||
compressed = lzma.compress(json.dumps(score_info).encode(), format=lzma.FORMAT_ALONE)
|
||||
data.extend(struct.pack("<i", len(compressed)))
|
||||
data.extend(compressed)
|
||||
|
||||
storage_service = get_storage_service()
|
||||
replay_path = (
|
||||
f"replays/{score.id}_{score.beatmap_id}_{score.user_id}_lazer_replay.osr"
|
||||
)
|
||||
replay_path = f"replays/{score.id}_{score.beatmap_id}_{score.user_id}_lazer_replay.osr"
|
||||
await storage_service.write_file(
|
||||
replay_path,
|
||||
bytes(data),
|
||||
@@ -173,6 +163,7 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
|
||||
# Use centralized offline status management
|
||||
from app.service.online_status_manager import online_status_manager
|
||||
|
||||
await online_status_manager.set_user_offline(user_id)
|
||||
|
||||
if state.state:
|
||||
@@ -181,13 +172,9 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
# Critical fix: Notify all watched users that this spectator has disconnected
|
||||
# This matches the official CleanUpState implementation
|
||||
for watched_user_id in state.watched_user:
|
||||
if (
|
||||
target_client := self.get_client_by_id(str(watched_user_id))
|
||||
) is not None:
|
||||
if (target_client := self.get_client_by_id(str(watched_user_id))) is not None:
|
||||
await self.call_noblock(target_client, "UserEndedWatching", user_id)
|
||||
logger.debug(
|
||||
f"[SpectatorHub] Notified {watched_user_id} that {user_id} stopped watching"
|
||||
)
|
||||
logger.debug(f"[SpectatorHub] Notified {watched_user_id} that {user_id} stopped watching")
|
||||
|
||||
async def on_client_connect(self, client: Client) -> None:
|
||||
"""
|
||||
@@ -198,6 +185,7 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
|
||||
# Use centralized online status management
|
||||
from app.service.online_status_manager import online_status_manager
|
||||
|
||||
await online_status_manager.set_user_online(client.user_id, "spectator")
|
||||
|
||||
# Send all current player states to the new client
|
||||
@@ -208,17 +196,13 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
active_states.append((user_id, store.state))
|
||||
|
||||
if active_states:
|
||||
logger.debug(
|
||||
f"[SpectatorHub] Sending {len(active_states)} active player states to {client.user_id}"
|
||||
)
|
||||
logger.debug(f"[SpectatorHub] Sending {len(active_states)} active player states to {client.user_id}")
|
||||
# Send states sequentially to avoid overwhelming the client
|
||||
for user_id, state in active_states:
|
||||
try:
|
||||
await self.call_noblock(client, "UserBeganPlaying", user_id, state)
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
f"[SpectatorHub] Failed to send state for user {user_id}: {e}"
|
||||
)
|
||||
logger.debug(f"[SpectatorHub] Failed to send state for user {user_id}: {e}")
|
||||
|
||||
# Also sync with MultiplayerHub for cross-hub spectating
|
||||
await self._sync_with_multiplayer_hub(client)
|
||||
@@ -236,10 +220,7 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
for room_id, server_room in MultiplayerHubs.rooms.items():
|
||||
for room_user in server_room.room.users:
|
||||
# Send state for users who are playing or in results
|
||||
if (
|
||||
room_user.state.is_playing
|
||||
and room_user.user_id not in self.state
|
||||
):
|
||||
if room_user.state.is_playing and room_user.user_id not in self.state:
|
||||
# Create a synthetic SpectatorState for multiplayer players
|
||||
# This helps with cross-hub spectating
|
||||
try:
|
||||
@@ -261,13 +242,12 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
f"[SpectatorHub] Sent synthetic multiplayer state for user {room_user.user_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
f"[SpectatorHub] Failed to create synthetic state: {e}"
|
||||
)
|
||||
|
||||
logger.debug(f"[SpectatorHub] Failed to create synthetic state: {e}")
|
||||
|
||||
# Critical addition: Notify about finished players in multiplayer games
|
||||
elif (
|
||||
hasattr(room_user.state, 'name') and room_user.state.name == 'RESULTS'
|
||||
hasattr(room_user.state, "name")
|
||||
and room_user.state.name == "RESULTS"
|
||||
and room_user.user_id not in self.state
|
||||
):
|
||||
try:
|
||||
@@ -286,21 +266,15 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
room_user.user_id,
|
||||
finished_state,
|
||||
)
|
||||
logger.debug(
|
||||
f"[SpectatorHub] Sent synthetic finished state for user {room_user.user_id}"
|
||||
)
|
||||
logger.debug(f"[SpectatorHub] Sent synthetic finished state for user {room_user.user_id}")
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
f"[SpectatorHub] Failed to create synthetic finished state: {e}"
|
||||
)
|
||||
logger.debug(f"[SpectatorHub] Failed to create synthetic finished state: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"[SpectatorHub] Failed to sync with MultiplayerHub: {e}")
|
||||
# This is not critical, so we don't raise the exception
|
||||
|
||||
async def BeginPlaySession(
|
||||
self, client: Client, score_token: int, state: SpectatorState
|
||||
) -> None:
|
||||
async def BeginPlaySession(self, client: Client, score_token: int, state: SpectatorState) -> None:
|
||||
user_id = int(client.connection_id)
|
||||
store = self.get_or_create_state(client)
|
||||
if store.state is not None:
|
||||
@@ -312,14 +286,10 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
async with with_db() as session:
|
||||
async with session.begin():
|
||||
try:
|
||||
beatmap = await Beatmap.get_or_fetch(
|
||||
session, fetcher, bid=state.beatmap_id
|
||||
)
|
||||
beatmap = await Beatmap.get_or_fetch(session, fetcher, bid=state.beatmap_id)
|
||||
except HTTPError:
|
||||
raise InvokeException(f"Beatmap {state.beatmap_id} not found.")
|
||||
user = (
|
||||
await session.exec(select(User).where(User.id == user_id))
|
||||
).first()
|
||||
user = (await session.exec(select(User).where(User.id == user_id))).first()
|
||||
if not user:
|
||||
return
|
||||
name = user.username
|
||||
@@ -342,8 +312,8 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
from app.router.v2.stats import add_playing_user
|
||||
from app.service.online_status_manager import online_status_manager
|
||||
|
||||
asyncio.create_task(add_playing_user(user_id))
|
||||
|
||||
bg_tasks.add_task(add_playing_user, user_id)
|
||||
|
||||
# Critical fix: Maintain metadata online presence during gameplay
|
||||
# This ensures the user appears online while playing
|
||||
await online_status_manager.refresh_user_online_status(user_id, "playing")
|
||||
@@ -367,6 +337,7 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
# Critical fix: Refresh online status during active gameplay
|
||||
# This prevents users from appearing offline while playing
|
||||
from app.service.online_status_manager import online_status_manager
|
||||
|
||||
await online_status_manager.refresh_user_online_status(user_id, "playing_active")
|
||||
|
||||
header = frame_data.header
|
||||
@@ -377,15 +348,13 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
score_info.statistics = header.statistics
|
||||
store.score.replay_frames.extend(frame_data.frames)
|
||||
|
||||
await self.broadcast_group_call(
|
||||
self.group_id(user_id), "UserSentFrames", user_id, frame_data
|
||||
)
|
||||
await self.broadcast_group_call(self.group_id(user_id), "UserSentFrames", user_id, frame_data)
|
||||
|
||||
async def EndPlaySession(self, client: Client, state: SpectatorState) -> None:
|
||||
user_id = int(client.connection_id)
|
||||
store = self.get_or_create_state(client)
|
||||
score = store.score
|
||||
|
||||
|
||||
# Early return if no active session
|
||||
if (
|
||||
score is None
|
||||
@@ -398,19 +367,19 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
|
||||
try:
|
||||
# Process score if conditions are met
|
||||
if (
|
||||
settings.enable_all_beatmap_leaderboard
|
||||
and store.beatmap_status.has_leaderboard()
|
||||
) and any(k.is_hit() and v > 0 for k, v in score.score_info.statistics.items()):
|
||||
if (settings.enable_all_beatmap_leaderboard and store.beatmap_status.has_leaderboard()) and any(
|
||||
k.is_hit() and v > 0 for k, v in score.score_info.statistics.items()
|
||||
):
|
||||
await self._process_score(store, client)
|
||||
|
||||
|
||||
# End the play session and notify watchers
|
||||
await self._end_session(user_id, state, store)
|
||||
|
||||
# Remove from playing user tracking
|
||||
from app.router.v2.stats import remove_playing_user
|
||||
asyncio.create_task(remove_playing_user(user_id))
|
||||
|
||||
|
||||
bg_tasks.add_task(remove_playing_user, user_id)
|
||||
|
||||
finally:
|
||||
# CRITICAL FIX: Always clear state in finally block to ensure cleanup
|
||||
# This matches the official C# implementation pattern
|
||||
@@ -439,9 +408,9 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
)
|
||||
result = await session.exec(
|
||||
select(Score)
|
||||
.options(joinedload(Score.beatmap)) # pyright: ignore[reportArgumentType]
|
||||
.options(joinedload(Score.beatmap))
|
||||
.where(
|
||||
Score.id == sub_query,
|
||||
Score.id == sub_query.scalar_subquery(),
|
||||
Score.user_id == user_id,
|
||||
)
|
||||
)
|
||||
@@ -472,18 +441,12 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
frames=store.score.replay_frames,
|
||||
)
|
||||
|
||||
async def _end_session(
|
||||
self, user_id: int, state: SpectatorState, store: StoreClientState
|
||||
) -> None:
|
||||
async def _end_session(self, user_id: int, state: SpectatorState, store: StoreClientState) -> None:
|
||||
async def _add_failtime():
|
||||
async with with_db() as session:
|
||||
failtime = await session.get(FailTime, state.beatmap_id)
|
||||
total_length = (
|
||||
await session.exec(
|
||||
select(Beatmap.total_length).where(
|
||||
Beatmap.id == state.beatmap_id
|
||||
)
|
||||
)
|
||||
await session.exec(select(Beatmap.total_length).where(Beatmap.id == state.beatmap_id))
|
||||
).one()
|
||||
index = clamp(round((exit_time / total_length) * 100), 0, 99)
|
||||
if failtime is not None:
|
||||
@@ -495,7 +458,8 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
elif state.state == SpectatedUserState.Quit:
|
||||
resp.exit[index] += 1
|
||||
|
||||
new_failtime = FailTime.from_resp(state.beatmap_id, resp) # pyright: ignore[reportArgumentType]
|
||||
assert state.beatmap_id
|
||||
new_failtime = FailTime.from_resp(state.beatmap_id, resp)
|
||||
if failtime is not None:
|
||||
await session.merge(new_failtime)
|
||||
else:
|
||||
@@ -527,9 +491,7 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
|
||||
if state.state == SpectatedUserState.Playing:
|
||||
state.state = SpectatedUserState.Quit
|
||||
logger.debug(
|
||||
f"[SpectatorHub] Changed state from Playing to Quit for user {user_id}"
|
||||
)
|
||||
logger.debug(f"[SpectatorHub] Changed state from Playing to Quit for user {user_id}")
|
||||
|
||||
# Calculate exit time safely
|
||||
exit_time = 0
|
||||
@@ -558,10 +520,7 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
self.tasks.add(task)
|
||||
task.add_done_callback(self.tasks.discard)
|
||||
|
||||
logger.info(
|
||||
f"[SpectatorHub] {user_id} finished playing {state.beatmap_id} "
|
||||
f"with {state.state}"
|
||||
)
|
||||
logger.info(f"[SpectatorHub] {user_id} finished playing {state.beatmap_id} with {state.state}")
|
||||
await self.broadcast_group_call(
|
||||
self.group_id(user_id),
|
||||
"UserFinishedPlaying",
|
||||
@@ -585,9 +544,7 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
# CRITICAL FIX: Only send state if user is actually playing
|
||||
# Don't send state for finished/quit games
|
||||
if target_store.state.state == SpectatedUserState.Playing:
|
||||
logger.debug(
|
||||
f"[SpectatorHub] {target_id} is currently playing, sending state"
|
||||
)
|
||||
logger.debug(f"[SpectatorHub] {target_id} is currently playing, sending state")
|
||||
# Send current state to the watcher immediately
|
||||
await self.call_noblock(
|
||||
client,
|
||||
@@ -613,25 +570,17 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
# Get watcher's username and notify the target user
|
||||
try:
|
||||
async with with_db() as session:
|
||||
username = (
|
||||
await session.exec(select(User.username).where(User.id == user_id))
|
||||
).first()
|
||||
username = (await session.exec(select(User.username).where(User.id == user_id))).first()
|
||||
if not username:
|
||||
logger.warning(
|
||||
f"[SpectatorHub] Could not find username for user {user_id}"
|
||||
)
|
||||
logger.warning(f"[SpectatorHub] Could not find username for user {user_id}")
|
||||
return
|
||||
|
||||
# Notify target user that someone started watching
|
||||
if (target_client := self.get_client_by_id(str(target_id))) is not None:
|
||||
# Create watcher info array (matches official format)
|
||||
watcher_info = [[user_id, username]]
|
||||
await self.call_noblock(
|
||||
target_client, "UserStartedWatching", watcher_info
|
||||
)
|
||||
logger.debug(
|
||||
f"[SpectatorHub] Notified {target_id} that {username} started watching"
|
||||
)
|
||||
await self.call_noblock(target_client, "UserStartedWatching", watcher_info)
|
||||
logger.debug(f"[SpectatorHub] Notified {target_id} that {username} started watching")
|
||||
except Exception as e:
|
||||
logger.error(f"[SpectatorHub] Error notifying target user {target_id}: {e}")
|
||||
|
||||
@@ -654,10 +603,6 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
# Notify target user that watcher stopped watching
|
||||
if (target_client := self.get_client_by_id(str(target_id))) is not None:
|
||||
await self.call_noblock(target_client, "UserEndedWatching", user_id)
|
||||
logger.debug(
|
||||
f"[SpectatorHub] Notified {target_id} that {user_id} stopped watching"
|
||||
)
|
||||
logger.debug(f"[SpectatorHub] Notified {target_id} that {user_id} stopped watching")
|
||||
else:
|
||||
logger.debug(
|
||||
f"[SpectatorHub] Target user {target_id} not found for end watching notification"
|
||||
)
|
||||
logger.debug(f"[SpectatorHub] Target user {target_id} not found for end watching notification")
|
||||
|
||||
@@ -100,10 +100,7 @@ class MsgpackProtocol:
|
||||
elif issubclass(typ, datetime.timedelta):
|
||||
return int(v.total_seconds() * 10_000_000)
|
||||
elif isinstance(v, dict):
|
||||
return {
|
||||
cls.serialize_msgpack(k): cls.serialize_msgpack(value)
|
||||
for k, value in v.items()
|
||||
}
|
||||
return {cls.serialize_msgpack(k): cls.serialize_msgpack(value) for k, value in v.items()}
|
||||
elif issubclass(typ, Enum):
|
||||
list_ = list(typ)
|
||||
return list_.index(v) if v in list_ else v.value
|
||||
@@ -113,9 +110,7 @@ class MsgpackProtocol:
|
||||
def serialize_to_list(cls, value: BaseModel) -> list[Any]:
|
||||
values = []
|
||||
for field, info in value.__class__.model_fields.items():
|
||||
metadata = next(
|
||||
(m for m in info.metadata if isinstance(m, SignalRMeta)), None
|
||||
)
|
||||
metadata = next((m for m in info.metadata if isinstance(m, SignalRMeta)), None)
|
||||
if metadata and metadata.member_ignore:
|
||||
continue
|
||||
values.append(cls.serialize_msgpack(v=getattr(value, field)))
|
||||
@@ -130,9 +125,7 @@ class MsgpackProtocol:
|
||||
d = {}
|
||||
i = 0
|
||||
for field, info in typ.model_fields.items():
|
||||
metadata = next(
|
||||
(m for m in info.metadata if isinstance(m, SignalRMeta)), None
|
||||
)
|
||||
metadata = next((m for m in info.metadata if isinstance(m, SignalRMeta)), None)
|
||||
if metadata and metadata.member_ignore:
|
||||
continue
|
||||
anno = info.annotation
|
||||
@@ -224,10 +217,7 @@ class MsgpackProtocol:
|
||||
return list_[v] if isinstance(v, int) and 0 <= v < len(list_) else typ(v)
|
||||
elif get_origin(typ) is dict:
|
||||
return {
|
||||
cls.validate_object(k, get_args(typ)[0]): cls.validate_object(
|
||||
v, get_args(typ)[1]
|
||||
)
|
||||
for k, v in v.items()
|
||||
cls.validate_object(k, get_args(typ)[0]): cls.validate_object(v, get_args(typ)[1]) for k, v in v.items()
|
||||
}
|
||||
elif (origin := get_origin(typ)) is Union or origin is UnionType:
|
||||
args = get_args(typ)
|
||||
@@ -242,13 +232,8 @@ class MsgpackProtocol:
|
||||
# except `X (Other Type) | None`
|
||||
if NoneType in args and v is None:
|
||||
return None
|
||||
if not all(
|
||||
issubclass(arg, SignalRUnionMessage) or arg is NoneType for arg in args
|
||||
):
|
||||
raise ValueError(
|
||||
f"Cannot validate {v} to {typ}, "
|
||||
"only SignalRUnionMessage subclasses are supported"
|
||||
)
|
||||
if not all(issubclass(arg, SignalRUnionMessage) or arg is NoneType for arg in args):
|
||||
raise ValueError(f"Cannot validate {v} to {typ}, only SignalRUnionMessage subclasses are supported")
|
||||
union_type = v[0]
|
||||
for arg in args:
|
||||
assert issubclass(arg, SignalRUnionMessage)
|
||||
@@ -267,9 +252,7 @@ class MsgpackProtocol:
|
||||
]
|
||||
)
|
||||
if packet.arguments is not None:
|
||||
payload.append(
|
||||
[MsgpackProtocol.serialize_msgpack(arg) for arg in packet.arguments]
|
||||
)
|
||||
payload.append([MsgpackProtocol.serialize_msgpack(arg) for arg in packet.arguments])
|
||||
if packet.stream_ids is not None:
|
||||
payload.append(packet.stream_ids)
|
||||
elif isinstance(packet, CompletionPacket):
|
||||
@@ -282,9 +265,7 @@ class MsgpackProtocol:
|
||||
[
|
||||
packet.invocation_id,
|
||||
result_kind,
|
||||
packet.error
|
||||
or MsgpackProtocol.serialize_msgpack(packet.result)
|
||||
or None,
|
||||
packet.error or MsgpackProtocol.serialize_msgpack(packet.result) or None,
|
||||
]
|
||||
)
|
||||
elif isinstance(packet, ClosePacket):
|
||||
@@ -307,10 +288,7 @@ class JSONProtocol:
|
||||
if issubclass(typ, BaseModel):
|
||||
return cls.serialize_model(v, in_union)
|
||||
elif isinstance(v, dict):
|
||||
return {
|
||||
cls.serialize_to_json(k, True): cls.serialize_to_json(value)
|
||||
for k, value in v.items()
|
||||
}
|
||||
return {cls.serialize_to_json(k, True): cls.serialize_to_json(value) for k, value in v.items()}
|
||||
elif isinstance(v, list):
|
||||
return [cls.serialize_to_json(item) for item in v]
|
||||
elif isinstance(v, datetime.datetime):
|
||||
@@ -333,9 +311,7 @@ class JSONProtocol:
|
||||
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
|
||||
)
|
||||
metadata = next((m for m in info.metadata if isinstance(m, SignalRMeta)), None)
|
||||
if metadata and metadata.json_ignore:
|
||||
continue
|
||||
name = (
|
||||
@@ -358,14 +334,10 @@ class JSONProtocol:
|
||||
return d
|
||||
|
||||
@staticmethod
|
||||
def process_object(
|
||||
v: Any, typ: type[BaseModel], from_union: bool = False
|
||||
) -> dict[str, Any]:
|
||||
def process_object(v: Any, typ: type[BaseModel], from_union: bool = False) -> dict[str, Any]:
|
||||
d = {}
|
||||
for field, info in typ.model_fields.items():
|
||||
metadata = next(
|
||||
(m for m in info.metadata if isinstance(m, SignalRMeta)), None
|
||||
)
|
||||
metadata = next((m for m in info.metadata if isinstance(m, SignalRMeta)), None)
|
||||
if metadata and metadata.json_ignore:
|
||||
continue
|
||||
name = (
|
||||
@@ -435,9 +407,7 @@ class JSONProtocol:
|
||||
# d.hh:mm:ss
|
||||
parts = v.split(":")
|
||||
if len(parts) == 3:
|
||||
return datetime.timedelta(
|
||||
hours=int(parts[0]), minutes=int(parts[1]), seconds=int(parts[2])
|
||||
)
|
||||
return datetime.timedelta(hours=int(parts[0]), minutes=int(parts[1]), seconds=int(parts[2]))
|
||||
elif len(parts) == 2:
|
||||
return datetime.timedelta(minutes=int(parts[0]), seconds=int(parts[1]))
|
||||
elif len(parts) == 1:
|
||||
@@ -449,10 +419,7 @@ class JSONProtocol:
|
||||
return list_[v] if isinstance(v, int) and 0 <= v < len(list_) else typ(v)
|
||||
elif get_origin(typ) is dict:
|
||||
return {
|
||||
cls.validate_object(k, get_args(typ)[0]): cls.validate_object(
|
||||
v, get_args(typ)[1]
|
||||
)
|
||||
for k, v in v.items()
|
||||
cls.validate_object(k, get_args(typ)[0]): cls.validate_object(v, get_args(typ)[1]) for k, v in v.items()
|
||||
}
|
||||
elif (origin := get_origin(typ)) is Union or origin is UnionType:
|
||||
args = get_args(typ)
|
||||
@@ -467,13 +434,8 @@ class JSONProtocol:
|
||||
# except `X (Other Type) | None`
|
||||
if NoneType in args and v is None:
|
||||
return None
|
||||
if not all(
|
||||
issubclass(arg, SignalRUnionMessage) or arg is NoneType for arg in args
|
||||
):
|
||||
raise ValueError(
|
||||
f"Cannot validate {v} to {typ}, "
|
||||
"only SignalRUnionMessage subclasses are supported"
|
||||
)
|
||||
if not all(issubclass(arg, SignalRUnionMessage) or arg is NoneType for arg in args):
|
||||
raise ValueError(f"Cannot validate {v} to {typ}, only SignalRUnionMessage subclasses are supported")
|
||||
# https://github.com/ppy/osu/blob/98acd9/osu.Game/Online/SignalRDerivedTypeWorkaroundJsonConverter.cs
|
||||
union_type = v["$dtype"]
|
||||
for arg in args:
|
||||
@@ -498,9 +460,7 @@ class JSONProtocol:
|
||||
if packet.invocation_id is not None:
|
||||
payload["invocationId"] = packet.invocation_id
|
||||
if packet.arguments is not None:
|
||||
payload["arguments"] = [
|
||||
JSONProtocol.serialize_to_json(arg) for arg in packet.arguments
|
||||
]
|
||||
payload["arguments"] = [JSONProtocol.serialize_to_json(arg) for arg in packet.arguments]
|
||||
if packet.stream_ids is not None:
|
||||
payload["streamIds"] = packet.stream_ids
|
||||
elif isinstance(packet, CompletionPacket):
|
||||
|
||||
@@ -56,11 +56,9 @@ async def connect(
|
||||
return
|
||||
try:
|
||||
async for session in factory():
|
||||
if (
|
||||
user := await get_current_user(
|
||||
session, SecurityScopes(scopes=["*"]), token_pw=token
|
||||
)
|
||||
) is None or str(user.id) != user_id:
|
||||
if (user := await get_current_user(session, SecurityScopes(scopes=["*"]), token_pw=token)) is None or str(
|
||||
user.id
|
||||
) != user_id:
|
||||
await websocket.close(code=1008)
|
||||
return
|
||||
except HTTPException:
|
||||
|
||||
@@ -19,9 +19,7 @@ class ResultStore:
|
||||
self._seq = (self._seq + 1) % sys.maxsize
|
||||
return str(s)
|
||||
|
||||
def add_result(
|
||||
self, invocation_id: str, result: Any, error: str | None = None
|
||||
) -> None:
|
||||
def add_result(self, invocation_id: str, result: Any, error: str | None = None) -> None:
|
||||
if isinstance(invocation_id, str) and invocation_id.isdecimal():
|
||||
if future := self._futures.get(invocation_id):
|
||||
future.set_result((result, error))
|
||||
|
||||
@@ -13,9 +13,7 @@ if sys.version_info < (3, 12, 4):
|
||||
else:
|
||||
|
||||
def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
|
||||
return cast(Any, type_)._evaluate(
|
||||
globalns, localns, type_params=(), recursive_guard=set()
|
||||
)
|
||||
return cast(Any, type_)._evaluate(globalns, localns, type_params=(), recursive_guard=set())
|
||||
|
||||
|
||||
def get_annotation(param: inspect.Parameter, globalns: dict[str, Any]) -> Any:
|
||||
|
||||
Reference in New Issue
Block a user