Обработка ситуаций рекконекта. Доработка 2.

This commit is contained in:
PsiMagistr 2025-05-29 15:17:59 +03:00
parent eaaf7ae14c
commit 3973ac748c

View File

@ -23,13 +23,8 @@ class PlayerConnectionHandler {
const existingPlayerByIdentifier = Object.values(this.players).find(p => p.identifier === identifier); const existingPlayerByIdentifier = Object.values(this.players).find(p => p.identifier === identifier);
if (existingPlayerByIdentifier) { if (existingPlayerByIdentifier) {
console.warn(`[PCH ${this.gameId}] Идентификатор ${identifier} уже связан с ролью игрока ${existingPlayerByIdentifier.id} (сокет ${existingPlayerByIdentifier.socket?.id}). Обрабатывается как возможное переподключение.`); console.warn(`[PCH ${this.gameId}] Идентификатор ${identifier} уже связан с ролью игрока ${existingPlayerByIdentifier.id} (сокет ${existingPlayerByIdentifier.socket?.id}). Обрабатывается как возможное переподключение (вызов handlePlayerReconnected).`);
if (this.gameInstance.gameState && this.gameInstance.gameState.isGameOver) { // Делегируем handlePlayerReconnected, который разберется, новый ли это сокет, был ли игрок отключен и т.д.
console.warn(`[PCH ${this.gameId}] Игрок ${identifier} пытается (пере)присоединиться к уже завершенной игре. Отправка gameError.`);
socket.emit('gameError', { message: 'Эта игра уже завершена.' });
return false;
}
// Делегируем handlePlayerReconnected, который разберется, новый ли это сокет или тот же.
return this.handlePlayerReconnected(existingPlayerByIdentifier.id, socket); return this.handlePlayerReconnected(existingPlayerByIdentifier.id, socket);
} }
@ -38,14 +33,14 @@ class PlayerConnectionHandler {
socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' });
return false; return false;
} }
if (this.mode === 'ai' && this.playerCount >= 1) { if (this.mode === 'ai' && this.playerCount >= 1) { // В AI режиме только 1 человек
socket.emit('gameError', { message: 'К AI игре может присоединиться только один игрок.'}); socket.emit('gameError', { message: 'К AI игре может присоединиться только один игрок.'});
return false; return false;
} }
let assignedPlayerId; let assignedPlayerId;
let actualCharacterKey = chosenCharacterKey || 'elena'; let actualCharacterKey = chosenCharacterKey || 'elena';
const charDataForName = dataUtils.getCharacterData(actualCharacterKey); // Для имени const charDataForName = dataUtils.getCharacterData(actualCharacterKey);
if (this.mode === 'ai') { if (this.mode === 'ai') {
assignedPlayerId = GAME_CONFIG.PLAYER_ID; assignedPlayerId = GAME_CONFIG.PLAYER_ID;
@ -57,8 +52,10 @@ class PlayerConnectionHandler {
const firstPlayerInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); const firstPlayerInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === actualCharacterKey) { if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === actualCharacterKey) {
const allKeys = dataUtils.getAllCharacterKeys ? dataUtils.getAllCharacterKeys() : ['elena', 'almagest', 'balard']; const allKeys = dataUtils.getAllCharacterKeys ? dataUtils.getAllCharacterKeys() : ['elena', 'almagest', 'balard'];
const otherKey = allKeys.find(k => k !== firstPlayerInfo.chosenCharacterKey && k !== 'balard'); // Не даем Баларда второму игроку по умолчанию // Исключаем Баларда из автовыбора для второго игрока, если первый не Балард
actualCharacterKey = otherKey || (actualCharacterKey === 'elena' ? 'almagest' : 'elena'); // Фоллбэк const disallowedKeyForOpponent = firstPlayerInfo.chosenCharacterKey === 'balard' ? null : 'balard';
const otherKey = allKeys.find(k => k !== firstPlayerInfo.chosenCharacterKey && k !== disallowedKeyForOpponent);
actualCharacterKey = otherKey || (actualCharacterKey === 'elena' ? 'almagest' : 'elena');
} }
} else { } else {
socket.emit('gameError', { message: 'Не удалось найти свободный слот в PvP игре.' }); socket.emit('gameError', { message: 'Не удалось найти свободный слот в PvP игре.' });
@ -67,24 +64,23 @@ class PlayerConnectionHandler {
} }
// Удаление старой записи, если сокет для этой роли уже существует, но с другим ID // Удаление старой записи, если сокет для этой роли уже существует, но с другим ID
// (на случай очень быстрой смены сокета до срабатывания disconnect) // Это может произойти при очень быстрой смене сокета до срабатывания disconnect старого.
const oldPlayerSocketEntry = Object.entries(this.players).find(([sid, pInfo]) => pInfo.id === assignedPlayerId); const oldPlayerSocketEntryForRole = Object.entries(this.players).find(([sid, pInfo]) => pInfo.id === assignedPlayerId);
if (oldPlayerSocketEntry) { if (oldPlayerSocketEntryForRole) {
const [oldSocketId, oldPlayerInfo] = oldPlayerSocketEntry; const [oldSocketId, oldPlayerInfo] = oldPlayerSocketEntryForRole;
if (oldPlayerInfo.socket && oldPlayerInfo.socket.id !== socket.id) { if (oldPlayerInfo.socket && oldPlayerInfo.socket.id !== socket.id) {
console.warn(`[PCH ${this.gameId}] addPlayer: Найдена старая запись для роли ${assignedPlayerId} с сокетом ${oldPlayerInfo.socket.id}. Новый сокет: ${socket.id}. Удаляем старую запись.`); console.warn(`[PCH ${this.gameId}] addPlayer: Найдена старая запись для роли ${assignedPlayerId} с сокетом ${oldPlayerInfo.socket.id}овый сокет: ${socket.id}). Удаляем старую запись.`);
try { try {
if (oldPlayerInfo.socket.connected) oldPlayerInfo.socket.disconnect(true); if (oldPlayerInfo.socket.connected) oldPlayerInfo.socket.disconnect(true);
} catch (e) { console.error(`[PCH ${this.gameId}] Ошибка при дисконнекте старого сокета: ${e.message}`); } } catch (e) { console.error(`[PCH ${this.gameId}] Ошибка при дисконнекте старого сокета в addPlayer: ${e.message}`); }
delete this.players[oldSocketId]; delete this.players[oldSocketId]; // Удаляем по старому socket.id
if (this.playerSockets[assignedPlayerId] === oldPlayerInfo.socket) { if (this.playerSockets[assignedPlayerId] === oldPlayerInfo.socket) {
delete this.playerSockets[assignedPlayerId]; delete this.playerSockets[assignedPlayerId]; // Удаляем из авторитетных, если это был он
} }
// Не уменьшаем playerCount здесь, так как это замена, а не уход // playerCount не уменьшаем, т.к. это замена, а не полноценный уход игрока из игры
} }
} }
this.players[socket.id] = { this.players[socket.id] = {
id: assignedPlayerId, id: assignedPlayerId,
socket: socket, socket: socket,
@ -94,17 +90,15 @@ class PlayerConnectionHandler {
name: charDataForName?.baseStats?.name || actualCharacterKey name: charDataForName?.baseStats?.name || actualCharacterKey
}; };
this.playerSockets[assignedPlayerId] = socket; this.playerSockets[assignedPlayerId] = socket;
this.playerCount++; // Увеличиваем счетчик активных игроков this.playerCount++;
try { try {
socket.join(this.gameId); socket.join(this.gameId);
console.log(`[PCH ${this.gameId}] Сокет ${socket.id} присоединен к комнате ${this.gameId} (addPlayer).`); console.log(`[PCH ${this.gameId}] Сокет ${socket.id} (identifier: ${identifier}) присоединен к комнате ${this.gameId} (addPlayer).`);
} catch (e) { } catch (e) {
console.error(`[PCH ${this.gameId}] КРИТИЧЕСКАЯ ОШИБКА при socket.join: ${e.message}. Игрок ${identifier} может не получать широковещательные сообщения.`); console.error(`[PCH ${this.gameId}] КРИТИЧЕСКАЯ ОШИБКА при socket.join в addPlayer для ${identifier}: ${e.message}.`);
// Возможно, стоит откатить добавление игрока или вернуть false
} }
if (assignedPlayerId === GAME_CONFIG.PLAYER_ID) this.gameInstance.setPlayerCharacterKey(actualCharacterKey); if (assignedPlayerId === GAME_CONFIG.PLAYER_ID) this.gameInstance.setPlayerCharacterKey(actualCharacterKey);
else if (assignedPlayerId === GAME_CONFIG.OPPONENT_ID) this.gameInstance.setOpponentCharacterKey(actualCharacterKey); else if (assignedPlayerId === GAME_CONFIG.OPPONENT_ID) this.gameInstance.setOpponentCharacterKey(actualCharacterKey);
@ -124,10 +118,13 @@ class PlayerConnectionHandler {
console.log(`[PCH ${this.gameId}] Окончательное удаление игрока ${playerIdentifier} (Socket: ${socketId}, Role: ${playerRole}). Причина: ${reason}.`); console.log(`[PCH ${this.gameId}] Окончательное удаление игрока ${playerIdentifier} (Socket: ${socketId}, Role: ${playerRole}). Причина: ${reason}.`);
if (playerInfo.socket) { if (playerInfo.socket) {
try { playerInfo.socket.leave(this.gameId); } catch (e) { console.warn(`[PCH ${this.gameId}] Ошибка при playerInfo.socket.leave в removePlayer: ${e.message}`); } try {
console.log(`[PCH ${this.gameId}] Попытка socket.leave(${this.gameId}) для ${socketId} в removePlayer.`);
playerInfo.socket.leave(this.gameId);
} catch (e) { console.warn(`[PCH ${this.gameId}] Ошибка при playerInfo.socket.leave в removePlayer для ${socketId}: ${e.message}`); }
} }
if (!playerInfo.isTemporarilyDisconnected) { if (!playerInfo.isTemporarilyDisconnected) { // Уменьшаем счетчик только если это был активный игрок
this.playerCount--; this.playerCount--;
} }
@ -135,7 +132,7 @@ class PlayerConnectionHandler {
if (this.playerSockets[playerRole]?.id === socketId) { if (this.playerSockets[playerRole]?.id === socketId) {
delete this.playerSockets[playerRole]; delete this.playerSockets[playerRole];
} }
this.clearReconnectTimer(playerRole); this.clearReconnectTimer(playerRole); // Очищаем таймер переподключения для этой роли
console.log(`[PCH ${this.gameId}] Игрок ${playerIdentifier} удален. Активных игроков сейчас: ${this.playerCount}.`); console.log(`[PCH ${this.gameId}] Игрок ${playerIdentifier} удален. Активных игроков сейчас: ${this.playerCount}.`);
this.gameInstance.handlePlayerPermanentlyLeft(playerRole, playerInfo.chosenCharacterKey, reason); this.gameInstance.handlePlayerPermanentlyLeft(playerRole, playerInfo.chosenCharacterKey, reason);
@ -147,25 +144,31 @@ class PlayerConnectionHandler {
handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey, disconnectedSocketId) { handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey, disconnectedSocketId) {
console.log(`[PCH ${this.gameId}] handlePlayerPotentiallyLeft для роли ${playerIdRole}, id ${identifier}, char ${characterKey}, disconnectedSocketId ${disconnectedSocketId}`); console.log(`[PCH ${this.gameId}] handlePlayerPotentiallyLeft для роли ${playerIdRole}, id ${identifier}, char ${characterKey}, disconnectedSocketId ${disconnectedSocketId}`);
// Находим запись игрока по роли и идентификатору, т.к. disconnectedSocketId может быть уже старым
const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier); const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
if (!playerEntry || !playerEntry.socket) { if (!playerEntry || !playerEntry.socket) {
console.warn(`[PCH ${this.gameId}] Запись игрока или сокет не найдены для ${identifier} (роль ${playerIdRole}) во время потенциального выхода. disconnectedSocketId: ${disconnectedSocketId}`); console.warn(`[PCH ${this.gameId}] Запись игрока по роли/id (${identifier}/${playerIdRole}) не найдена, или у нее нет сокета. disconnectedSocketId: ${disconnectedSocketId}`);
// Если запись по disconnectedSocketId все еще существует (например, старая запись), удаляем ее.
if (this.players[disconnectedSocketId]) { if (this.players[disconnectedSocketId]) {
console.warn(`[PCH ${this.gameId}] Найдена запись по disconnectedSocketId ${disconnectedSocketId} (без playerEntry по роли/id), удаляем ее.`); console.warn(`[PCH ${this.gameId}] Найдена запись по disconnectedSocketId ${disconnectedSocketId} (без playerEntry по роли/id), удаляем ее через removePlayer.`);
this.removePlayer(disconnectedSocketId, 'stale_socket_disconnect_no_main_entry'); this.removePlayer(disconnectedSocketId, 'stale_socket_disconnect_no_active_entry');
} }
return; return;
} }
// Если сокет, который отключился, не является текущим авторитетным сокетом для этого игрока,
// значит, игрок уже переподключился с новым сокетом. Это запоздалое событие.
if (playerEntry.socket.id !== disconnectedSocketId) { if (playerEntry.socket.id !== disconnectedSocketId) {
console.log(`[PCH ${this.gameId}] Событие отключения для УСТАРЕВШЕГО сокета ${disconnectedSocketId} для игрока ${identifier} (Роль ${playerIdRole}). Текущий активный сокет: ${playerEntry.socket.id}. Игнорируем.`); console.log(`[PCH ${this.gameId}] Событие отключения для УСТАРЕВШЕГО сокета ${disconnectedSocketId} для игрока ${identifier} (Роль ${playerIdRole}). Текущий активный сокет: ${playerEntry.socket.id}. Игнорируем.`);
// Удаляем запись по устаревшему disconnectedSocketId, если она еще есть
if (this.players[disconnectedSocketId]) { if (this.players[disconnectedSocketId]) {
delete this.players[disconnectedSocketId]; delete this.players[disconnectedSocketId];
} }
return; return;
} }
// Далее, playerEntry.socket.id === disconnectedSocketId (отключился текущий авторитетный сокет)
if (this.gameInstance.gameState && this.gameInstance.gameState.isGameOver) { if (this.gameInstance.gameState && this.gameInstance.gameState.isGameOver) {
console.log(`[PCH ${this.gameId}] Игра уже завершена, не обрабатываем потенциальный выход для ${identifier}.`); console.log(`[PCH ${this.gameId}] Игра уже завершена, не обрабатываем потенциальный выход для ${identifier}.`);
return; return;
@ -176,7 +179,7 @@ class PlayerConnectionHandler {
} }
playerEntry.isTemporarilyDisconnected = true; playerEntry.isTemporarilyDisconnected = true;
this.playerCount--; // Уменьшаем счетчик активных this.playerCount--;
console.log(`[PCH ${this.gameId}] Игрок ${identifier} (роль ${playerIdRole}, сокет ${disconnectedSocketId}) временно отключен. Активных: ${this.playerCount}. Запускаем таймер переподключения.`); console.log(`[PCH ${this.gameId}] Игрок ${identifier} (роль ${playerIdRole}, сокет ${disconnectedSocketId}) временно отключен. Активных: ${this.playerCount}. Запускаем таймер переподключения.`);
const disconnectedName = playerEntry.name || this.gameInstance.gameState?.[playerIdRole]?.name || characterKey || `Игрок (Роль ${playerIdRole})`; const disconnectedName = playerEntry.name || this.gameInstance.gameState?.[playerIdRole]?.name || characterKey || `Игрок (Роль ${playerIdRole})`;
@ -201,13 +204,13 @@ class PlayerConnectionHandler {
this.pausedTurnState = null; this.pausedTurnState = null;
} }
this.clearReconnectTimer(playerIdRole); // Очищаем старый, если был this.clearReconnectTimer(playerIdRole);
const reconnectDuration = GAME_CONFIG.RECONNECT_TIMEOUT_MS || 30000; const reconnectDuration = GAME_CONFIG.RECONNECT_TIMEOUT_MS || 30000;
const reconnectStartTime = Date.now(); const reconnectStartTime = Date.now();
const updateInterval = setInterval(() => { const updateInterval = setInterval(() => {
const timerInfo = this.reconnectTimers[playerIdRole]; const timerInfo = this.reconnectTimers[playerIdRole];
if (!timerInfo || timerInfo.timerId === null) { // Если основной таймер уже сработал/очищен if (!timerInfo || timerInfo.timerId === null) {
if (timerInfo?.updateIntervalId) clearInterval(timerInfo.updateIntervalId); if (timerInfo?.updateIntervalId) clearInterval(timerInfo.updateIntervalId);
if (timerInfo) timerInfo.updateIntervalId = null; if (timerInfo) timerInfo.updateIntervalId = null;
this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: 0 }); this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: 0 });
@ -215,7 +218,6 @@ class PlayerConnectionHandler {
} }
const remaining = reconnectDuration - (Date.now() - reconnectStartTime); const remaining = reconnectDuration - (Date.now() - reconnectStartTime);
if (remaining <= 0) { if (remaining <= 0) {
// Даем основному setTimeout сработать, здесь просто останавливаем интервал тиков
clearInterval(timerInfo.updateIntervalId); clearInterval(timerInfo.updateIntervalId);
timerInfo.updateIntervalId = null; timerInfo.updateIntervalId = null;
this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: 0 }); this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: 0 });
@ -225,24 +227,25 @@ class PlayerConnectionHandler {
}, 1000); }, 1000);
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
const timerInfo = this.reconnectTimers[playerIdRole]; const timerInfo = this.reconnectTimers[playerIdRole]; // Получаем ссылку на объект таймера
if (timerInfo?.updateIntervalId) { if (timerInfo) { // Проверяем, что объект еще существует
clearInterval(timerInfo.updateIntervalId); if (timerInfo.updateIntervalId) {
timerInfo.updateIntervalId = null; clearInterval(timerInfo.updateIntervalId);
timerInfo.updateIntervalId = null;
}
timerInfo.timerId = null; // Помечаем, что основной таймаут сработал или очищен
}
// Удаляем всю запись о таймере для этой роли, если она еще есть
if (this.reconnectTimers[playerIdRole]) {
delete this.reconnectTimers[playerIdRole];
} }
if (timerInfo) timerInfo.timerId = null; // Помечаем, что сработал
// this.clearReconnectTimer(playerIdRole) здесь вызовет сам себя рекурсивно, если удалить delete this.reconnectTimers[playerIdRole];
// Поэтому просто удаляем запись, т.к. таймеры уже очищены или помечены.
if (this.reconnectTimers[playerIdRole]) delete this.reconnectTimers[playerIdRole];
const stillDiscPlayer = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier); const stillDiscPlayer = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
if (stillDiscPlayer && stillDiscPlayer.isTemporarilyDisconnected) { if (stillDiscPlayer && stillDiscPlayer.isTemporarilyDisconnected) {
console.log(`[PCH ${this.gameId}] Таймаут переподключения для ${identifier}. Удаляем игрока.`); console.log(`[PCH ${this.gameId}] Таймаут переподключения для ${identifier} (${playerIdRole}). Удаляем игрока.`);
this.removePlayer(stillDiscPlayer.socket.id, "reconnect_timeout"); this.removePlayer(stillDiscPlayer.socket.id, "reconnect_timeout");
} else { } else {
console.log(`[PCH ${this.gameId}] Таймаут переподключения для ${identifier}, но игрок уже не (или не был) isTemporarilyDisconnected.`); console.log(`[PCH ${this.gameId}] Таймаут переподключения для ${identifier} (${playerIdRole}), но игрок уже не (или не был) isTemporarilyDisconnected. Или не найден.`);
} }
}, reconnectDuration); }, reconnectDuration);
this.reconnectTimers[playerIdRole] = { timerId: timeoutId, updateIntervalId: updateInterval, startTimeMs: reconnectStartTime, durationMs: reconnectDuration }; this.reconnectTimers[playerIdRole] = { timerId: timeoutId, updateIntervalId: updateInterval, startTimeMs: reconnectStartTime, durationMs: reconnectDuration };
@ -258,45 +261,50 @@ class PlayerConnectionHandler {
} }
let playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier); let playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
console.log(`[PCH RECONNECT_ATTEMPT] Found playerEntry:`, playerEntry ? {id: playerEntry.id, identifier: playerEntry.identifier, oldSocketId: playerEntry.socket?.id, isTempDisc: playerEntry.isTemporarilyDisconnected} : null); console.log(`[PCH RECONNECT_ATTEMPT] Found playerEntry for role ${playerIdRole}, id ${identifier}:`, playerEntry ? {socketId: playerEntry.socket?.id, isTempDisc: playerEntry.isTemporarilyDisconnected} : 'Not Found');
if (playerEntry) { if (playerEntry) {
const oldSocket = playerEntry.socket; const oldSocket = playerEntry.socket;
const wasTemporarilyDisconnected = playerEntry.isTemporarilyDisconnected; const wasTemporarilyDisconnected = playerEntry.isTemporarilyDisconnected;
// Обновляем сокет в playerEntry и в this.players / this.playerSockets, если сокет новый
if (oldSocket && oldSocket.id !== newSocket.id) { if (oldSocket && oldSocket.id !== newSocket.id) {
console.log(`[PCH ${this.gameId}] Новый сокет ${newSocket.id} для игрока ${identifier}. Старый сокет: ${oldSocket.id}. Обновляем записи.`); console.log(`[PCH ${this.gameId}] Новый сокет ${newSocket.id} для игрока ${identifier}. Старый сокет: ${oldSocket.id}. Обновляем записи.`);
if (this.players[oldSocket.id]) delete this.players[oldSocket.id]; if (this.players[oldSocket.id]) delete this.players[oldSocket.id];
if (oldSocket.connected) { if (oldSocket.connected) {
console.log(`[PCH ${this.gameId}] Отключаем старый "подвисший" сокет ${oldSocket.id}.`); console.log(`[PCH ${this.gameId}] Отключаем старый "подвисший" сокет ${oldSocket.id}.`);
oldSocket.disconnect(true); oldSocket.disconnect(true); // true - close an underlying connection
} }
} }
playerEntry.socket = newSocket; playerEntry.socket = newSocket;
this.players[newSocket.id] = playerEntry; // Обновляем/добавляем запись с новым socket.id this.players[newSocket.id] = playerEntry; // Обновляем/добавляем запись с новым socket.id
// Если старый ID был ключом для playerEntry, и он не равен newSocket.id, удаляем старый ключ
if (oldSocket && oldSocket.id !== newSocket.id && this.players[oldSocket.id] === playerEntry) { if (oldSocket && oldSocket.id !== newSocket.id && this.players[oldSocket.id] === playerEntry) {
// Если playerEntry был взят по старому socket.id, и этот ID теперь должен быть удален
delete this.players[oldSocket.id]; delete this.players[oldSocket.id];
} }
this.playerSockets[playerIdRole] = newSocket; this.playerSockets[playerIdRole] = newSocket;
// Всегда заново присоединяем сокет к комнате (Socket.IO handle'ит дубликаты)
console.log(`[PCH ${this.gameId}] Socket ${newSocket.id} (identifier: ${identifier}) attempting to join room ${this.gameId}.`);
try { try {
newSocket.join(this.gameId); newSocket.join(this.gameId);
console.log(`[PCH ${this.gameId}] Сокет ${newSocket.id} (identifier: ${identifier}) присоединен/переприсоединен к комнате ${this.gameId} (handlePlayerReconnected).`); const socketsInRoomAfterJoin = Array.from(this.io.sockets.adapter.rooms.get(this.gameId) || []);
console.log(`[PCH ${this.gameId}] Socket ${newSocket.id} finished join attempt. Sockets in room ${this.gameId} NOW: [${socketsInRoomAfterJoin.join(', ')}]. Expected to include: ${newSocket.id}`);
if (!socketsInRoomAfterJoin.includes(newSocket.id)) {
console.error(`[PCH ${this.gameId}] CRITICAL: Socket ${newSocket.id} DID NOT APPEAR IN ROOM ${this.gameId} immediately after join! Client might not receive room-based events.`);
}
} catch (e) { } catch (e) {
console.error(`[PCH ${this.gameId}] КРИТИЧЕСКАЯ ОШИБКА при newSocket.join в handlePlayerReconnected: ${e.message}.`); console.error(`[PCH ${this.gameId}] CRITICAL ERROR during newSocket.join(${this.gameId}): ${e.message}.`);
} }
if (wasTemporarilyDisconnected) { if (wasTemporarilyDisconnected) {
console.log(`[PCH ${this.gameId}] Переподключение игрока ${identifier} (Роль: ${playerIdRole}), который был временно отключен.`); console.log(`[PCH ${this.gameId}] Переподключение игрока ${identifier} (Роль: ${playerIdRole}), который был временно отключен.`);
this.clearReconnectTimer(playerIdRole); this.clearReconnectTimer(playerIdRole);
this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: null }); this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: null });
playerEntry.isTemporarilyDisconnected = false; playerEntry.isTemporarilyDisconnected = false;
this.playerCount++; this.playerCount++;
} else { } else {
console.log(`[PCH ${this.gameId}] Игрок ${identifier} (Роль: ${playerIdRole}) переподключился/запросил состояние, не будучи помеченным как 'temporarilyDisconnected'. Старый сокет ID: ${oldSocket?.id}, Новый сокет ID: ${newSocket.id}`); console.log(`[PCH ${this.gameId}] Игрок ${identifier} (Роль: ${playerIdRole}) переподключился/запросил состояние, не будучи помеченным как 'temporarilyDisconnected'. Old socket ID: ${oldSocket?.id}, New socket ID: ${newSocket.id}. Player count (${this.playerCount}) not changed.`);
} }
// Обновление имени // Обновление имени
@ -306,41 +314,39 @@ class PlayerConnectionHandler {
const charData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey); const charData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey);
playerEntry.name = charData?.baseStats?.name || playerEntry.chosenCharacterKey; playerEntry.name = charData?.baseStats?.name || playerEntry.chosenCharacterKey;
} }
console.log(`[PCH ${this.gameId}] Имя игрока ${identifier} обновлено/установлено на: ${playerEntry.name}`); console.log(`[PCH ${this.gameId}] Имя игрока ${identifier} (${playerIdRole}) обновлено/установлено на: ${playerEntry.name}`);
this.gameInstance.addToLog(`🔌 Игрок ${playerEntry.name || identifier} снова в игре! (Сессия обновлена)`, GAME_CONFIG.LOG_TYPE_SYSTEM); this.gameInstance.addToLog(`🔌 Игрок ${playerEntry.name || identifier} снова в игре! (Сессия обновлена)`, GAME_CONFIG.LOG_TYPE_SYSTEM);
this.sendFullGameStateOnReconnect(newSocket, playerEntry, playerIdRole); this.sendFullGameStateOnReconnect(newSocket, playerEntry, playerIdRole); // Отправляем полное состояние
// Логика возобновления игры/таймера // Логика возобновления игры/таймера
if (wasTemporarilyDisconnected && this.pausedTurnState) { if (wasTemporarilyDisconnected && this.pausedTurnState) {
this.resumeGameLogicAfterReconnect(playerIdRole); this.resumeGameLogicAfterReconnect(playerIdRole); // Здесь возобновляется таймер, если он был на паузе
} else if (!wasTemporarilyDisconnected) { } else if (!wasTemporarilyDisconnected) {
// Игрок не был temp disconnected. Таймер на сервере, если шел, то продолжал идти. // Игрок не был temp disconnected. Таймер на сервере, если шел, то продолжал идти.
// Клиент получил новое состояние. Нужно, чтобы он начал получать обновления таймера. // Клиент получил новое состояние. Принудительный join выше должен был обеспечить доставку тиков.
// Принудительный join выше должен был помочь. // Дополнительно "пнем" таймер, чтобы он отправил текущее состояние, если он активен.
// Дополнительно заставим таймер отправить текущее состояние.
console.log(`[PCH ${this.gameId}] Player was not temp disconnected. Forcing timer update if active (for socket ${newSocket.id}).`); console.log(`[PCH ${this.gameId}] Player was not temp disconnected. Forcing timer update if active (for socket ${newSocket.id}).`);
if (this.gameInstance.turnTimer && this.gameInstance.turnTimer.onTickCallback) { if (this.gameInstance.turnTimer && this.gameInstance.turnTimer.onTickCallback) {
const tt = this.gameInstance.turnTimer; const tt = this.gameInstance.turnTimer;
// Если таймер реально работает (не ход AI и не на ручной паузе от другого игрока) if (tt.isCurrentlyRunning && !tt.isManuallyPausedState) { // Если таймер реально работает
if (tt.isCurrentlyRunning && !tt.isManuallyPausedState && !tt.isConfiguredForAiMove) {
const elapsedTime = Date.now() - tt.segmentStartTimeMs; const elapsedTime = Date.now() - tt.segmentStartTimeMs;
const currentRemaining = Math.max(0, tt.segmentDurationMs - elapsedTime); const currentRemaining = Math.max(0, tt.segmentDurationMs - elapsedTime);
console.log(`[PCH ${this.gameId}] Forcing onTickCallback. Remaining: ${currentRemaining}, ForPlayer: ${tt.isConfiguredForPlayerSlotTurn}, ManualPause: ${tt.isManuallyPausedState}`); console.log(`[PCH ${this.gameId}] Forcing onTickCallback. Remaining: ${currentRemaining}, ForPlayer: ${tt.isConfiguredForPlayerSlotTurn}, ManualPause: ${tt.isManuallyPausedState}`);
tt.onTickCallback(currentRemaining, tt.isConfiguredForPlayerSlotTurn, tt.isManuallyPausedState); tt.onTickCallback(currentRemaining, tt.isConfiguredForPlayerSlotTurn, tt.isManuallyPausedState);
} else if (tt.isConfiguredForAiMove && !tt.isCurrentlyRunning) { // Если ход AI } else if (tt.isConfiguredForAiMove && !tt.isCurrentlyRunning) {
console.log(`[PCH ${this.gameId}] Forcing onTickCallback for AI move state.`); console.log(`[PCH ${this.gameId}] Forcing onTickCallback for AI move state.`);
tt.onTickCallback(tt.initialTurnDurationMs, tt.isConfiguredForPlayerSlotTurn, false); tt.onTickCallback(tt.initialTurnDurationMs, tt.isConfiguredForPlayerSlotTurn, false); // false = not paused by PCH
} else if (tt.isManuallyPausedState) { // Если на ручной паузе (из-за другого игрока) } else if (tt.isManuallyPausedState) {
console.log(`[PCH ${this.gameId}] Forcing onTickCallback for manually paused state. Remaining: ${tt.segmentDurationMs}`); console.log(`[PCH ${this.gameId}] Forcing onTickCallback for manually paused state. Remaining: ${tt.segmentDurationMs}`);
tt.onTickCallback(tt.segmentDurationMs, tt.isConfiguredForPlayerSlotTurn, true); tt.onTickCallback(tt.segmentDurationMs, tt.isConfiguredForPlayerSlotTurn, true); // true = paused by PCH
} else if (!tt.isCurrentlyRunning && !tt.isManuallyPausedState && !this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver) { } else if (!tt.isCurrentlyRunning && !tt.isManuallyPausedState && !this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver) {
// Таймер не работает, не на паузе, игра не на паузе - возможно, его нужно запустить // Таймер не работает, не на ручной паузе, игра не на общей паузе - возможно, его нужно запустить
const gs = this.gameInstance.gameState; const gs = this.gameInstance.gameState;
const isHisTurnNow = (gs.isPlayerTurn && playerIdRole === GAME_CONFIG.PLAYER_ID) || (!gs.isPlayerTurn && playerIdRole === GAME_CONFIG.OPPONENT_ID); const isHisTurnNow = (gs.isPlayerTurn && playerIdRole === GAME_CONFIG.PLAYER_ID) || (!gs.isPlayerTurn && playerIdRole === GAME_CONFIG.OPPONENT_ID && this.mode === 'pvp');
const isAiTurnNow = this.mode === 'ai' && !gs.isPlayerTurn; const isAiTurnNow = this.mode === 'ai' && !gs.isPlayerTurn;
if(isHisTurnNow || isAiTurnNow) { if(isHisTurnNow || isAiTurnNow) {
console.log(`[PCH ${this.gameId}] Timer not active, attempting to start for ${playerIdRole}. HisTurn: ${isHisTurnNow}, AITurn: ${isAiTurnNow}`); console.log(`[PCH ${this.gameId}] Timer not running & not paused. Attempting to start for ${playerIdRole}. HisTurn: ${isHisTurnNow}, AITurn: ${isAiTurnNow}`);
this.gameInstance.turnTimer.start(gs.isPlayerTurn, isAiTurnNow); this.gameInstance.turnTimer.start(gs.isPlayerTurn, isAiTurnNow);
if (isAiTurnNow && !this.gameInstance.turnTimer.getIsConfiguredForAiMove?.()) { if (isAiTurnNow && !this.gameInstance.turnTimer.getIsConfiguredForAiMove?.()) {
setTimeout(() => { setTimeout(() => {
@ -355,9 +361,16 @@ class PlayerConnectionHandler {
} }
return true; return true;
} else { } else { // playerEntry не найден для этой роли и идентификатора
console.warn(`[PCH ${this.gameId}] Попытка переподключения для ${identifier} (Роль ${playerIdRole}), но запись playerEntry не найдена.`); console.warn(`[PCH ${this.gameId}] Попытка переподключения для ${identifier} (Роль ${playerIdRole}), но запись playerEntry не найдена. Это новый игрок?`);
newSocket.emit('gameError', { message: 'Не удалось найти вашу игровую сессию. Попробуйте создать игру заново.' }); // Это может быть сценарий, когда игрок впервые присоединяется к игре,
// и GameManager.handleRequestGameState вызвал этот метод.
// В этом случае, нам нужно вызвать addPlayer.
// Однако, если addPlayer уже был вызван и не нашел existingPlayerByIdentifier,
// то эта ветка означает, что что-то пошло не так с логикой GM или состоянием PCH.
// Для чистоты, handlePlayerReconnected не должен добавлять нового игрока.
// Если игрок не найден, это значит, что его сессии нет.
newSocket.emit('gameError', { message: 'Не удалось найти вашу игровую сессию для переподключения. Попробуйте создать игру заново.' });
return false; return false;
} }
} }
@ -401,7 +414,7 @@ class PlayerConnectionHandler {
opponentBaseStats: oData?.baseStats || {name: (this.mode === 'pvp' ? 'Ожидание...' : 'Противник AI'), maxHp:1, maxResource:0, resourceName:'N/A', attackPower:0, characterKey: null}, opponentBaseStats: oData?.baseStats || {name: (this.mode === 'pvp' ? 'Ожидание...' : 'Противник AI'), maxHp:1, maxResource:0, resourceName:'N/A', attackPower:0, characterKey: null},
playerAbilities: pData?.abilities, playerAbilities: pData?.abilities,
opponentAbilities: oData?.abilities || [], opponentAbilities: oData?.abilities || [],
log: this.gameInstance.consumeLogBuffer(), // Отправляем все накопленные логи log: this.gameInstance.consumeLogBuffer(),
clientConfig: { ...GAME_CONFIG } clientConfig: { ...GAME_CONFIG }
}); });
} }
@ -424,36 +437,40 @@ class PlayerConnectionHandler {
} }
} }
this.gameInstance.broadcastGameStateUpdate(); // Обновляем состояние для всех this.gameInstance.broadcastGameStateUpdate();
if (!this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver) { if (!this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver) {
if (Object.keys(this.reconnectTimers).length === 0) { if (Object.keys(this.reconnectTimers).length === 0) {
const currentTurnIsForPlayerInGS = this.gameInstance.gameState.isPlayerTurn; const currentTurnIsForPlayerInGS = this.gameInstance.gameState.isPlayerTurn;
const isCurrentTurnAiForTimer = this.mode === 'ai' && !currentTurnIsForPlayerInGS; // isAiMakingMove для TurnTimer.resume/start должно быть true, если это ход AI, И он сейчас не на паузе из-за дисконнекта игрока
const isAiTurnAndShouldAct = this.mode === 'ai' && !currentTurnIsForPlayerInGS;
let resumedFromPausedState = false; let resumedFromPausedState = false;
if (this.pausedTurnState && typeof this.pausedTurnState.remainingTime === 'number') { if (this.pausedTurnState && typeof this.pausedTurnState.remainingTime === 'number') {
const gsTurnMatchesPausedTurn = (currentTurnIsForPlayerInGS && this.pausedTurnState.forPlayerRoleIsPlayer) || // pausedTurnState.forPlayerRoleIsPlayer - это isConfiguredForPlayerSlotTurn таймера
(!currentTurnIsForPlayerInGS && !this.pausedTurnState.forPlayerRoleIsPlayer); // pausedTurnState.isAiCurrentlyMoving - это isConfiguredForAiMove таймера
const gsTurnMatchesPausedTurnPlayerPerspective = (currentTurnIsForPlayerInGS === this.pausedTurnState.forPlayerRoleIsPlayer);
// const gsAiMatchesPausedAi = (isAiTurnAndShouldAct === this.pausedTurnState.isAiCurrentlyMoving);
if (gsTurnMatchesPausedTurn) { // Возобновляем, если ход в GS совпадает с тем, для кого был таймер, ИЛИ если это был ход AI
if (gsTurnMatchesPausedTurnPlayerPerspective) {
console.log(`[PCH ${this.gameId}] Возобновляем таймер хода из pausedTurnState. Время: ${this.pausedTurnState.remainingTime}мс. Для игрока (в pausedState): ${this.pausedTurnState.forPlayerRoleIsPlayer}. GS ход игрока: ${currentTurnIsForPlayerInGS}. AI ход (в pausedState): ${this.pausedTurnState.isAiCurrentlyMoving}`); console.log(`[PCH ${this.gameId}] Возобновляем таймер хода из pausedTurnState. Время: ${this.pausedTurnState.remainingTime}мс. Для игрока (в pausedState): ${this.pausedTurnState.forPlayerRoleIsPlayer}. GS ход игрока: ${currentTurnIsForPlayerInGS}. AI ход (в pausedState): ${this.pausedTurnState.isAiCurrentlyMoving}`);
this.gameInstance.turnTimer.resume( this.gameInstance.turnTimer.resume(
this.pausedTurnState.remainingTime, this.pausedTurnState.remainingTime,
this.pausedTurnState.forPlayerRoleIsPlayer, this.pausedTurnState.forPlayerRoleIsPlayer, // Для кого был таймер
this.pausedTurnState.isAiCurrentlyMoving this.pausedTurnState.isAiCurrentlyMoving // Был ли это AI ход, когда остановили
); );
resumedFromPausedState = true; resumedFromPausedState = true;
} else { } else {
console.warn(`[PCH ${this.gameId}] pausedTurnState (${JSON.stringify(this.pausedTurnState)}) не совпадает с текущим ходом в gameState (isPlayerTurn: ${currentTurnIsForPlayerInGS}). Сбрасываем pausedTurnState и запускаем таймер заново, если нужно.`); console.warn(`[PCH ${this.gameId}] pausedTurnState (${JSON.stringify(this.pausedTurnState)}) НЕ СОВПАДАЕТ с текущим ходом в gameState (isPlayerTurn: ${currentTurnIsForPlayerInGS}). Сбрасываем pausedTurnState. Таймер будет запущен заново, если нужно.`);
} }
this.pausedTurnState = null; this.pausedTurnState = null;
} }
if (!resumedFromPausedState && this.gameInstance.turnTimer && !this.gameInstance.turnTimer.isActive() && !this.gameInstance.turnTimer.isPaused()) { if (!resumedFromPausedState && this.gameInstance.turnTimer && !this.gameInstance.turnTimer.isActive() && !this.gameInstance.turnTimer.isPaused()) {
console.log(`[PCH ${this.gameId}] Запускаем таймер хода заново после реконнекта (pausedState не использовался/неактуален, таймер неактивен и не на паузе). GS ход игрока: ${currentTurnIsForPlayerInGS}. AI ход для таймера: ${isCurrentTurnAiForTimer}`); console.log(`[PCH ${this.gameId}] Запускаем таймер хода заново после реконнекта (pausedState не использовался/неактуален, таймер неактивен и не на паузе). GS ход игрока: ${currentTurnIsForPlayerInGS}. AI ход для таймера: ${isAiTurnAndShouldAct}`);
this.gameInstance.turnTimer.start(currentTurnIsForPlayerInGS, isCurrentTurnAiForTimer); this.gameInstance.turnTimer.start(currentTurnIsForPlayerInGS, isAiTurnAndShouldAct);
if (isCurrentTurnAiForTimer && !this.gameInstance.turnTimer.getIsConfiguredForAiMove?.()) { if (isAiTurnAndShouldAct && !this.gameInstance.turnTimer.getIsConfiguredForAiMove?.()) {
setTimeout(() => { setTimeout(() => {
if (!this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver && this.mode === 'ai' && !this.gameInstance.gameState.isPlayerTurn) { if (!this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver && this.mode === 'ai' && !this.gameInstance.gameState.isPlayerTurn) {
this.gameInstance.processAiTurn(); this.gameInstance.processAiTurn();
@ -461,7 +478,7 @@ class PlayerConnectionHandler {
}, GAME_CONFIG.DELAY_OPPONENT_TURN); }, GAME_CONFIG.DELAY_OPPONENT_TURN);
} }
} else if (!resumedFromPausedState && this.gameInstance.turnTimer && this.gameInstance.turnTimer.isActive()){ } else if (!resumedFromPausedState && this.gameInstance.turnTimer && this.gameInstance.turnTimer.isActive()){
console.log(`[PCH ${this.gameId}] Таймер уже был активен при попытке перезапуска после реконнекта (pausedTurnState не использовался/неактуален). Ничего не делаем с таймером.`); console.log(`[PCH ${this.gameId}] Таймер уже был активен при попытке перезапуска после реконнекта (pausedTurnState не использовался/неактуален). Ничего не делаем с таймером, он должен сам слать обновления.`);
} }
} else { } else {
console.log(`[PCH ${this.gameId}] Возобновление логики таймера отложено, есть другие активные таймеры реконнекта: ${Object.keys(this.reconnectTimers)}`); console.log(`[PCH ${this.gameId}] Возобновление логики таймера отложено, есть другие активные таймеры реконнекта: ${Object.keys(this.reconnectTimers)}`);