整理代码
This commit is contained in:
@@ -92,11 +92,12 @@ class MetadataHub(Hub[MetadataClientState]):
|
||||
@override
|
||||
async def _clean_state(self, state: MetadataClientState) -> None:
|
||||
user_id = int(state.connection_id)
|
||||
|
||||
|
||||
# Remove from online user tracking
|
||||
from app.router.v2.stats import remove_online_user
|
||||
|
||||
asyncio.create_task(remove_online_user(user_id))
|
||||
|
||||
|
||||
if state.pushable:
|
||||
await asyncio.gather(*self.broadcast_tasks(user_id, None))
|
||||
redis = get_redis()
|
||||
@@ -125,6 +126,7 @@ class MetadataHub(Hub[MetadataClientState]):
|
||||
|
||||
# Track online user
|
||||
from app.router.v2.stats import add_online_user
|
||||
|
||||
asyncio.create_task(add_online_user(user_id))
|
||||
|
||||
async with with_db() as session:
|
||||
|
||||
@@ -163,11 +163,12 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
@override
|
||||
async def _clean_state(self, state: MultiplayerClientState):
|
||||
user_id = int(state.connection_id)
|
||||
|
||||
|
||||
# Remove from online user tracking
|
||||
from app.router.v2.stats import remove_online_user
|
||||
|
||||
asyncio.create_task(remove_online_user(user_id))
|
||||
|
||||
|
||||
if state.room_id != 0 and state.room_id in self.rooms:
|
||||
server_room = self.rooms[state.room_id]
|
||||
room = server_room.room
|
||||
@@ -180,9 +181,10 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
async def on_client_connect(self, client: Client) -> None:
|
||||
"""Track online users when connecting to multiplayer hub"""
|
||||
logger.info(f"[MultiplayerHub] Client {client.user_id} connected")
|
||||
|
||||
|
||||
# Track online user
|
||||
from app.router.v2.stats import add_online_user
|
||||
|
||||
asyncio.create_task(add_online_user(client.user_id))
|
||||
|
||||
def _ensure_in_room(self, client: Client) -> ServerMultiplayerRoom:
|
||||
@@ -292,11 +294,11 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
room.users.append(user)
|
||||
self.add_to_group(client, self.group_id(room_id))
|
||||
await server_room.match_type_handler.handle_join(user)
|
||||
|
||||
|
||||
# Critical fix: Send current room and gameplay state to new user
|
||||
# This ensures spectators joining ongoing games get proper state sync
|
||||
await self._send_room_state_to_new_user(client, server_room)
|
||||
|
||||
|
||||
await self.event_logger.player_joined(room_id, user.user_id)
|
||||
|
||||
async with with_db() as session:
|
||||
@@ -669,16 +671,22 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
# Enhanced spectator validation - allow transitions from more states
|
||||
# This matches official osu-server-spectator behavior
|
||||
if old not in (
|
||||
MultiplayerUserState.IDLE,
|
||||
MultiplayerUserState.IDLE,
|
||||
MultiplayerUserState.READY,
|
||||
MultiplayerUserState.RESULTS, # Allow spectating after results
|
||||
):
|
||||
# Allow spectating during gameplay states only if the room is in appropriate state
|
||||
if not (old.is_playing and room.room.state in (
|
||||
MultiplayerRoomState.WAITING_FOR_LOAD,
|
||||
MultiplayerRoomState.PLAYING
|
||||
)):
|
||||
raise InvokeException(f"Cannot change state from {old} to {new}")
|
||||
if not (
|
||||
old.is_playing
|
||||
and room.room.state
|
||||
in (
|
||||
MultiplayerRoomState.WAITING_FOR_LOAD,
|
||||
MultiplayerRoomState.PLAYING,
|
||||
)
|
||||
):
|
||||
raise InvokeException(
|
||||
f"Cannot change state from {old} to {new}"
|
||||
)
|
||||
case _:
|
||||
raise InvokeException(f"Invalid state transition from {old} to {new}")
|
||||
|
||||
@@ -691,7 +699,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
|
||||
if user.state == state:
|
||||
return
|
||||
|
||||
|
||||
# Special handling for state changes during gameplay
|
||||
match state:
|
||||
case MultiplayerUserState.IDLE:
|
||||
@@ -704,15 +712,15 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
logger.info(
|
||||
f"[MultiplayerHub] User {user.user_id} changing state from {user.state} to {state}"
|
||||
)
|
||||
|
||||
|
||||
await self.validate_user_stare(
|
||||
server_room,
|
||||
user.state,
|
||||
state,
|
||||
)
|
||||
|
||||
|
||||
await self.change_user_state(server_room, user, state)
|
||||
|
||||
|
||||
# Enhanced spectator handling based on official implementation
|
||||
if state == MultiplayerUserState.SPECTATING:
|
||||
await self.handle_spectator_state_change(client, server_room, user)
|
||||
@@ -738,24 +746,21 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
)
|
||||
|
||||
async def handle_spectator_state_change(
|
||||
self,
|
||||
client: Client,
|
||||
room: ServerMultiplayerRoom,
|
||||
user: MultiplayerRoomUser
|
||||
self, client: Client, room: ServerMultiplayerRoom, user: MultiplayerRoomUser
|
||||
):
|
||||
"""
|
||||
Handle special logic for users entering spectator mode during ongoing gameplay.
|
||||
Based on official osu-server-spectator implementation.
|
||||
"""
|
||||
room_state = room.room.state
|
||||
|
||||
|
||||
# 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"
|
||||
)
|
||||
await self.call_noblock(client, "LoadRequested")
|
||||
|
||||
|
||||
elif room_state == MultiplayerRoomState.PLAYING:
|
||||
logger.info(
|
||||
f"[MultiplayerHub] Spectator {user.user_id} joining during active gameplay"
|
||||
@@ -763,9 +768,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
await self.call_noblock(client, "LoadRequested")
|
||||
|
||||
async def _send_current_gameplay_state_to_spectator(
|
||||
self,
|
||||
client: Client,
|
||||
room: ServerMultiplayerRoom
|
||||
self, client: Client, room: ServerMultiplayerRoom
|
||||
):
|
||||
"""
|
||||
Send current gameplay state information to a newly joined spectator.
|
||||
@@ -773,12 +776,8 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
"""
|
||||
try:
|
||||
# Send current room state
|
||||
await self.call_noblock(
|
||||
client,
|
||||
"RoomStateChanged",
|
||||
room.room.state
|
||||
)
|
||||
|
||||
await self.call_noblock(client, "RoomStateChanged", room.room.state)
|
||||
|
||||
# Send current user states for all players
|
||||
for room_user in room.room.users:
|
||||
if room_user.state.is_playing:
|
||||
@@ -788,7 +787,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
room_user.user_id,
|
||||
room_user.state,
|
||||
)
|
||||
|
||||
|
||||
logger.debug(
|
||||
f"[MultiplayerHub] Sent current gameplay state to spectator {client.user_id}"
|
||||
)
|
||||
@@ -798,9 +797,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
)
|
||||
|
||||
async def _send_room_state_to_new_user(
|
||||
self,
|
||||
client: Client,
|
||||
room: ServerMultiplayerRoom
|
||||
self, client: Client, room: ServerMultiplayerRoom
|
||||
):
|
||||
"""
|
||||
Send complete room state to a newly joined user.
|
||||
@@ -809,23 +806,19 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
try:
|
||||
# Send current room state
|
||||
if room.room.state != MultiplayerRoomState.OPEN:
|
||||
await self.call_noblock(
|
||||
client,
|
||||
"RoomStateChanged",
|
||||
room.room.state
|
||||
)
|
||||
|
||||
await self.call_noblock(client, "RoomStateChanged", room.room.state)
|
||||
|
||||
# If room is in gameplay state, send LoadRequested immediately
|
||||
if room.room.state in (
|
||||
MultiplayerRoomState.WAITING_FOR_LOAD,
|
||||
MultiplayerRoomState.PLAYING
|
||||
MultiplayerRoomState.PLAYING,
|
||||
):
|
||||
logger.info(
|
||||
f"[MultiplayerHub] Sending LoadRequested to user {client.user_id} "
|
||||
f"joining ongoing game (room state: {room.room.state})"
|
||||
)
|
||||
await self.call_noblock(client, "LoadRequested")
|
||||
|
||||
|
||||
# Send all user states to help with synchronization
|
||||
for room_user in room.room.users:
|
||||
if room_user.user_id != client.user_id: # Don't send own state
|
||||
@@ -835,11 +828,11 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
room_user.user_id,
|
||||
room_user.state,
|
||||
)
|
||||
|
||||
|
||||
# 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}"
|
||||
)
|
||||
@@ -849,9 +842,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
)
|
||||
|
||||
async def _sync_with_spectator_hub(
|
||||
self,
|
||||
client: Client,
|
||||
room: ServerMultiplayerRoom
|
||||
self, client: Client, room: ServerMultiplayerRoom
|
||||
):
|
||||
"""
|
||||
Sync with SpectatorHub to ensure cross-hub spectating works properly.
|
||||
@@ -860,7 +851,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
try:
|
||||
# Import here to avoid circular imports
|
||||
from app.signalr.hub import SpectatorHubs
|
||||
|
||||
|
||||
# For each playing user in the room, check if they have SpectatorHub state
|
||||
# and notify the new client about their playing status
|
||||
for room_user in room.room.users:
|
||||
@@ -878,7 +869,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
f"[MultiplayerHub] Synced spectator state for user {room_user.user_id} "
|
||||
f"to new client {client.user_id}"
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"[MultiplayerHub] Failed to sync with SpectatorHub: {e}")
|
||||
# This is not critical, so we don't raise the exception
|
||||
|
||||
@@ -170,21 +170,22 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
Properly notifies watched users when spectator disconnects.
|
||||
"""
|
||||
user_id = int(state.connection_id)
|
||||
|
||||
|
||||
# Remove from online and playing tracking
|
||||
from app.router.v2.stats import remove_online_user
|
||||
|
||||
asyncio.create_task(remove_online_user(user_id))
|
||||
|
||||
|
||||
if state.state:
|
||||
await self._end_session(user_id, state.state, state)
|
||||
|
||||
|
||||
# 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:
|
||||
await self.call_noblock(
|
||||
target_client, "UserEndedWatching", user_id
|
||||
)
|
||||
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"
|
||||
)
|
||||
@@ -195,18 +196,19 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
Send all active player states to newly connected clients.
|
||||
"""
|
||||
logger.info(f"[SpectatorHub] Client {client.user_id} connected")
|
||||
|
||||
|
||||
# Track online user
|
||||
from app.router.v2.stats import add_online_user
|
||||
|
||||
asyncio.create_task(add_online_user(client.user_id))
|
||||
|
||||
|
||||
# Send all current player states to the new client
|
||||
# This matches the official OnConnectedAsync behavior
|
||||
active_states = []
|
||||
for user_id, store in self.state.items():
|
||||
if store.state is not None:
|
||||
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}"
|
||||
@@ -216,8 +218,10 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
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)
|
||||
|
||||
@@ -229,14 +233,15 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
try:
|
||||
# Import here to avoid circular imports
|
||||
from app.signalr.hub import MultiplayerHubs
|
||||
|
||||
|
||||
# Check all active multiplayer rooms for playing users
|
||||
for room_id, server_room in MultiplayerHubs.rooms.items():
|
||||
for room_user in server_room.room.users:
|
||||
# If user is playing in multiplayer but we don't have their spectator state
|
||||
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:
|
||||
@@ -245,9 +250,9 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
ruleset_id=room_user.ruleset_id or 0, # Default to osu!
|
||||
mods=room_user.mods,
|
||||
state=SpectatedUserState.Playing,
|
||||
maximum_statistics={}
|
||||
maximum_statistics={},
|
||||
)
|
||||
|
||||
|
||||
await self.call_noblock(
|
||||
client,
|
||||
"UserBeganPlaying",
|
||||
@@ -258,8 +263,10 @@ 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}"
|
||||
)
|
||||
|
||||
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
|
||||
@@ -306,6 +313,7 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
|
||||
# Track playing user
|
||||
from app.router.v2.stats import add_playing_user
|
||||
|
||||
asyncio.create_task(add_playing_user(user_id))
|
||||
|
||||
# # 预缓存beatmap文件以加速后续PP计算
|
||||
@@ -356,11 +364,12 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
) and any(k.is_hit() and v > 0 for k, v in score.score_info.statistics.items()):
|
||||
await self._process_score(store, client)
|
||||
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))
|
||||
|
||||
|
||||
store.state = None
|
||||
store.beatmap_status = None
|
||||
store.checksum = None
|
||||
@@ -473,8 +482,10 @@ 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
|
||||
if store.score and store.score.replay_frames:
|
||||
@@ -491,7 +502,7 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
)
|
||||
self.tasks.add(task)
|
||||
task.add_done_callback(self.tasks.discard)
|
||||
|
||||
|
||||
# Background task for failtime tracking - only for failed/quit states with valid data
|
||||
if (
|
||||
state.beatmap_id is not None
|
||||
@@ -519,14 +530,16 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
Properly handles state synchronization and watcher notifications.
|
||||
"""
|
||||
user_id = int(client.connection_id)
|
||||
|
||||
|
||||
logger.info(f"[SpectatorHub] {user_id} started watching {target_id}")
|
||||
|
||||
|
||||
try:
|
||||
# Get target user's current state if it exists
|
||||
target_store = self.state.get(target_id)
|
||||
if target_store and target_store.state:
|
||||
logger.debug(f"[SpectatorHub] {target_id} is currently {target_store.state.state}")
|
||||
logger.debug(
|
||||
f"[SpectatorHub] {target_id} is currently {target_store.state.state}"
|
||||
)
|
||||
# Send current state to the watcher immediately
|
||||
await self.call_noblock(
|
||||
client,
|
||||
@@ -552,7 +565,9 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
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
|
||||
@@ -562,7 +577,9 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
await self.call_noblock(
|
||||
target_client, "UserStartedWatching", watcher_info
|
||||
)
|
||||
logger.debug(f"[SpectatorHub] Notified {target_id} that {username} started watching")
|
||||
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}")
|
||||
|
||||
@@ -572,19 +589,23 @@ class SpectatorHub(Hub[StoreClientState]):
|
||||
Properly cleans up watcher state and notifies target user.
|
||||
"""
|
||||
user_id = int(client.connection_id)
|
||||
|
||||
|
||||
logger.info(f"[SpectatorHub] {user_id} ended watching {target_id}")
|
||||
|
||||
|
||||
# Remove from SignalR group
|
||||
self.remove_from_group(client, self.group_id(target_id))
|
||||
|
||||
|
||||
# Remove from our tracked watched users
|
||||
store = self.get_or_create_state(client)
|
||||
store.watched_user.discard(target_id)
|
||||
|
||||
|
||||
# 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"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user