Game-Server/Server/socket/socketHandlers.js
2026-01-24 16:47:19 -04:00

407 lines
13 KiB
JavaScript

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;