回滚代码
This commit is contained in:
339
app/signalr/hub/spectator_buffer.py
Normal file
339
app/signalr/hub/spectator_buffer.py
Normal file
@@ -0,0 +1,339 @@
|
||||
"""
|
||||
观战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()
|
||||
Reference in New Issue
Block a user