240 lines
11 KiB
JavaScript
240 lines
11 KiB
JavaScript
// /server/bc.js - Главный файл сервера Battle Club
|
|
|
|
require('dotenv').config({ path: require('node:path').resolve(process.cwd(), '.env') });
|
|
|
|
const express = require('express');
|
|
const http = require('http');
|
|
const { Server } = require('socket.io');
|
|
const path = require('path');
|
|
const jwt = require('jsonwebtoken');
|
|
const cors = require('cors');
|
|
|
|
// Импорт серверных модулей
|
|
const authService = require('./auth/authService');
|
|
const GameManager = require('./game/GameManager');
|
|
const db = require('./core/db');
|
|
const GAME_CONFIG = require('./core/config');
|
|
|
|
const app = express();
|
|
const server = http.createServer(app);
|
|
|
|
// --- НАСТРОЙКА EXPRESS ---
|
|
const clientOrigin = process.env.CORS_ORIGIN_CLIENT || (process.env.NODE_ENV === 'development' ? '*' : undefined);
|
|
if (!clientOrigin && process.env.NODE_ENV !== 'development') {
|
|
console.warn("[Server Config] CORS_ORIGIN_CLIENT не установлен для продакшн сборки. HTTP API могут быть недоступны.");
|
|
}
|
|
|
|
app.use(cors({
|
|
origin: clientOrigin,
|
|
methods: ["GET", "POST"],
|
|
}));
|
|
|
|
app.use(express.json());
|
|
app.use(express.static(path.join(__dirname, '..', 'public')));
|
|
|
|
// --- HTTP МАРШРУТЫ АУТЕНТИФИКАЦИИ ---
|
|
app.post('/auth/register', async (req, res) => {
|
|
const { username, password } = req.body;
|
|
console.log(`[HTTP /auth/register] Attempt for username: "${username}"`);
|
|
if (!username || !password) {
|
|
return res.status(400).json({ success: false, message: 'Имя пользователя и пароль обязательны.' });
|
|
}
|
|
const result = await authService.registerUser(username, password);
|
|
if (result.success) {
|
|
res.status(201).json(result);
|
|
} else {
|
|
res.status(400).json(result);
|
|
}
|
|
});
|
|
|
|
app.post('/auth/login', async (req, res) => {
|
|
const { username, password } = req.body;
|
|
console.log(`[HTTP /auth/login] Attempt for username: "${username}"`);
|
|
if (!username || !password) {
|
|
return res.status(400).json({ success: false, message: 'Имя пользователя и пароль обязательны.' });
|
|
}
|
|
const result = await authService.loginUser(username, password);
|
|
if (result.success) {
|
|
res.json(result);
|
|
} else {
|
|
res.status(401).json(result);
|
|
}
|
|
});
|
|
// ------------------------------
|
|
|
|
const socketCorsOrigin = process.env.CORS_ORIGIN_SOCKET || (process.env.NODE_ENV === 'development' ? '*' : undefined);
|
|
if (!socketCorsOrigin && process.env.NODE_ENV !== 'development') {
|
|
console.warn("[Server Config] CORS_ORIGIN_SOCKET не установлен для продакшн сборки. Socket.IO может быть недоступен.");
|
|
}
|
|
|
|
const io = new Server(server, {
|
|
cors: {
|
|
origin: socketCorsOrigin,
|
|
methods: ["GET", "POST"]
|
|
},
|
|
});
|
|
|
|
const gameManager = new GameManager(io);
|
|
const loggedInUsers = {};
|
|
|
|
// --- MIDDLEWARE АУТЕНТИФИКАЦИИ SOCKET.IO ---
|
|
io.use(async (socket, next) => {
|
|
const token = socket.handshake.auth.token;
|
|
console.log(`[Socket.IO Middleware] Attempting to auth socket ${socket.id}. Token ${token ? 'present' : 'absent'}.`);
|
|
|
|
if (token) {
|
|
try {
|
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
socket.userData = { userId: decoded.userId, username: decoded.username };
|
|
console.log(`[Socket.IO Middleware] Socket ${socket.id} authenticated for user ${decoded.username} (ID: ${decoded.userId}).`);
|
|
return next();
|
|
} catch (err) {
|
|
console.warn(`[Socket.IO Middleware] Socket ${socket.id} auth failed: Invalid token. Error: ${err.message}`);
|
|
}
|
|
} else {
|
|
console.log(`[Socket.IO Middleware] Socket ${socket.id} has no token. Proceeding as unauthenticated.`);
|
|
}
|
|
next();
|
|
});
|
|
// ------------------------------------
|
|
|
|
io.on('connection', (socket) => {
|
|
if (socket.userData && socket.userData.userId) {
|
|
console.log(`[Socket.IO] Authenticated user ${socket.userData.username} (ID: ${socket.userData.userId}) connected: ${socket.id}`);
|
|
loggedInUsers[socket.id] = socket.userData;
|
|
if (gameManager && typeof gameManager.handleRequestGameState === 'function') {
|
|
gameManager.handleRequestGameState(socket, socket.userData.userId);
|
|
}
|
|
} else {
|
|
console.log(`[Socket.IO] Unauthenticated user connected: ${socket.id}. No game state will be restored.`);
|
|
}
|
|
|
|
socket.on('logout', () => {
|
|
const username = socket.userData?.username || 'UnknownUser';
|
|
const userId = socket.userData?.userId;
|
|
console.log(`[Socket.IO] Logout request from user ${username} (ID: ${userId}, Socket: ${socket.id})`);
|
|
// GameManager.handleDisconnect будет вызван автоматически при событии 'disconnect'
|
|
// Если игрок нажал "выход", но еще в игре, клиент пошлет 'playerSurrender' ДО этого.
|
|
// Здесь просто очищаем локальные данные сессии для сокета.
|
|
if (loggedInUsers[socket.id]) {
|
|
delete loggedInUsers[socket.id];
|
|
}
|
|
socket.userData = null;
|
|
console.log(`[Socket.IO] User ${username} (Socket: ${socket.id}) logged out (client-side action).`);
|
|
});
|
|
|
|
// --- НАЧАЛО ИЗМЕНЕНИЯ ---
|
|
socket.on('playerSurrender', () => {
|
|
if (!socket.userData?.userId) {
|
|
socket.emit('gameError', { message: 'Необходимо войти в систему, чтобы сдаться в игре.' });
|
|
return;
|
|
}
|
|
const identifier = socket.userData.userId;
|
|
const username = socket.userData.username;
|
|
console.log(`[Socket.IO] Player Surrender request from user ${username} (ID: ${identifier}, Socket: ${socket.id})`);
|
|
if (gameManager && typeof gameManager.handlePlayerSurrender === 'function') {
|
|
gameManager.handlePlayerSurrender(identifier);
|
|
} else {
|
|
console.error("[Socket.IO] CRITICAL: gameManager or handlePlayerSurrender method not found!");
|
|
socket.emit('gameError', { message: 'Ошибка сервера при обработке сдачи игры.' });
|
|
}
|
|
// После этого клиент выполнит logout, что вызовет 'disconnect' и GameManager.handleDisconnect
|
|
});
|
|
// --- КОНЕЦ ИЗМЕНЕНИЯ ---
|
|
|
|
socket.on('createGame', (data) => {
|
|
if (!socket.userData?.userId) {
|
|
socket.emit('gameError', { message: 'Необходимо войти в систему для создания игры.' });
|
|
return;
|
|
}
|
|
const identifier = socket.userData.userId;
|
|
const mode = data?.mode || 'ai';
|
|
console.log(`[Socket.IO] Create Game from ${socket.userData.username} (ID: ${identifier}). Mode: ${mode}, Char: ${data?.characterKey}`);
|
|
gameManager.createGame(socket, mode, data?.characterKey, identifier);
|
|
});
|
|
|
|
socket.on('joinGame', (data) => {
|
|
if (!socket.userData?.userId) {
|
|
socket.emit('gameError', { message: 'Необходимо войти для присоединения к PvP игре.' });
|
|
return;
|
|
}
|
|
console.log(`[Socket.IO] Join Game from ${socket.userData.username} (ID: ${socket.userData.userId}). GameID: ${data?.gameId}`);
|
|
gameManager.joinGame(socket, data?.gameId, socket.userData.userId);
|
|
});
|
|
|
|
socket.on('findRandomGame', (data) => {
|
|
if (!socket.userData?.userId) {
|
|
socket.emit('gameError', { message: 'Необходимо войти для поиска случайной PvP игры.' });
|
|
return;
|
|
}
|
|
console.log(`[Socket.IO] Find Random Game from ${socket.userData.username} (ID: ${socket.userData.userId}). PrefChar: ${data?.characterKey}`);
|
|
gameManager.findAndJoinRandomPvPGame(socket, data?.characterKey, socket.userData.userId);
|
|
});
|
|
|
|
socket.on('requestPvPGameList', () => {
|
|
const availableGames = gameManager.getAvailablePvPGamesListForClient();
|
|
socket.emit('availablePvPGamesList', availableGames);
|
|
});
|
|
|
|
socket.on('requestGameState', () => {
|
|
if (!socket.userData?.userId) {
|
|
socket.emit('gameNotFound', { message: 'Необходимо войти для восстановления игры.' });
|
|
return;
|
|
}
|
|
gameManager.handleRequestGameState(socket, socket.userData.userId);
|
|
});
|
|
|
|
socket.on('playerAction', (actionData) => {
|
|
if (!socket.userData?.userId) {
|
|
socket.emit('gameError', { message: 'Действие не разрешено: пользователь не аутентифицирован.' });
|
|
return;
|
|
}
|
|
const identifier = socket.userData.userId;
|
|
gameManager.handlePlayerAction(identifier, actionData);
|
|
});
|
|
|
|
socket.on('disconnect', (reason) => {
|
|
const identifier = socket.userData?.userId;
|
|
const username = socket.userData?.username || 'UnauthenticatedUser';
|
|
console.log(`[Socket.IO] User ${username} (ID: ${identifier || 'N/A'}, Socket: ${socket.id}) disconnected. Reason: ${reason}.`);
|
|
if (identifier) {
|
|
gameManager.handleDisconnect(socket.id, identifier);
|
|
}
|
|
if (loggedInUsers[socket.id]) {
|
|
delete loggedInUsers[socket.id];
|
|
}
|
|
});
|
|
});
|
|
|
|
const PORT = parseInt(process.env.BC_APP_PORT || '3200', 10);
|
|
const HOSTNAME = process.env.BC_APP_HOSTNAME || '127.0.0.1';
|
|
|
|
if (isNaN(PORT)) {
|
|
console.error(`[Server FATAL] Некорректное значение для BC_APP_PORT: "${process.env.BC_APP_PORT}". Ожидается число.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
server.listen(PORT, HOSTNAME, () => {
|
|
console.log(`Battle Club HTTP Application Server running at http://${HOSTNAME}:${PORT}`);
|
|
if (HOSTNAME === '127.0.0.1') {
|
|
console.log(`Server is listening on localhost only. This is suitable if a reverse proxy handles external traffic.`);
|
|
} else if (HOSTNAME === '0.0.0.0') {
|
|
console.log(`Server is listening on all available network interfaces.`);
|
|
} else {
|
|
console.log(`Server is listening on a specific interface: ${HOSTNAME}.`);
|
|
}
|
|
console.log(`Environment (NODE_ENV): ${process.env.NODE_ENV || 'not set (defaults to development behavior)'}`);
|
|
console.log(`Serving static files from: ${path.join(__dirname, '..', 'public')}`);
|
|
console.log(`Client HTTP requests should target: http://${HOSTNAME}:${PORT}`);
|
|
console.log(`Socket.IO CORS origin set to: ${socketCorsOrigin || 'Not explicitly set, defaults may apply'}`);
|
|
console.log(`HTTP API CORS origin set to: ${clientOrigin || 'Not explicitly set, defaults may apply'}`);
|
|
});
|
|
|
|
process.on('unhandledRejection', (reason, promise) => {
|
|
console.error('[Server FATAL] Unhandled Rejection at:', promise, 'reason:', reason);
|
|
});
|
|
|
|
process.on('uncaughtException', (err) => {
|
|
console.error('[Server FATAL] Uncaught Exception:', err);
|
|
process.exit(1);
|
|
}); |