/** * Socket Handlers - Manages real-time socket connections for game server */ const jwt = require('jsonwebtoken'); const logger = require('../utils/logger'); const { getGameSystem } = require('../systems/GameSystem'); class SocketHandlers { constructor(io, gameServers, connectedPlayers) { this.io = io; this.gameServers = gameServers; this.connectedPlayers = connectedPlayers; // Track actual player connections this.gameSystem = null; // Track connected users to prevent duplicate connections this.connectedUsers = new Map(); // userId -> socket.id this.userSockets = new Map(); // socket.id -> userId // Add connection cleanup interval this.startConnectionCleanup(); } startConnectionCleanup() { // Clean up stale connections every 30 seconds setInterval(() => { this.cleanupStaleConnections(); }, 30000); } cleanupStaleConnections() { logger.info(`[GAME SERVER] Checking ${this.connectedUsers.size} active connections...`); logger.info(`[GAME SERVER] Current tracked players: ${Array.from(this.connectedPlayers)}`); let playersRemoved = 0; for (const [userId, socketId] of this.connectedUsers.entries()) { const socket = this.io.sockets.sockets.get(socketId); if (!socket || !socket.connected) { logger.warn(`[GAME SERVER] Cleaning up stale connection for user ${userId} (socket: ${socketId})`); this.connectedUsers.delete(userId); this.userSockets.delete(socketId); if (this.connectedPlayers.has(userId)) { this.connectedPlayers.delete(userId); playersRemoved++; logger.info(`[GAME SERVER] Removed stale player ${userId}. Players removed: ${playersRemoved}`); } } } logger.info(`[GAME SERVER] Cleanup complete. Players removed: ${playersRemoved}, Total players now: ${this.connectedPlayers.size}`); // Update player count on API if players were removed if (playersRemoved > 0 && this.serverRegistration) { logger.info(`[GAME SERVER] Updating API player count after cleanup to: ${this.connectedPlayers.size}`); this.serverRegistration.updatePlayerCount(this.connectedPlayers.size); } } async initializeGameSystem() { const { initializeGameSystems } = require('../systems/GameSystem'); this.gameSystem = await initializeGameSystems(); } handleConnection(socket) { logger.info(`Game Server: Socket connected - ${socket.id}`); // Authentication middleware socket.use(async (packet, next) => { try { const token = socket.handshake.auth.token; if (!token) { return next(new Error('Authentication required')); } // Verify JWT token const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret'); socket.userId = decoded.userId; socket.email = decoded.email; // Check if user is already connected from another client const existingSocketId = this.connectedUsers.get(decoded.userId); const wasAlreadyConnected = this.connectedPlayers.has(decoded.userId); logger.info(`[GAME SERVER] User ${decoded.userId} (${decoded.email}) connecting. Was already connected: ${wasAlreadyConnected}, Existing socket: ${existingSocketId}`); if (existingSocketId && existingSocketId !== socket.id) { logger.warn(`[GAME SERVER] User ${decoded.userId} attempting to connect from multiple clients. Disconnecting previous client.`); logger.warn(`[GAME SERVER] Existing socket: ${existingSocketId}, New socket: ${socket.id}`); // Disconnect the previous client const previousSocket = this.io.sockets.sockets.get(existingSocketId); if (previousSocket) { logger.info(`[GAME SERVER] Force disconnecting previous socket: ${existingSocketId}`); previousSocket.emit('force_disconnect', { reason: 'Another client connected with your account', newSocketId: socket.id }); previousSocket.disconnect(true); } else { logger.warn(`[GAME SERVER] Previous socket ${existingSocketId} not found in active connections`); // Clean up the stale mapping this.connectedUsers.delete(decoded.userId); this.userSockets.delete(existingSocketId); if (!wasAlreadyConnected) { this.connectedPlayers.delete(decoded.userId); } } } else { logger.info(`[GAME SERVER] New connection for user ${decoded.userId} (socket: ${socket.id})`); } // Store user connection this.connectedUsers.set(decoded.userId, socket.id); this.userSockets.set(socket.id, decoded.userId); // Add to connected players tracking only if not already there const wasNotAlreadyConnected = !this.connectedPlayers.has(decoded.userId); this.connectedPlayers.add(decoded.userId); logger.info(`[GAME SERVER] User ${decoded.userId} added to tracking. Was not already connected: ${wasNotAlreadyConnected}, Total players: ${this.connectedPlayers.size}`); // Update player count on API only if this is a new unique user if (this.serverRegistration && wasNotAlreadyConnected) { logger.info(`[GAME SERVER] Updating API player count to: ${this.connectedPlayers.size}`); this.serverRegistration.updatePlayerCount(this.connectedPlayers.size); } next(); } catch (error) { logger.error(`Socket authentication failed: ${error.message}`); socket.emit('authError', { error: 'Authentication failed' }); socket.disconnect(); } }); // Event handlers socket.on('joinServer', (data) => this.handleJoinServer(socket, data)); socket.on('leaveServer', (data) => this.handleLeaveServer(socket, data)); socket.on('gameAction', (data) => this.handleGameAction(socket, data)); socket.on('chatMessage', (data) => this.handleChatMessage(socket, data)); socket.on('getPlayerList', (data) => this.handleGetPlayerList(socket, data)); socket.on('disconnect', () => this.handleDisconnect(socket)); } async handleJoinServer(socket, data) { try { const { serverId, userId, username } = data; // Verify user matches socket authentication if (socket.userId !== userId) { socket.emit('error', { message: 'User authentication mismatch' }); return; } // Create or get game instance let gameInstance = this.gameSystem.getGameInstance(serverId); if (!gameInstance) { // This should ideally be handled by the API server // But for now, create a basic game instance gameInstance = this.gameSystem.createGameInstance({ id: serverId, name: `Game ${serverId}`, type: 'public', region: 'us-east', maxPlayers: 10 }); } // Join player to game instance const playerData = { userId: userId, username: username, currentShip: data.currentShip, stats: data.stats }; const joinedGame = this.gameSystem.joinGameInstance(socket, serverId, playerData); if (joinedGame) { // Notify player of successful join socket.emit('joinedServer', { serverId: serverId, gameInstance: { id: joinedGame.id, name: joinedGame.name } }); // Notify other players socket.to(`game_${gameId}`).emit('playerJoined', { userId: socket.userId, username: socket.email || 'Player' }); logger.info(`Player ${socket.userId} joined game ${gameId}`); } else { socket.emit('error', { message: 'Failed to join game' }); } } catch (error) { logger.error(`Join game error: ${error.message}`); socket.emit('error', { message: 'Failed to join game' }); } } async handleLeaveGame(socket, data) { try { const { gameId } = data; const leftGame = this.gameSystem.leaveGameInstance(socket, gameId); if (leftGame) { // Notify player of successful leave socket.emit('leftGame', { gameId: gameId }); // Notify other players socket.to(`game_${gameId}`).emit('playerLeft', { userId: socket.userId, currentPlayers: leftGame.currentPlayers }); logger.info(`Player left game ${gameId}`); } else { socket.emit('error', { message: 'Failed to leave game' }); } } catch (error) { logger.error(`Leave game error: ${error.message}`); socket.emit('error', { message: 'Failed to leave game' }); } } async handleGameAction(socket, data) { try { const { type, actionData } = data; // Handle game action through game system const success = this.gameSystem.handlePlayerAction(socket, type, actionData); if (!success) { socket.emit('error', { message: 'Failed to process game action' }); } } catch (error) { logger.error(`Game action error: ${error.message}`); socket.emit('error', { message: 'Failed to process game action' }); } } async handleChatMessage(socket, data) { try { const { message } = data; // Handle chat through game system const success = this.gameSystem.handlePlayerChat(socket, { message }); if (!success) { socket.emit('error', { message: 'Failed to send chat message' }); } } catch (error) { logger.error(`Chat message error: ${error.message}`); socket.emit('error', { message: 'Failed to send chat message' }); } } async handleGetPlayerList(socket, data) { try { const { serverId } = data; this.sendPlayerList(socket, serverId); } catch (error) { logger.error(`Get player list error: ${error.message}`); socket.emit('error', { message: 'Failed to get player list' }); } } async sendPlayerList(socket, serverId) { const gameInstance = this.gameSystem.getGameInstance(serverId); if (gameInstance) { const players = Array.from(gameInstance.players.values()).map(player => ({ userId: player.userId, username: player.username, joinedAt: player.joinedAt, isReady: player.isReady, stats: player.stats })); socket.emit('playerList', { serverId: serverId, players: players, currentPlayers: gameInstance.currentPlayers, maxPlayers: gameInstance.maxPlayers }); } else { socket.emit('error', { message: 'Game instance not found' }); } } async handleDisconnect(socket) { try { logger.info(`Game Server: Socket disconnected - ${socket.id}`); // Get user ID from socket const userId = this.userSockets.get(socket.id); if (userId) { logger.info(`[GAME SERVER] User ${userId} disconnecting (socket: ${socket.id})`); // Remove from tracking maps this.connectedUsers.delete(userId); this.userSockets.delete(socket.id); // Remove from connected players tracking const wasTracked = this.connectedPlayers.has(userId); this.connectedPlayers.delete(userId); logger.info(`[GAME SERVER] User ${userId} removed from tracking. Was tracked: ${wasTracked}, Total players: ${this.connectedPlayers.size}`); // Update player count on API only if user was being tracked if (this.serverRegistration && wasTracked) { logger.info(`[GAME SERVER] Updating API player count to: ${this.connectedPlayers.size}`); this.serverRegistration.updatePlayerCount(this.connectedPlayers.size); } // Get player connection info const connection = this.gameSystem.getPlayerConnection(socket.id); if (connection && connection.gameId) { const gameId = connection.gameId; // Remove player from game const success = this.gameSystem.removePlayerFromGame(gameId, userId); if (success) { // Notify other players in the game const game = this.gameSystem.getGame(gameId); if (game) { this.io.to(gameId).emit('playerLeft', { userId, username: socket.email || 'Unknown', gameId, currentPlayers: game.currentPlayers }); } logger.info(`Player ${userId} disconnected from game ${gameId}`); } if (success) { // Notify other players this.io.to(`game_${gameId}`).emit('playerLeft', { userId: socket.userId, currentPlayers: game.currentPlayers }); logger.info(`Player ${socket.userId} disconnected from game ${gameId}`); } } logger.info(`[GAME SERVER] User ${userId} fully disconnected and cleaned up`); } else { logger.warn(`[GAME SERVER] Unknown socket ${socket.id} disconnected without user mapping`); } } catch (error) { logger.error(`[GAME SERVER] Disconnect error: ${error.message}`); } } // Server management methods getServerStatus() { const gameInstances = this.gameSystem.getAllGameInstances(); return { activeServers: gameInstances.length, connectedPlayers: this.connectedPlayers.size, }; } getConnectedUsers() { return Array.from(this.connectedUsers.keys()); } getUserCount() { return this.connectedUsers.size; } broadcastToAll(event, data) { this.io.emit(event, data); } broadcastToServer(serverId, event, data) { this.io.to(`game_${serverId}`).emit(event, data); } } module.exports = SocketHandlers;