394 lines
14 KiB
JavaScript
394 lines
14 KiB
JavaScript
/**
|
|
* 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;
|