Files
g0v0-server/app/signalr/hub/spectator_buffer.py
2025-08-22 14:58:13 +08:00

340 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
观战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()