rollback code

This commit is contained in:
咕谷酱
2025-08-22 15:07:50 +08:00
parent b1f0cbfed1
commit e293d7541b
12 changed files with 74 additions and 1710 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,221 +0,0 @@
"""
多人游戏数据包清理管理器
基于osu-server源码实现的数据包清理逻辑
"""
from __future__ import annotations
import asyncio
from datetime import UTC, datetime, timedelta
from typing import Dict, List, Optional, Set
from collections import defaultdict
import logging
logger = logging.getLogger(__name__)
class MultiplayerPacketCleaner:
"""多人游戏数据包清理管理器基于osu源码设计"""
def __init__(self):
# 待清理的数据包队列
self.cleanup_queue: Dict[int, List[Dict]] = defaultdict(list)
# 清理任务映射
self.cleanup_tasks: Dict[int, asyncio.Task] = {}
# 延迟清理时间(秒)
self.cleanup_delay = 5.0
# 强制清理时间(秒)
self.force_cleanup_delay = 30.0
async def schedule_cleanup(self, room_id: int, packet_data: Dict):
"""安排数据包清理参考osu源码的清理调度"""
self.cleanup_queue[room_id].append({
**packet_data,
'scheduled_at': datetime.now(UTC),
'room_id': room_id
})
# 如果没有正在进行的清理任务,开始新的清理任务
if room_id not in self.cleanup_tasks or self.cleanup_tasks[room_id].done():
self.cleanup_tasks[room_id] = asyncio.create_task(
self._delayed_cleanup_task(room_id)
)
logger.debug(f"[PacketCleaner] Scheduled cleanup task for room {room_id}")
async def _delayed_cleanup_task(self, room_id: int):
"""延迟清理任务类似osu源码的延迟清理机制"""
try:
# 等待延迟时间
await asyncio.sleep(self.cleanup_delay)
# 执行清理
await self._execute_cleanup(room_id)
except asyncio.CancelledError:
logger.debug(f"[PacketCleaner] Cleanup task for room {room_id} was cancelled")
raise
except Exception as e:
logger.error(f"[PacketCleaner] Error during cleanup for room {room_id}: {e}")
async def _execute_cleanup(self, room_id: int):
"""执行实际的清理操作"""
if room_id not in self.cleanup_queue:
return
packets_to_clean = self.cleanup_queue.pop(room_id, [])
if not packets_to_clean:
return
logger.info(f"[PacketCleaner] Cleaning {len(packets_to_clean)} packets for room {room_id}")
# 按类型分组处理清理
score_packets = []
state_packets = []
leaderboard_packets = []
for packet in packets_to_clean:
packet_type = packet.get('type', 'unknown')
if packet_type == 'score':
score_packets.append(packet)
elif packet_type == 'state':
state_packets.append(packet)
elif packet_type == 'leaderboard':
leaderboard_packets.append(packet)
# 清理分数数据包
if score_packets:
await self._cleanup_score_packets(room_id, score_packets)
# 清理状态数据包
if state_packets:
await self._cleanup_state_packets(room_id, state_packets)
# 清理排行榜数据包
if leaderboard_packets:
await self._cleanup_leaderboard_packets(room_id, leaderboard_packets)
async def _cleanup_score_packets(self, room_id: int, packets: List[Dict]):
"""清理分数相关数据包"""
user_ids = set(p.get('user_id') for p in packets if p.get('user_id'))
logger.debug(f"[PacketCleaner] Cleaning score packets for {len(user_ids)} users in room {room_id}")
# 这里可以添加具体的清理逻辑,比如:
# - 清理过期的分数帧
# - 压缩历史分数数据
# - 清理缓存
async def _cleanup_state_packets(self, room_id: int, packets: List[Dict]):
"""清理状态相关数据包"""
logger.debug(f"[PacketCleaner] Cleaning {len(packets)} state packets for room {room_id}")
# 这里可以添加状态清理逻辑,比如:
# - 清理过期状态数据
# - 重置用户状态缓存
async def _cleanup_leaderboard_packets(self, room_id: int, packets: List[Dict]):
"""清理排行榜相关数据包"""
logger.debug(f"[PacketCleaner] Cleaning {len(packets)} leaderboard packets for room {room_id}")
# 这里可以添加排行榜清理逻辑
async def force_cleanup(self, room_id: int):
"""强制立即清理指定房间的数据包"""
# 取消延迟清理任务
if room_id in self.cleanup_tasks:
task = self.cleanup_tasks.pop(room_id)
if not task.done():
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
# 立即执行清理
await self._execute_cleanup(room_id)
logger.info(f"[PacketCleaner] Force cleaned packets for room {room_id}")
async def cleanup_all_for_room(self, room_id: int):
"""清理房间的所有数据包(房间结束时调用)"""
await self.force_cleanup(room_id)
# 清理任务引用
self.cleanup_tasks.pop(room_id, None)
self.cleanup_queue.pop(room_id, None)
logger.info(f"[PacketCleaner] Completed full cleanup for room {room_id}")
async def cleanup_expired_packets(self):
"""定期清理过期的数据包"""
current_time = datetime.now(UTC)
expired_rooms = []
for room_id, packets in self.cleanup_queue.items():
# 查找超过强制清理时间的数据包
expired_packets = [
p for p in packets
if (current_time - p['scheduled_at']).total_seconds() > self.force_cleanup_delay
]
if expired_packets:
expired_rooms.append(room_id)
# 强制清理过期数据包
for room_id in expired_rooms:
await self.force_cleanup(room_id)
def get_cleanup_stats(self) -> Dict:
"""获取清理统计信息"""
return {
'active_cleanup_tasks': len([t for t in self.cleanup_tasks.values() if not t.done()]),
'pending_packets': sum(len(packets) for packets in self.cleanup_queue.values()),
'rooms_with_pending_cleanup': len(self.cleanup_queue),
}
# 全局实例
packet_cleaner = MultiplayerPacketCleaner()
class GameSessionCleaner:
"""游戏会话清理器参考osu源码的会话管理"""
@staticmethod
async def cleanup_game_session(room_id: int, game_completed: bool = False):
"""清理游戏会话数据(每局游戏结束后调用)"""
try:
# 安排数据包清理
await packet_cleaner.schedule_cleanup(room_id, {
'type': 'game_session_end',
'completed': game_completed,
'timestamp': datetime.now(UTC).isoformat()
})
logger.info(f"[GameSessionCleaner] Scheduled cleanup for game session in room {room_id} (completed: {game_completed})")
except Exception as e:
logger.error(f"[GameSessionCleaner] Failed to cleanup game session for room {room_id}: {e}")
@staticmethod
async def cleanup_user_session(room_id: int, user_id: int):
"""清理单个用户的会话数据"""
try:
await packet_cleaner.schedule_cleanup(room_id, {
'type': 'user_session_end',
'user_id': user_id,
'timestamp': datetime.now(UTC).isoformat()
})
logger.debug(f"[GameSessionCleaner] Scheduled cleanup for user {user_id} in room {room_id}")
except Exception as e:
logger.error(f"[GameSessionCleaner] Failed to cleanup user session {user_id} in room {room_id}: {e}")
@staticmethod
async def cleanup_room_fully(room_id: int):
"""完全清理房间数据(房间关闭时调用)"""
try:
await packet_cleaner.cleanup_all_for_room(room_id)
logger.info(f"[GameSessionCleaner] Completed full room cleanup for {room_id}")
except Exception as e:
logger.error(f"[GameSessionCleaner] Failed to fully cleanup room {room_id}: {e}")

