diff --git a/bc.js b/bc.js index ea7e837..b28f6d7 100644 --- a/bc.js +++ b/bc.js @@ -1,166 +1,228 @@ -// bc.js (или server.js - ваш основной файл сервера) +// bc.js - Главный файл сервера Battle Club + const express = require('express'); const http = require('http'); -const socketIo = require('socket.io'); +const { Server } = require('socket.io'); const path = require('path'); -//hello6 -// Серверные модули -const GameManager = require('./server_modules/gameManager'); -const authController = require('./server_modules/auth'); // Ваш модуль аутентификации -// const GAME_CONFIG = require('./server_modules/config'); // Не используется напрямую здесь, но может быть полезен для отладки -const hostname = 'localhost'; // или '0.0.0.0' для доступа извне +// Импорт серверных модулей +const auth = require('./server_modules/auth'); +const GameManager = require('./server_modules/gameManager'); +const db = require('./server_modules/db'); // Импорт для инициализации соединения с БД (хотя пул создается при require) +const GAME_CONFIG = require('./server_modules/config'); // Конфиг игры +// gameData импортируется внутри GameInstance и GameLogic + const app = express(); const server = http.createServer(app); -const io = socketIo(server, { + +// Настройка Socket.IO +// cors options могут потребоваться, если клиент и сервер работают на разных портах/доменах +const io = new Server(server, { cors: { - origin: "*", // Разрешить все источники для простоты разработки. В продакшене укажите конкретный домен клиента. + origin: "*", // Разрешить подключение с любого домена (для разработки). В продакшене лучше указать конкретный домен клиента. methods: ["GET", "POST"] } }); -const PORT = process.env.PORT || 3200; - -// Статическое обслуживание файлов из папки 'public' +// Раздача статических файлов из папки 'public' app.use(express.static(path.join(__dirname, 'public'))); -// Создание экземпляра GameManager +// Создаем экземпляр GameManager const gameManager = new GameManager(io); +// Хранилище информации о залогиненных пользователях по socket.id +// В более сложном приложении здесь может быть Redis или другое внешнее хранилище сессий +const loggedInUsers = {}; // { socket.id: { userId: ..., username: ... } } + +// Обработка подключений Socket.IO io.on('connection', (socket) => { - console.log(`[Server BC.JS] New client connected: ${socket.id}`); + console.log(`[Socket.IO] Пользователь подключился: ${socket.id}`); - // При подключении нового клиента, отправляем ему текущий список доступных PvP игр - const availableGames = gameManager.getAvailablePvPGamesListForClient(); - socket.emit('availablePvPGamesList', availableGames); + // Привязываем user data к сокету (пока пустые) + socket.userData = null; // { userId: ..., username: ... } - // Обработчик запроса на обновление списка PvP игр - socket.on('requestPvPGameList', () => { - const currentAvailableGames = gameManager.getAvailablePvPGamesListForClient(); - socket.emit('availablePvPGamesList', currentAvailableGames); - }); + // При подключении клиента, если он уже залогинен (например, по cookie/token, что здесь не реализовано, + // но может быть добавлено), нужно восстановить его user data и проверить, не в игре ли он. + // В текущей простой реализации, мы полагаемся на то, что клиент после коннекта сам отправит логин, + // если он был залогинен. Но если бы была проверка сессии, логика была бы тут. + // Добавляем вызов handleRequestGameState при коннекте, если есть user data (для примера, + // но для полной реализации нужны cookies/токены) + // if (socket.userData?.userId) { // Эта проверка сработает только после успешного логина в текущей сессии + // gameManager.handleRequestGameState(socket, socket.userData.userId); // Передаем объект socket + // } - // --- Аутентификация --- + + // --- Обработчики событий Аутентификации --- socket.on('register', async (data) => { - console.log(`[Server BC.JS] Received 'register' event from ${socket.id} with username: ${data?.username}`); - if (!data || typeof data.username !== 'string' || typeof data.password !== 'string') { - socket.emit('registerResponse', { success: false, message: 'Некорректные данные запроса для регистрации.' }); - return; + console.log(`[Socket.IO] Register attempt for username: "${data?.username}" from ${socket.id}`); + const result = await auth.registerUser(data?.username, data?.password); + if (result.success) { + console.log(`[Socket.IO] Registration successful for ${result.username} (${result.userId})`); + } else { + console.warn(`[Socket.IO] Registration failed for "${data?.username}": ${result.message}`); } - const result = await authController.registerUser(data.username, data.password); socket.emit('registerResponse', result); }); socket.on('login', async (data) => { - console.log(`[Server BC.JS] Received 'login' event from ${socket.id} with username: ${data?.username}`); - if (!data || typeof data.username !== 'string' || typeof data.password !== 'string') { - socket.emit('loginResponse', { success: false, message: 'Некорректные данные запроса для входа.' }); - return; - } - const result = await authController.loginUser(data.username, data.password); + console.log(`[Socket.IO] Login attempt for username: "${data?.username}" from ${socket.id}`); + const result = await auth.loginUser(data?.username, data?.password); if (result.success) { - // Сохраняем данные пользователя в объекте сокета для последующего использования + console.log(`[Socket.IO] Login successful for ${result.username} (${result.userId}). Assigning to socket ${socket.id}.`); + // Сохраняем информацию о пользователе в сессии сокета socket.userData = { userId: result.userId, username: result.username }; - console.log(`[Server BC.JS] User ${result.username} (ID: ${result.userId}) associated with socket ${socket.id}. Welcome!`); + loggedInUsers[socket.id] = socket.userData; + + // Проверяем, есть ли у пользователя активная игра при логине (если он был отключен) + // ИСПРАВЛЕНИЕ: Передаем объект socket + gameManager.handleRequestGameState(socket, socket.userData.userId); + + } else { + console.warn(`[Socket.IO] Login failed for "${data?.username}": ${result.message}`); + socket.userData = null; // Убеждаемся, что данные пользователя на сокете сброшены + if (loggedInUsers[socket.id]) delete loggedInUsers[socket.id]; } socket.emit('loginResponse', result); }); socket.on('logout', () => { - const username = socket.userData?.username || socket.id; - console.log(`[Server BC.JS] Received 'logout' event from ${username}.`); - if (socket.userData) { - // При выходе пользователя, обрабатываем его возможное участие в играх - gameManager.handleDisconnect(socket.id, socket.userData.userId); // Используем userId для более точной обработки - delete socket.userData; // Удаляем данные пользователя из сокета - console.log(`[Server BC.JS] User data cleared for ${username}.`); - } - // Можно отправить подтверждение выхода, если нужно - // socket.emit('logoutResponse', { success: true, message: 'Вы успешно вышли.' }); + console.log(`[Socket.IO] Logout for user ${socket.userData?.username || socket.id}`); + // Уведомляем gameManager о дисконнекте (для корректного выхода из игры, если в ней был) + // Game Manager сам очистит ссылку socketToGame[socket.id] при handleDisconnect + // ИСПРАВЛЕНИЕ: Передаем userId или socket.id в handleDisconnect + gameManager.handleDisconnect(socket.id, socket.userData?.userId || socket.id); + + // Очищаем информацию о пользователе на сокете и в хранилище + socket.userData = null; + if (loggedInUsers[socket.id]) delete loggedInUsers[socket.id]; + + // Клиент должен сам переключиться на экран аутентификации }); - // --- Управление Играми --- + // --- Обработчики событий Управления Играми --- + socket.on('createGame', (data) => { - if (!socket.userData) { - socket.emit('gameError', { message: "Ошибка: Вы не авторизованы для создания игры." }); + // Пользователь, даже не залогиненный, может создать AI игру (идентифицируется по socket.id) + // Для PvP игры нужна аутентификация (идентификация по userId) + const identifier = socket.userData?.userId || socket.id; // Используем userId для залогиненных, socket.id для гостей + const mode = data?.mode || 'ai'; + + if (mode === 'pvp' && !socket.userData) { + socket.emit('gameError', { message: 'Необходимо войти в систему для создания PvP игры.' }); return; } - const mode = data?.mode || 'ai'; // 'ai' или 'pvp' - const characterKey = (data?.characterKey === 'almagest') ? 'almagest' : 'elena'; // По умолчанию Елена - console.log(`[Server BC.JS] User ${socket.userData.username} (socket: ${socket.id}) requests createGame. Mode: ${mode}, Character: ${characterKey}`); - gameManager.createGame(socket, mode, characterKey, socket.userData.userId); + + console.log(`[Socket.IO] Create Game request from ${socket.userData?.username || socket.id} (Identifier: ${identifier}). Mode: ${mode}, Character: ${data?.characterKey}`); + + const characterKey = data?.characterKey || 'elena'; // По умолчанию Елена + gameManager.createGame(socket, mode, characterKey, identifier); // Передаем идентификатор + }); socket.on('joinGame', (data) => { - if (!socket.userData) { - socket.emit('gameError', { message: "Ошибка: Вы не авторизованы для присоединения к игре." }); + if (!socket.userData) { // Проверяем, залогинен ли пользователь + socket.emit('gameError', { message: 'Необходимо войти в систему для присоединения к игре.' }); return; } - console.log(`[Server BC.JS] User ${socket.userData.username} (socket: ${socket.id}) requests joinGame for ID: ${data?.gameId}`); - if (data && typeof data.gameId === 'string') { - gameManager.joinGame(socket, data.gameId, socket.userData.userId); + console.log(`[Socket.IO] Join Game request from ${socket.userData.username} (${socket.id}). Game ID: ${data?.gameId}`); + const gameId = data?.gameId; + const identifier = socket.userData.userId; // Присоединиться может только залогиненный + + if (gameId) { + gameManager.joinGame(socket, gameId, identifier); // Передаем идентификатор } else { - socket.emit('gameError', { message: 'Ошибка присоединения: неверный формат ID игры.' }); + socket.emit('gameError', { message: 'Не указан ID игры для присоединения.' }); } }); socket.on('findRandomGame', (data) => { - if (!socket.userData) { - socket.emit('gameError', { message: "Ошибка: Вы не авторизованы для поиска игры." }); + if (!socket.userData) { // Проверяем, залогинен ли пользователь + socket.emit('gameError', { message: 'Необходимо войти в систему для поиска игры.' }); return; } - const characterKey = (data?.characterKey === 'almagest') ? 'almagest' : 'elena'; - console.log(`[Server BC.JS] User ${socket.userData.username} (socket: ${socket.id}) requests findRandomGame. Preferred Character: ${characterKey}`); - gameManager.findAndJoinRandomPvPGame(socket, characterKey, socket.userData.userId); + console.log(`[Socket.IO] Find Random Game request from ${socket.userData.username} (${socket.id}). Preferred Character: ${data?.characterKey}`); + const characterKey = data?.characterKey || 'elena'; // Предпочитаемый персонаж для создания, если не найдено + const identifier = socket.userData.userId; // Ищет и создает только залогиненный + + gameManager.findAndJoinRandomPvPGame(socket, characterKey, identifier); // Передаем идентификатор }); - // --- Игровые Действия --- - socket.on('playerAction', (data) => { + socket.on('requestPvPGameList', () => { + // Список игр доступен всем, даже не залогиненным, но присоединиться можно только залогиненным + // if (!socket.userData) { + // socket.emit('gameError', { message: 'Необходимо войти в систему для просмотра игр.' }); + // return; + // } + console.log(`[Socket.IO] Request PvP Game List from ${socket.userData?.username || socket.id}`); + const availableGames = gameManager.getAvailablePvPGamesListForClient(); + socket.emit('availablePvPGamesList', availableGames); + }); + + // Обработчик для клиента, запрашивающего состояние игры (например, при переподключении) + socket.on('requestGameState', () => { + // Запрашивать состояние игры может только залогиненный пользователь, т.к. только у них есть userId для идентификации if (!socket.userData) { - // Если пользователь не авторизован, но пытается совершить действие (маловероятно при правильной логике клиента) - socket.emit('gameError', { message: "Ошибка: Вы не авторизованы для совершения этого действия." }); + console.log(`[Socket.IO] Request Game State from unauthenticated socket ${socket.id}.`); + socket.emit('gameNotFound', { message: 'Необходимо войти для восстановления игры.' }); return; } - // GameManager сам проверит, принадлежит ли этот сокет к активной игре - gameManager.handlePlayerAction(socket.id, data); + console.log(`[Socket.IO] Request Game State from ${socket.userData.username} (${socket.id}).`); + // ИСПРАВЛЕНИЕ: Передаем объект socket и identifier (userId) + gameManager.handleRequestGameState(socket, socket.userData.userId); }); - // Обработчик 'requestRestart' удален, так как эта функциональность заменена на "возврат в меню" - // --- Отключение Клиента --- + // --- Обработчик события Игрового Действия --- + socket.on('playerAction', (actionData) => { + // Действие в игре может совершить как залогиненный (PvP), так и не залогиненный (AI) игрок. + // Используем userId для залогиненных, socket.id для гостей. + const identifier = socket.userData?.userId || socket.id; + + // Game Manager сам проверит, находится ли идентификатор в игре и его ли сейчас ход + // ИСПРАВЛЕНИЕ: Передаем идентификатор вместо socket.id + gameManager.handlePlayerAction(identifier, actionData); // Передаем идентификатор + }); + + + // --- Обработчик отключения сокета --- socket.on('disconnect', (reason) => { - const username = socket.userData?.username || socket.id; - console.log(`[Server BC.JS] Client ${username} disconnected. Reason: ${reason}. Socket ID: ${socket.id}`); - // Передаем userId, если он есть, для более точной обработки в GameManager - // (например, для удаления его ожидающих игр или корректного завершения активной игры) - const userId = socket.userData?.userId; - gameManager.handleDisconnect(socket.id, userId); - // socket.userData автоматически очистится для этого объекта socket при его удалении из io.sockets + const identifier = socket.userData?.userId || socket.id; // Используем userId для залогиненных, socket.id для гостей + console.log(`[Socket.IO] Пользователь отключился: ${socket.id} (Причина: ${reason}). Identifier: ${identifier}`); + + // Уведомляем gameManager о дисконнекте, чтобы он обновил состояние игры. + // Передаем идентификатор пользователя. + gameManager.handleDisconnect(socket.id, identifier); // Передаем как socketId, так и identifier + + // Удаляем пользователя из списка залогиненных, если был там + if (loggedInUsers[socket.id]) { + delete loggedInUsers[socket.id]; + } + // Если сокет не был залогинен, его identifier был socket.id. + // Связь userIdentifierToGameId будет очищена в gameManager.handleDisconnect, если игра пуста. }); - // Для отладки: вывод списка активных игр каждые N секунд - // setInterval(() => { - // console.log("--- Active Games ---"); - // const activeGames = gameManager.getActiveGamesList(); - // if (activeGames.length > 0) { - // activeGames.forEach(game => { - // console.log(`ID: ${game.id}, Mode: ${game.mode}, Players: ${game.playerCount}, GameOver: ${game.isGameOver}, P1: ${game.playerSlot}, P2: ${game.opponentSlot}, Owner: ${game.ownerUserId}, Pending: ${game.pending}`); - // }); - // } else { - // console.log("No active games."); - // } - // console.log("--- Pending PvP Games IDs ---"); - // console.log(gameManager.pendingPvPGames.map(id => id.substring(0,8))); - // console.log("--- User to Pending Game Map ---"); - // console.log(gameManager.userToPendingGame); - // console.log("---------------------"); - // }, 30000); // Каждые 30 секунд + // Опционально: отправка списка активных игр на сервере для отладки (по запросу с консоли или админки) + // global.getActiveGames = () => gameManager.getActiveGamesList(); + // console.log("Type getActiveGames() in server console to list games."); }); -server.listen(PORT, hostname, () => { - console.log(`==== Medieval Clash Server ====`); - console.log(` Listening on http://${hostname}:${PORT}`); - console.log(` Public files served from: ${path.join(__dirname, 'public')}`); - console.log(` Waiting for connections...`); - console.log(`===============================`); +// Запуск HTTP сервера +const PORT = process.env.PORT || 3200; // Использовать порт из переменных окружения или 3000 по умолчанию +server.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); + console.log(`Serving static files from: ${path.join(__dirname, 'public')}`); + // console.log("Database connection pool created/checked (from db.js require)."); // db.js уже логирует +}); + +// Обработка необработанных промис-ошибок +process.on('unhandledRejection', (reason, promise) => { + console.error('[UNHANDLED REJECTION] Unhandled Rejection at:', promise, 'reason:', reason); + // Логировать ошибку, возможно, завершить процесс в продакшене +}); + +process.on('uncaughtException', (err) => { + console.error('[UNCAUGHT EXCEPTION] Caught exception:', err); + // Логировать ошибку, выполнить очистку ресурсов, и завершить процесс + // В продакшене здесь может быть более сложная логика, например, graceful shutdown + // process.exit(1); // Аварийное завершение процесса - можно раскомментировать в продакшене }); \ No newline at end of file