const logger = require('../utils/logger'); const { getGameSystem } = require('../systems/GameSystem'); const Player = require('../models/Player'); class SocketHandlers { constructor(io) { this.io = io; 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(`[SOCKET HANDLERS] Checking ${this.connectedUsers.size} active connections...`); for (const [userId, socketId] of this.connectedUsers.entries()) { const socket = this.io.sockets.sockets.get(socketId); if (!socket || !socket.connected) { logger.warn(`[SOCKET HANDLERS] Cleaning up stale connection for user ${userId} (socket: ${socketId})`); this.connectedUsers.delete(userId); this.userSockets.delete(socketId); } } } handleConnection(socket) { logger.info(`Client connected: ${socket.id}`); // Authentication socket.on('authenticate', async (token) => { try { const jwt = require('jsonwebtoken'); const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret'); const player = await Player.findOne({ userId: decoded.userId }); if (!player) { socket.emit('auth_error', { error: 'Player not found' }); return; } // Check if user is already connected from another client const existingSocketId = this.connectedUsers.get(decoded.userId); if (existingSocketId && existingSocketId !== socket.id) { logger.warn(`[SOCKET HANDLERS] User ${decoded.userId} attempting to connect from multiple clients. Disconnecting previous client.`); logger.warn(`[SOCKET HANDLERS] Existing socket: ${existingSocketId}, New socket: ${socket.id}`); // Disconnect the previous client const previousSocket = this.io.sockets.sockets.get(existingSocketId); if (previousSocket) { logger.info(`[SOCKET HANDLERS] 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(`[SOCKET HANDLERS] Previous socket ${existingSocketId} not found in active connections`); // Clean up the stale mapping this.connectedUsers.delete(decoded.userId); this.userSockets.delete(existingSocketId); } } else { logger.info(`[SOCKET HANDLERS] 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); socket.userId = decoded.userId; socket.emit('authenticated', { userId: decoded.userId }); logger.info(`User authenticated: ${decoded.userId} (socket: ${socket.id})`); // Join user to their current server if any if (player.currentServer) { socket.join(player.currentServer); this.broadcastToServer(player.currentServer, 'user_joined', { userId: decoded.userId, username: player.username, socketId: socket.id }); } } catch (error) { logger.error('Authentication error:', error); socket.emit('auth_error', { error: 'Invalid token' }); } }); // Server management socket.on('join_server', async (data) => { try { if (!socket.userId) { socket.emit('error', { error: 'Not authenticated' }); return; } const gameSystem = getGameSystem(); const server = await gameSystem.joinServer(data.serverId, socket.userId); // Update player's current server await Player.findOneAndUpdate( { userId: socket.userId }, { currentServer: data.serverId } ); // Join socket room socket.join(data.serverId); socket.emit('server_joined', { server }); this.broadcastToServer(data.serverId, 'user_joined', { userId: socket.userId, serverId: data.serverId }); logger.info(`User ${socket.userId} joined server ${data.serverId}`); } catch (error) { logger.error('Error joining server:', error); socket.emit('error', { error: error.message }); } }); socket.on('leave_server', async (data) => { try { if (!socket.userId) { socket.emit('error', { error: 'Not authenticated' }); return; } const gameSystem = getGameSystem(); const server = await gameSystem.leaveServer(data.serverId, socket.userId); // Update player's current server await Player.findOneAndUpdate( { userId: socket.userId }, { currentServer: null } ); // Leave socket room socket.leave(data.serverId); socket.emit('server_left', { server }); this.broadcastToServer(data.serverId, 'user_left', { userId: socket.userId, serverId: data.serverId }); logger.info(`User ${socket.userId} left server ${data.serverId}`); } catch (error) { logger.error('Error leaving server:', error); socket.emit('error', { error: error.message }); } }); // Game actions socket.on('game_action', async (data) => { try { if (!socket.userId) { socket.emit('error', { error: 'Not authenticated' }); return; } const gameSystem = getGameSystem(); const result = await gameSystem.processGameAction(socket.userId, data); socket.emit('action_result', { action: data.type, result }); // Broadcast relevant actions to server if (data.broadcast && socket.userId) { const player = await Player.findOne({ userId: socket.userId }); if (player && player.currentServer) { this.broadcastToServer(player.currentServer, 'user_action', { userId: socket.userId, username: player.username, action: data.type, result }); } } } catch (error) { logger.error('Error processing game action:', error); socket.emit('error', { error: error.message }); } }); // Chat functionality socket.on('send_message', async (data) => { try { if (!socket.userId) { socket.emit('error', { error: 'Not authenticated' }); return; } const player = await Player.findOne({ userId: socket.userId }); if (!player || !player.currentServer) { socket.emit('error', { error: 'Not in a server' }); return; } const message = { userId: socket.userId, username: player.username, message: data.message, timestamp: new Date(), type: data.type || 'chat' }; // Broadcast to server this.broadcastToServer(player.currentServer, 'new_message', message); logger.info(`Chat message from ${socket.userId} in server ${player.currentServer}`); } catch (error) { logger.error('Error sending message:', error); socket.emit('error', { error: error.message }); } }); // Real-time updates socket.on('request_server_status', async () => { try { if (!socket.userId) { socket.emit('error', { error: 'Not authenticated' }); return; } const player = await Player.findOne({ userId: socket.userId }); if (!player || !player.currentServer) { socket.emit('server_status', { server: null }); return; } const gameSystem = getGameSystem(); const server = gameSystem.servers.get(player.currentServer); if (server) { const players = await Player.find({ userId: { $in: server.players } }).select('userId username info.stats.level'); socket.emit('server_status', { server: { id: server.id, name: server.name, currentPlayers: server.players.length, maxPlayers: server.maxPlayers, players: players.map(p => ({ userId: p.userId, username: p.username, level: p.info.stats.level })) } }); } } catch (error) { logger.error('Error getting server status:', error); socket.emit('error', { error: error.message }); } }); // Disconnection socket.on('disconnect', async () => { logger.info(`Client disconnected: ${socket.id}`); const userId = this.userSockets.get(socket.id); if (userId) { logger.info(`User ${userId} disconnected (socket: ${socket.id})`); // Remove from tracking maps this.connectedUsers.delete(userId); this.userSockets.delete(socket.id); // Update player's online status try { const player = await Player.findOne({ userId }); if (player) { // Notify server if user was in one if (player.currentServer) { this.broadcastToServer(player.currentServer, 'user_disconnected', { userId, username: player.username, socketId: socket.id }); // Leave the server room socket.leave(player.currentServer); } logger.info(`User ${userId} fully disconnected and cleaned up`); } } catch (error) { logger.error(`Error cleaning up user ${userId} on disconnect:`, error); } } else { logger.warn(`Unknown socket ${socket.id} disconnected without user mapping`); } }); } broadcastToServer(serverId, event, data) { this.io.to(serverId).emit(event, data); } sendToUser(userId, event, data) { const socketId = this.connectedUsers.get(userId); if (socketId) { this.io.to(socketId).emit(event, data); } } // Method to check for duplicate accounts async checkForDuplicateAccounts() { try { const Player = require('../models/Player'); const allPlayers = await Player.find({}, 'userId email username'); const duplicateEmails = []; const duplicateUsernames = []; const emailMap = new Map(); const usernameMap = new Map(); allPlayers.forEach(player => { if (emailMap.has(player.email)) { duplicateEmails.push({ email: player.email, user1: emailMap.get(player.email), user2: player.userId }); } else { emailMap.set(player.email, player.userId); } if (usernameMap.has(player.username)) { duplicateUsernames.push({ username: player.username, user1: usernameMap.get(player.username), user2: player.userId }); } else { usernameMap.set(player.username, player.userId); } }); if (duplicateEmails.length > 0) { logger.error(`[SOCKET HANDLERS] Found ${duplicateEmails.length} duplicate emails in database:`, duplicateEmails); } if (duplicateUsernames.length > 0) { logger.error(`[SOCKET HANDLERS] Found ${duplicateUsernames.length} duplicate usernames in database:`, duplicateUsernames); } logger.info(`[SOCKET HANDLERS] Account check complete: ${allPlayers.length} total players, ${duplicateEmails.length} duplicate emails, ${duplicateUsernames.length} duplicate usernames`); return { totalPlayers: allPlayers.length, duplicateEmails, duplicateUsernames }; } catch (error) { logger.error('[SOCKET HANDLERS] Error checking for duplicate accounts:', error); return { error: error.message }; } } // Method to get connection statistics getConnectionStats() { return { connectedUsers: this.connectedUsers.size, userSockets: this.userSockets.size, activeSockets: this.io.engine.clientsCount, connections: Array.from(this.connectedUsers.entries()).map(([userId, socketId]) => ({ userId, socketId, isActive: !!this.io.sockets.sockets.get(socketId) })) }; } broadcastToAll(event, data) { this.io.emit(event, data); } getConnectedUsers() { return Array.from(this.connectedUsers.keys()); } getUserCount() { return this.connectedUsers.size; } } module.exports = SocketHandlers;