View File

@@ -34,7 +34,6 @@ from app.models.spectator_hub import (
from app.utils import unix_timestamp_to_windows
from .hub import Client, Hub
from .spectator_buffer import spectator_state_manager
from httpx import HTTPError
from sqlalchemy.orm import joinedload
@@ -204,22 +203,8 @@ class SpectatorHub(Hub[StoreClientState]):
# Send all current player states to the new client
# This matches the official OnConnectedAsync behavior
active_states = []
# 首先从缓冲区获取状态
buffer_stats = spectator_state_manager.get_buffer_stats()
if buffer_stats['active_users'] > 0:
logger.debug(f"[SpectatorHub] Found {buffer_stats['active_users']} users in buffer")
# 获取缓冲区中的所有活跃用户
active_users = spectator_state_manager.buffer.get_all_active_users()
for user_id in active_users:
state = spectator_state_manager.buffer.get_user_state(user_id)
if state and state.state == SpectatedUserState.Playing:
active_states.append((user_id, state))
# 然后从本地状态获取
for user_id, store in self.state.items():
if store.state is not None and user_id not in [state[0] for state in active_states]:
if store.state is not None:
active_states.append((user_id, store.state))
if active_states:
@@ -256,41 +241,24 @@ class SpectatorHub(Hub[StoreClientState]):
and room_user.user_id not in self.state
):
# Create a synthetic SpectatorState for multiplayer players
# 关键修复:处理多人游戏中不同用户可能选择不同谱面的情况
# This helps with cross-hub spectating
try:
# 获取用户选择的谱面ID如果是自由选择模式
user_beatmap_id = getattr(room_user, 'beatmap_id', None) or server_room.queue.current_item.beatmap_id
user_ruleset_id = room_user.ruleset_id or server_room.queue.current_item.ruleset_id or 0
user_mods = room_user.mods or []
synthetic_state = SpectatorState(
beatmap_id=user_beatmap_id,
ruleset_id=user_ruleset_id,
mods=user_mods,
beatmap_id=server_room.queue.current_item.beatmap_id,
ruleset_id=room_user.ruleset_id or 0, # Default to osu!
mods=room_user.mods,
state=SpectatedUserState.Playing,
maximum_statistics={},
)
# 同步到缓冲区管理器
multiplayer_data = {
'room_id': room_id,
'beatmap_id': user_beatmap_id,
'ruleset_id': user_ruleset_id,
'mods': user_mods,
'state': room_user.state,
'maximum_statistics': {},
'is_multiplayer': True
}
await spectator_state_manager.sync_with_multiplayer(room_user.user_id, multiplayer_data)
await self.call_noblock(
client,
"UserBeganPlaying",
room_user.user_id,
synthetic_state,
)
logger.info(
f"[SpectatorHub] Sent synthetic multiplayer state for user {room_user.user_id} (beatmap: {user_beatmap_id}, ruleset: {user_ruleset_id})"
logger.debug(
f"[SpectatorHub] Sent synthetic multiplayer state for user {room_user.user_id}"
)
except Exception as e:
logger.debug(
@@ -312,9 +280,6 @@ class SpectatorHub(Hub[StoreClientState]):
maximum_statistics={},
)
# 也同步结束状态到缓冲区
await spectator_state_manager.handle_user_finished_playing(room_user.user_id, finished_state)
await self.call_noblock(
client,
"UserFinishedPlaying",
@@ -386,15 +351,6 @@ class SpectatorHub(Hub[StoreClientState]):
# # 预缓存beatmap文件以加速后续PP计算
# await self._preload_beatmap_for_pp_calculation(state.beatmap_id)
# 更新缓冲区状态
session_data = {
'beatmap_checksum': store.checksum,
'score_token': score_token,
'username': name,
'started_at': time.time()
}
await spectator_state_manager.handle_user_began_playing(user_id, state, session_data)
await self.broadcast_group_call(
self.group_id(user_id),
"UserBeganPlaying",
@@ -421,9 +377,6 @@ class SpectatorHub(Hub[StoreClientState]):
score_info.statistics = header.statistics
store.score.replay_frames.extend(frame_data.frames)
# 更新缓冲区的帧数据
await spectator_state_manager.handle_frame_data(user_id, frame_data)
await self.broadcast_group_call(
self.group_id(user_id), "UserSentFrames", user_id, frame_data
)
@@ -605,9 +558,6 @@ class SpectatorHub(Hub[StoreClientState]):
self.tasks.add(task)
task.add_done_callback(self.tasks.discard)
# 通知缓冲区管理器用户结束游戏
await spectator_state_manager.handle_user_finished_playing(user_id, state)
logger.info(
f"[SpectatorHub] {user_id} finished playing {state.beatmap_id} "
f"with {state.state}"
@@ -628,54 +578,27 @@ class SpectatorHub(Hub[StoreClientState]):
logger.info(f"[SpectatorHub] {user_id} started watching {target_id}")
# 使用缓冲区管理器处理观战开始,获取追赶数据
catchup_bundle = await spectator_state_manager.handle_spectator_start_watching(user_id, target_id)
try:
# 首先尝试从缓冲区获取状态
buffered_state = spectator_state_manager.buffer.get_user_state(target_id)
if buffered_state and buffered_state.state == SpectatedUserState.Playing:
logger.info(
f"[SpectatorHub] Sending buffered state for {target_id} to spectator {user_id} "
f"(beatmap: {buffered_state.beatmap_id}, ruleset: {buffered_state.ruleset_id})"
)
await self.call_noblock(client, "UserBeganPlaying", target_id, buffered_state)
# 发送最近的帧数据以帮助同步
recent_frames = spectator_state_manager.buffer.get_recent_frames(target_id, 10)
for frame_data in recent_frames:
try:
await self.call_noblock(client, "UserSentFrames", target_id, frame_data)
except Exception as e:
logger.debug(f"[SpectatorHub] Failed to send frame data: {e}")
# 如果有追赶数据包,发送额外的同步信息
if catchup_bundle:
multiplayer_data = catchup_bundle.get('multiplayer_data')
if multiplayer_data and multiplayer_data.get('is_multiplayer'):
logger.info(
f"[SpectatorHub] Sending multiplayer sync data for {target_id} "
f"(room: {multiplayer_data.get('room_id')})"
)
else:
# 尝试从本地状态获取
target_store = self.state.get(target_id)
if target_store and target_store.state:
# 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 local state")
await self.call_noblock(client, "UserBeganPlaying", target_id, target_store.state)
else:
logger.debug(f"[SpectatorHub] {target_id} state is {target_store.state.state}, not sending to watcher")
# Get target user's current state if it exists
target_store = self.state.get(target_id)
if target_store and target_store.state:
# 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"
)
# Send current state to the watcher immediately
await self.call_noblock(
client,
"UserBeganPlaying",
target_id,
target_store.state,
)
else:
# 检查多人游戏同步缓存
multiplayer_data = spectator_state_manager.buffer.get_multiplayer_sync_data(target_id)
if multiplayer_data:
logger.debug(f"[SpectatorHub] Sending multiplayer sync data for {target_id}")
# 这里可以发送多人游戏的状态信息
logger.debug(
f"[SpectatorHub] {target_id} state is {target_store.state.state}, not sending to watcher"
)
except Exception as e:
# User isn't tracked or error occurred - this is not critical
logger.debug(f"[SpectatorHub] Could not get state for {target_id}: {e}")
@@ -721,9 +644,6 @@ class SpectatorHub(Hub[StoreClientState]):
logger.info(f"[SpectatorHub] {user_id} ended watching {target_id}")
# 使用缓冲区管理器处理观战结束
await spectator_state_manager.handle_spectator_stop_watching(user_id, target_id)
# Remove from SignalR group
self.remove_from_group(client, self.group_id(target_id))

View File

@@ -1,339 +0,0 @@
"""
观战Hub缓冲区管理器
解决第一局游戏结束后观战和排行榜不同步的问题
"""
from __future__ import annotations
import asyncio
from datetime import UTC, datetime, timedelta
from typing import Dict, List, Optional, Tuple, Set
from collections import defaultdict, deque
import logging
from app.models.spectator_hub import SpectatorState, FrameDataBundle, SpectatedUserState
from app.models.multiplayer_hub import MultiplayerUserState
logger = logging.getLogger(__name__)
class SpectatorBuffer:
"""观战数据缓冲区,解决观战状态不同步问题"""
def __init__(self):
# 用户ID -> 游戏状态缓存
self.user_states: Dict[int, SpectatorState] = {}
# 用户ID -> 帧数据缓冲区 (保留最近的帧数据)
self.frame_buffers: Dict[int, deque] = defaultdict(lambda: deque(maxlen=30))
# 用户ID -> 最后活跃时间
self.last_activity: Dict[int, datetime] = {}
# 用户ID -> 观战者列表
self.spectators: Dict[int, Set[int]] = defaultdict(set)
# 用户ID -> 游戏会话信息
self.session_info: Dict[int, Dict] = {}
# 多人游戏同步缓存
self.multiplayer_sync_cache: Dict[int, Dict] = {} # user_id -> multiplayer_data
# 缓冲区过期时间(分钟)
self.buffer_expire_time = 10
def update_user_state(self, user_id: int, state: SpectatorState, session_data: Optional[Dict] = None):
"""更新用户状态到缓冲区"""
self.user_states[user_id] = state
self.last_activity[user_id] = datetime.now(UTC)
if session_data:
self.session_info[user_id] = session_data
logger.debug(f"[SpectatorBuffer] Updated state for user {user_id}: {state.state}")
def add_frame_data(self, user_id: int, frame_data: FrameDataBundle):
"""添加帧数据到缓冲区"""
self.frame_buffers[user_id].append({
'data': frame_data,
'timestamp': datetime.now(UTC)
})
self.last_activity[user_id] = datetime.now(UTC)
def get_user_state(self, user_id: int) -> Optional[SpectatorState]:
"""获取用户当前状态"""
return self.user_states.get(user_id)
def get_recent_frames(self, user_id: int, count: int = 10) -> List[FrameDataBundle]:
"""获取用户最近的帧数据"""
frames = self.frame_buffers.get(user_id, deque())
recent_frames = list(frames)[-count:] if len(frames) >= count else list(frames)
return [frame['data'] for frame in recent_frames]
def add_spectator(self, user_id: int, spectator_id: int):
"""添加观战者"""
self.spectators[user_id].add(spectator_id)
logger.debug(f"[SpectatorBuffer] Added spectator {spectator_id} to user {user_id}")
def remove_spectator(self, user_id: int, spectator_id: int):
"""移除观战者"""
self.spectators[user_id].discard(spectator_id)
logger.debug(f"[SpectatorBuffer] Removed spectator {spectator_id} from user {user_id}")
def get_spectators(self, user_id: int) -> Set[int]:
"""获取用户的所有观战者"""
return self.spectators.get(user_id, set())
def clear_user_data(self, user_id: int):
"""清理用户数据(游戏结束时调用,但保留一段时间用于观战同步)"""
# 不立即删除,而是标记为已结束,延迟清理
if user_id in self.user_states:
current_state = self.user_states[user_id]
if current_state.state == SpectatedUserState.Playing:
# 将状态标记为已结束,但保留在缓冲区中
current_state.state = SpectatedUserState.Passed # 或其他结束状态
self.user_states[user_id] = current_state
logger.debug(f"[SpectatorBuffer] Marked user {user_id} as finished, keeping in buffer")
def cleanup_expired_data(self):
"""清理过期数据"""
current_time = datetime.now(UTC)
expired_users = []
for user_id, last_time in self.last_activity.items():
if (current_time - last_time).total_seconds() > self.buffer_expire_time * 60:
expired_users.append(user_id)
for user_id in expired_users:
self._force_clear_user(user_id)
logger.debug(f"[SpectatorBuffer] Cleaned expired data for user {user_id}")
def _force_clear_user(self, user_id: int):
"""强制清理用户数据"""
self.user_states.pop(user_id, None)
self.frame_buffers.pop(user_id, None)
self.last_activity.pop(user_id, None)
self.spectators.pop(user_id, None)
self.session_info.pop(user_id, None)
self.multiplayer_sync_cache.pop(user_id, None)
def sync_multiplayer_state(self, user_id: int, multiplayer_data: Dict):
"""同步多人游戏状态"""
self.multiplayer_sync_cache[user_id] = {
**multiplayer_data,
'synced_at': datetime.now(UTC)
}
logger.debug(f"[SpectatorBuffer] Synced multiplayer state for user {user_id}")
def get_multiplayer_sync_data(self, user_id: int) -> Optional[Dict]:
"""获取多人游戏同步数据"""
return self.multiplayer_sync_cache.get(user_id)
def has_active_spectators(self, user_id: int) -> bool:
"""检查用户是否有活跃的观战者"""
return len(self.spectators.get(user_id, set())) > 0
def get_all_active_users(self) -> List[int]:
"""获取所有活跃用户"""
current_time = datetime.now(UTC)
active_users = []
for user_id, last_time in self.last_activity.items():
if (current_time - last_time).total_seconds() < 300: # 5分钟内活跃
active_users.append(user_id)
return active_users
def create_catchup_bundle(self, user_id: int) -> Optional[Dict]:
"""为新观战者创建追赶数据包"""
if user_id not in self.user_states:
return None
state = self.user_states[user_id]
recent_frames = self.get_recent_frames(user_id, 20) # 获取最近20帧
session_data = self.session_info.get(user_id, {})
return {
'user_id': user_id,
'state': state,
'recent_frames': recent_frames,
'session_info': session_data,
'multiplayer_data': self.get_multiplayer_sync_data(user_id),
'created_at': datetime.now(UTC).isoformat()
}
def get_buffer_stats(self) -> Dict:
"""获取缓冲区统计信息"""
return {
'active_users': len(self.user_states),
'total_spectators': sum(len(specs) for specs in self.spectators.values()),
'buffered_frames': sum(len(frames) for frames in self.frame_buffers.values()),
'multiplayer_synced_users': len(self.multiplayer_sync_cache)
}
class SpectatorStateManager:
"""观战状态管理器,处理状态同步和缓冲"""
def __init__(self):
self.buffer = SpectatorBuffer()
self.cleanup_task: Optional[asyncio.Task] = None
self.start_cleanup_task()
def start_cleanup_task(self):
"""启动定期清理任务"""
if self.cleanup_task is None or self.cleanup_task.done():
self.cleanup_task = asyncio.create_task(self._periodic_cleanup())
async def _periodic_cleanup(self):
"""定期清理过期数据"""
while True:
try:
await asyncio.sleep(300) # 每5分钟清理一次
self.buffer.cleanup_expired_data()
stats = self.buffer.get_buffer_stats()
if stats['active_users'] > 0:
logger.debug(f"[SpectatorStateManager] Buffer stats: {stats}")
except Exception as e:
logger.error(f"[SpectatorStateManager] Error in periodic cleanup: {e}")
except asyncio.CancelledError:
logger.info("[SpectatorStateManager] Periodic cleanup task cancelled")
break
async def handle_user_began_playing(self, user_id: int, state: SpectatorState, session_data: Optional[Dict] = None):
"""处理用户开始游戏"""
self.buffer.update_user_state(user_id, state, session_data)
# 如果有观战者,发送追赶数据
spectators = self.buffer.get_spectators(user_id)
if spectators:
logger.debug(f"[SpectatorStateManager] User {user_id} has {len(spectators)} spectators, maintaining buffer")
async def handle_user_finished_playing(self, user_id: int, final_state: SpectatorState):
"""处理用户结束游戏"""
# 更新为结束状态,但保留在缓冲区中以便观战者同步
self.buffer.update_user_state(user_id, final_state)
# 如果有观战者,保持数据在缓冲区中更长时间
if self.buffer.has_active_spectators(user_id):
logger.debug(f"[SpectatorStateManager] User {user_id} finished, keeping data for spectators")
else:
# 延迟清理
asyncio.create_task(self._delayed_cleanup_user(user_id, 60)) # 60秒后清理
async def _delayed_cleanup_user(self, user_id: int, delay_seconds: int):
"""延迟清理用户数据"""
await asyncio.sleep(delay_seconds)
if not self.buffer.has_active_spectators(user_id):
self.buffer.clear_user_data(user_id)
logger.debug(f"[SpectatorStateManager] Delayed cleanup for user {user_id}")
async def handle_frame_data(self, user_id: int, frame_data: FrameDataBundle):
"""处理帧数据"""
self.buffer.add_frame_data(user_id, frame_data)
async def handle_spectator_start_watching(self, spectator_id: int, target_id: int) -> Optional[Dict]:
"""处理观战者开始观看,返回追赶数据包"""
self.buffer.add_spectator(target_id, spectator_id)
# 为新观战者创建追赶数据包
catchup_bundle = self.buffer.create_catchup_bundle(target_id)
if catchup_bundle:
logger.debug(f"[SpectatorStateManager] Created catchup bundle for spectator {spectator_id} watching {target_id}")
return catchup_bundle
async def handle_spectator_stop_watching(self, spectator_id: int, target_id: int):
"""处理观战者停止观看"""
self.buffer.remove_spectator(target_id, spectator_id)
async def sync_with_multiplayer(self, user_id: int, multiplayer_data: Dict):
"""与多人游戏模式同步"""
self.buffer.sync_multiplayer_state(user_id, multiplayer_data)
beatmap_id = multiplayer_data.get('beatmap_id')
ruleset_id = multiplayer_data.get('ruleset_id', 0)
logger.info(
f"[SpectatorStateManager] Syncing multiplayer data for user {user_id}: "
f"beatmap={beatmap_id}, ruleset={ruleset_id}"
)
# 如果用户没有在SpectatorHub中但在多人游戏中创建同步状态
if user_id not in self.buffer.user_states:
try:
synthetic_state = SpectatorState(
beatmap_id=beatmap_id,
ruleset_id=ruleset_id,
mods=multiplayer_data.get('mods', []),
state=self._convert_multiplayer_state(multiplayer_data.get('state')),
maximum_statistics=multiplayer_data.get('maximum_statistics', {}),
)
await self.handle_user_began_playing(user_id, synthetic_state, {
'source': 'multiplayer',
'room_id': multiplayer_data.get('room_id'),
'beatmap_id': beatmap_id,
'ruleset_id': ruleset_id,
'is_multiplayer': multiplayer_data.get('is_multiplayer', True),
'synced_at': datetime.now(UTC).isoformat()
})
logger.info(
f"[SpectatorStateManager] Created synthetic state for multiplayer user {user_id} "
f"(beatmap: {beatmap_id}, ruleset: {ruleset_id})"
)
except Exception as e:
logger.error(f"[SpectatorStateManager] Failed to create synthetic state for user {user_id}: {e}")
else:
# 更新现有状态
existing_state = self.buffer.user_states[user_id]
if existing_state.beatmap_id != beatmap_id or existing_state.ruleset_id != ruleset_id:
logger.info(
f"[SpectatorStateManager] Updating state for user {user_id}: "
f"beatmap {existing_state.beatmap_id} -> {beatmap_id}, "
f"ruleset {existing_state.ruleset_id} -> {ruleset_id}"
)
# 更新状态以匹配多人游戏
existing_state.beatmap_id = beatmap_id
existing_state.ruleset_id = ruleset_id
existing_state.mods = multiplayer_data.get('mods', [])
self.buffer.update_user_state(user_id, existing_state)
def _convert_multiplayer_state(self, mp_state) -> SpectatedUserState:
"""将多人游戏状态转换为观战状态"""
if not mp_state:
return SpectatedUserState.Playing
# 假设mp_state是MultiplayerUserState类型
if hasattr(mp_state, 'name'):
state_name = mp_state.name
if 'PLAYING' in state_name:
return SpectatedUserState.Playing
elif 'RESULTS' in state_name:
return SpectatedUserState.Passed
elif 'FAILED' in state_name:
return SpectatedUserState.Failed
elif 'QUIT' in state_name:
return SpectatedUserState.Quit
return SpectatedUserState.Playing # 默认状态
def get_buffer_stats(self) -> Dict:
"""获取缓冲区统计信息"""
return self.buffer.get_buffer_stats()
def stop_cleanup_task(self):
"""停止清理任务"""
if self.cleanup_task and not self.cleanup_task.done():
self.cleanup_task.cancel()
# 全局实例
spectator_state_manager = SpectatorStateManager()