回滚代码

This commit is contained in:
咕谷酱
2025-08-22 14:58:13 +08:00
parent 8c6f7aa0ef
commit b1f0cbfed1
7 changed files with 1114 additions and 290 deletions

View File

@@ -48,6 +48,7 @@ from app.models.room import (
from app.models.score import GameMode
from .hub import Client, Hub
from .multiplayer_packet_cleaner import packet_cleaner, GameSessionCleaner
from httpx import HTTPError
from sqlalchemy import update
@@ -201,6 +202,45 @@ class GameplayStateBuffer:
keys_to_remove = [key for key in self.spectator_states.keys() if key[0] == room_id]
for key in keys_to_remove:
self.spectator_states.pop(key, None)
async def cleanup_game_session(self, room_id: int):
"""清理单局游戏会话数据(每局游戏结束后调用)"""
# 清理分数缓冲区但保留房间结构
if room_id in self.score_buffers:
self.score_buffers[room_id].clear()
# 清理实时排行榜
self.leaderboards.pop(room_id, None)
# 清理游戏状态快照
self.gameplay_snapshots.pop(room_id, None)
# 清理观战者状态但不删除房间相关键
keys_to_remove = []
for key in self.spectator_states.keys():
if key[0] == room_id:
# 保留连接状态,清理游戏数据
state = self.spectator_states[key]
if 'game_data' in state:
state.pop('game_data', None)
if 'score_data' in state:
state.pop('score_data', None)
logger.info(f"[GameplayStateBuffer] Cleaned game session data for room {room_id}")
def reset_user_gameplay_state(self, room_id: int, user_id: int):
"""重置单个用户的游戏状态"""
# 清理用户分数缓冲区
if room_id in self.score_buffers and user_id in self.score_buffers[room_id]:
self.score_buffers[room_id][user_id].clear()
# 重置观战者状态中的游戏数据
key = (room_id, user_id)
if key in self.spectator_states:
state = self.spectator_states[key]
state.pop('game_data', None)
state.pop('score_data', None)
state['last_reset'] = datetime.now(UTC)
class SpectatorSyncManager:
@@ -393,6 +433,27 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
# 观战状态同步任务
self.spectator_sync_tasks: Dict[int, asyncio.Task] = {}
# 启动定期清理任务参考osu源码的清理机制
self._cleanup_task = asyncio.create_task(self._periodic_cleanup())
async def _periodic_cleanup(self):
"""定期清理过期数据包的后台任务"""
while True:
try:
await asyncio.sleep(60) # 每分钟执行一次
await packet_cleaner.cleanup_expired_packets()
# 记录清理统计
stats = packet_cleaner.get_cleanup_stats()
if stats['pending_packets'] > 0:
logger.debug(f"[MultiplayerHub] Cleanup stats: {stats}")
except Exception as e:
logger.error(f"[MultiplayerHub] Error in periodic cleanup: {e}")
except asyncio.CancelledError:
logger.info("[MultiplayerHub] Periodic cleanup task cancelled")
break
async def initialize_managers(self):
"""初始化管理器"""
if not self.spectator_sync_manager:
@@ -1160,17 +1221,72 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
except asyncio.CancelledError:
pass
async def _cleanup_game_session(self, room_id: int, game_completed: bool):
"""清理单局游戏会话数据基于osu源码实现"""
try:
# 停止实时排行榜广播
await self._stop_leaderboard_broadcast_task(room_id)
# 获取最终排行榜
final_leaderboard = gameplay_buffer.get_leaderboard(room_id)
# 发送最终排行榜给所有用户
if final_leaderboard:
await self.broadcast_group_call(
self.group_id(room_id),
"FinalLeaderboard",
final_leaderboard
)
# 使用新的清理管理器清理游戏会话参考osu源码
await GameSessionCleaner.cleanup_game_session(room_id, game_completed)
# 清理游戏会话数据
await gameplay_buffer.cleanup_game_session(room_id)
# 通知观战同步管理器游戏结束
if hasattr(self, 'spectator_sync_manager') and self.spectator_sync_manager:
await self.spectator_sync_manager.notify_gameplay_ended(room_id, {
'final_leaderboard': final_leaderboard,
'completed': game_completed,
'timestamp': datetime.now(UTC).isoformat()
})
# 重置所有用户的游戏状态
if room_id in self.rooms:
room = self.rooms[room_id]
for user in room.room.users:
gameplay_buffer.reset_user_gameplay_state(room_id, user.user_id)
# 安排用户会话清理
await GameSessionCleaner.cleanup_user_session(room_id, user.user_id)
logger.info(f"[MultiplayerHub] Cleaned up game session for room {room_id} (completed: {game_completed})")
except Exception as e:
logger.error(f"[MultiplayerHub] Failed to cleanup game session for room {room_id}: {e}")
# 即使清理失败也不应该影响游戏流程
async def change_user_state(
self,
room: ServerMultiplayerRoom,
user: MultiplayerRoomUser,
state: MultiplayerUserState,
):
old_state = user.state
logger.info(
f"[MultiplayerHub] {user.user_id}'s state "
f"changed from {user.state} to {state}"
f"changed from {old_state} to {state}"
)
user.state = state
# 在用户进入RESULTS状态时清理其游戏数据参考osu源码
if state == MultiplayerUserState.RESULTS and old_state.is_playing:
room_id = room.room.room_id
gameplay_buffer.reset_user_gameplay_state(room_id, user.user_id)
logger.debug(f"[MultiplayerHub] Reset gameplay state for user {user.user_id} in room {room_id}")
await self.broadcast_group_call(
self.group_id(room.room.room_id),
"UserStateChanged",
@@ -1431,6 +1547,10 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
# This ensures cross-hub spectating works properly
await self._notify_spectator_hub_game_ended(room)
# 每局游戏结束后的清理工作
room_id = room.room.room_id
await self._cleanup_game_session(room_id, any_user_finished_playing)
if any_user_finished_playing:
await self.event_logger.game_completed(
room.room.room_id,
@@ -1634,31 +1754,40 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
user: MultiplayerRoomUser,
kicked: bool = False,
):
room_id = room.room.room_id
user_id = user.user_id
if client:
self.remove_from_group(client, self.group_id(room.room.room_id))
self.remove_from_group(client, self.group_id(room_id))
room.room.users.remove(user)
target_store = self.state.get(user.user_id)
target_store = self.state.get(user_id)
if target_store:
target_store.room_id = 0
# 清理用户的游戏状态数据参考osu源码
gameplay_buffer.reset_user_gameplay_state(room_id, user_id)
# 使用清理管理器安排用户会话清理
await GameSessionCleaner.cleanup_user_session(room_id, user_id)
redis = get_redis()
await redis.publish("chat:room:left", f"{room.room.channel_id}:{user.user_id}")
await redis.publish("chat:room:left", f"{room.room.channel_id}:{user_id}")
async with with_db() as session:
async with session.begin():
participated_user = (
await session.exec(
select(RoomParticipatedUser).where(
RoomParticipatedUser.room_id == room.room.room_id,
RoomParticipatedUser.user_id == user.user_id,
RoomParticipatedUser.room_id == room_id,
RoomParticipatedUser.user_id == user_id,
)
)
).first()
if participated_user is not None:
participated_user.left_at = datetime.now(UTC)
db_room = await session.get(Room, room.room.room_id)
db_room = await session.get(Room, room_id)
if db_room is None:
raise InvokeException("Room does not exist in database")
if db_room.participant_count > 0:
@@ -1671,7 +1800,7 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
if (
len(room.room.users) != 0
and room.room.host
and room.room.host.user_id == user.user_id
and room.room.host.user_id == user_id
):
next_host = room.room.users[0]
await self.set_host(room, next_host)
@@ -1680,11 +1809,11 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
if client:
await self.call_noblock(client, "UserKicked", user)
await self.broadcast_group_call(
self.group_id(room.room.room_id), "UserKicked", user
self.group_id(room_id), "UserKicked", user
)
else:
await self.broadcast_group_call(
self.group_id(room.room.room_id), "UserLeft", user
self.group_id(room_id), "UserLeft", user
)
async def end_room(self, room: ServerMultiplayerRoom):
@@ -1716,6 +1845,9 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
await self._stop_leaderboard_broadcast_task(room_id)
await gameplay_buffer.cleanup_room(room_id)
# 使用清理管理器完全清理房间参考osu源码
await GameSessionCleaner.cleanup_room_fully(room_id)
# 清理观战同步任务
if room_id in self.spectator_sync_tasks:
task = self.spectator_sync_tasks.pop(room_id)
@@ -1756,6 +1888,23 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
'position': score_data.get('position', 0)
})
# 安排分数数据包清理参考osu源码的数据包管理
await packet_cleaner.schedule_cleanup(room_id, {
'type': 'score',
'user_id': client.user_id,
'data_size': len(str(score_data)),
'timestamp': datetime.now(UTC).isoformat()
})
# 如果游戏完成,标记用户状态
if score_data.get('completed', False):
await self.change_user_state(
server_room, user, MultiplayerUserState.FINISHED_PLAY
)
# 立即安排该用户的清理
await GameSessionCleaner.cleanup_user_session(room_id, client.user_id)
except Exception as e:
logger.error(f"Error updating score for user {client.user_id}: {e}")
@@ -1867,12 +2016,18 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
if not user.state.is_playing:
raise InvokeException("Cannot abort gameplay while not in a gameplay state")
# 清理用户游戏数据参考osu源码
room_id = room.room_id
gameplay_buffer.reset_user_gameplay_state(room_id, user.user_id)
await self.change_user_state(
server_room,
user,
MultiplayerUserState.IDLE,
)
await self.update_room_state(server_room)
logger.info(f"[MultiplayerHub] User {user.user_id} aborted gameplay in room {room_id}")
async def AbortMatch(self, client: Client):
server_room = self._ensure_in_room(client)
@@ -1885,6 +2040,13 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
):
raise InvokeException("Cannot abort a match that hasn't started.")
room_id = room.room_id
# 清理所有玩家的游戏状态数据参考osu源码
for user in room.users:
if user.state.is_playing:
gameplay_buffer.reset_user_gameplay_state(room_id, user.user_id)
await asyncio.gather(
*[
self.change_user_state(server_room, u, MultiplayerUserState.IDLE)
@@ -1892,14 +2054,18 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
if u.state.is_playing
]
)
# 执行完整的游戏会话清理
await self._cleanup_game_session(room_id, False) # False表示游戏被中断而非完成
await self.broadcast_group_call(
self.group_id(room.room_id),
self.group_id(room_id),
"GameplayAborted",
GameplayAbortReason.HOST_ABORTED,
)
await self.update_room_state(server_room)
logger.info(
f"[MultiplayerHub] {client.user_id} aborted match in room {room.room_id}"
f"[MultiplayerHub] {client.user_id} aborted match in room {room_id}"
)
async def change_user_match_state(
@@ -2077,6 +2243,9 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
# Import here to avoid circular imports
from app.signalr.hub import SpectatorHubs
from app.models.spectator_hub import SpectatedUserState, SpectatorState
from .spectator_buffer import spectator_state_manager
room_id = room.room.room_id
# For each user who finished the game, notify SpectatorHub
for room_user in room.room.users:
@@ -2090,6 +2259,12 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
maximum_statistics={},
)
# 同步到观战缓冲区管理器
await spectator_state_manager.handle_user_finished_playing(
room_user.user_id,
finished_state
)
# Notify all SpectatorHub watchers that this user finished
await SpectatorHubs.broadcast_group_call(
SpectatorHubs.group_id(room_user.user_id),
@@ -2099,9 +2274,35 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
)
logger.debug(
f"[MultiplayerHub] Notified SpectatorHub that user {room_user.user_id} finished game"
f"[MultiplayerHub] Synced and notified SpectatorHub that user {room_user.user_id} finished game"
)
# 同步游戏中玩家的状态
elif room_user.state == MultiplayerUserState.PLAYING:
try:
multiplayer_data = {
'room_id': room_id,
'beatmap_id': room.queue.current_item.beatmap_id,
'ruleset_id': room_user.ruleset_id or 0,
'mods': room_user.mods,
'state': room_user.state,
'maximum_statistics': {}
}
await spectator_state_manager.sync_with_multiplayer(
room_user.user_id,
multiplayer_data
)
logger.debug(
f"[MultiplayerHub] Synced playing state for user {room_user.user_id} to SpectatorHub buffer"
)
except Exception as e:
logger.debug(
f"[MultiplayerHub] Failed to sync playing state for user {room_user.user_id}: {e}"
)
except Exception as e:
logger.debug(
f"[MultiplayerHub] Failed to notify SpectatorHub about game end: {e}"