407 lines
13 KiB
JavaScript
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;
|