const express = require("express"); const { Server } = require("socket.io"); const http = require("http"); const app = express(); const path = require("path"); const GameManager = require("./game"); app.use(express.static(path.join(__dirname, '../client/dist'))); app.disable("x-powered-by"); app.get('*', (req, res) => res.sendFile(path.join(__dirname, '../client/dist', 'index.html'))); const server = http.createServer(app); const io = new Server(server, { cors: {origin: "*"}, pingTimeout: 30000, pingInterval: 10000 }); // Initialize game manager const gameManager = new GameManager(); // Socket.IO event handlers io.on('connection', (socket) => { console.log(`User connected: ${socket.id}`); // Create a new game lobby socket.on('create_lobby', ({ playerName }, callback) => { try { const result = gameManager.createLobby(socket.id, playerName); // Join the socket room for this lobby socket.join(result.lobbyId); // Send response to client if (callback) callback(result); console.log(`Lobby created: ${result.lobbyId} by ${playerName}`); } catch (error) { console.error('Error creating lobby:', error); if (callback) callback({ error: 'Failed to create lobby' }); } }); // Join an existing lobby socket.on('join_lobby', ({ lobbyId, playerName }, callback) => { try { const result = gameManager.joinLobby(socket.id, playerName, lobbyId); if (result.error) { if (callback) callback(result); return; } // Join the socket room for this lobby socket.join(lobbyId); // Notify all players in the lobby socket.to(lobbyId).emit('player_joined', { playerId: socket.id, playerName }); // Send response to client if (callback) callback(result); console.log(`Player ${playerName} joined lobby ${lobbyId}`); } catch (error) { console.error('Error joining lobby:', error); if (callback) callback({ error: 'Failed to join lobby' }); } }); // Attempt to reconnect to a lobby socket.on('reconnect_to_lobby', ({ lobbyId, playerName, lastKnownState }, callback) => { try { const result = gameManager.handleReconnect(socket.id, lobbyId, playerName, lastKnownState); if (result.error) { if (callback) callback(result); return; } // Join the socket room for this lobby socket.join(lobbyId); // Notify all players in the lobby socket.to(lobbyId).emit('player_reconnected', { playerId: socket.id, playerName }); // Send response to client if (callback) callback(result); console.log(`Player ${playerName} reconnected to lobby ${lobbyId}`); } catch (error) { console.error('Error reconnecting to lobby:', error); if (callback) callback({ error: 'Failed to reconnect to lobby' }); } }); // Update lobby settings socket.on('update_settings', ({ settings }, callback) => { try { const result = gameManager.updateSettings(socket.id, settings); if (result.error) { if (callback) callback(result); return; } // Notify all players in the lobby io.to(result.lobbyId).emit('settings_updated', { settings: result.lobby.settings }); // Send response to client if (callback) callback(result); } catch (error) { console.error('Error updating settings:', error); if (callback) callback({ error: 'Failed to update settings' }); } }); // Start the game from lobby socket.on('start_game', (_, callback) => { try { const result = gameManager.startGame(socket.id); if (result.error) { if (callback) callback(result); return; } // Notify all players in the lobby io.to(result.lobbyId).emit('game_started', { state: result.lobby.state }); // Send response to client if (callback) callback(result); console.log(`Game started in lobby ${result.lobbyId}`); } catch (error) { console.error('Error starting game:', error); if (callback) callback({ error: 'Failed to start game' }); } }); // Add a song socket.on('add_song', async ({ song }, callback) => { try { console.log(`[DEBUG] Attempting to add song: ${JSON.stringify(song)}`); // Fix: Properly await the result of the async addSong function const result = await gameManager.addSong(socket.id, song); if (result.error) { console.log(`[DEBUG] Error adding song: ${result.error}`); if (callback) callback(result); return; } // Add better error handling to prevent crash if lobby is not defined if (!result.lobby || !result.lobbyId) { console.log(`[DEBUG] Warning: Song added but lobby information is missing`); if (callback) callback({ error: 'Failed to associate song with lobby' }); return; } console.log(`[DEBUG] Song added successfully, notifying lobby ${result.lobbyId}`); console.log(`[DEBUG] Lobby songs: ${JSON.stringify(result.lobby.songs.map(s => s.title))}`); // Notify all players in the lobby about updated song count io.to(result.lobbyId).emit('songs_updated', { lobby: result.lobby }); // Send response to client if (callback) callback(result); } catch (error) { console.error('Error adding song:', error); if (callback) callback({ error: 'Failed to add song' }); } }); // Remove a song socket.on('remove_song', ({ songId }, callback) => { try { const result = gameManager.removeSong(socket.id, songId); if (result.error) { if (callback) callback(result); return; } // Notify all players in the lobby io.to(result.lobbyId).emit('songs_updated', { lobby: result.lobby }); // Send response to client if (callback) callback(result); } catch (error) { console.error('Error removing song:', error); if (callback) callback({ error: 'Failed to remove song' }); } }); // Search YouTube for songs socket.on('search_youtube', async ({ query }, callback) => { try { if (!query || typeof query !== 'string') { if (callback) callback({ error: 'Invalid search query' }); return; } const results = await gameManager.searchYouTube(query); // Send response to client if (callback) callback({ results }); } catch (error) { console.error('Error searching YouTube:', error); if (callback) callback({ error: 'Failed to search YouTube' }); } }); // Get metadata for a single YouTube video socket.on('get_video_metadata', async ({ videoId }, callback) => { try { if (!videoId || typeof videoId !== 'string') { if (callback) callback({ error: 'Invalid video ID' }); return; } const metadata = await gameManager.getYouTubeVideoMetadata(videoId); // Send response to client if (callback) callback({ metadata }); } catch (error) { console.error('Error getting YouTube metadata:', error); if (callback) callback({ error: 'Failed to get YouTube metadata' }); } }); // Mark player as ready socket.on('player_ready', (_, callback) => { try { const result = gameManager.setPlayerReady(socket.id); if (result.error) { if (callback) callback(result); return; } // Notify all players in the lobby io.to(result.lobbyId).emit('player_status_changed', { lobby: result.lobby }); // If the game state just changed to VOTING, notify about the first battle if (result.lobby.state === 'VOTING' && result.lobby.currentBattle) { console.log('Sending new_battle event to clients'); io.to(result.lobbyId).emit('new_battle', { battle: result.lobby.currentBattle }); } // If tournament just started, explicitly notify about the state change and first battle if (result.tournamentStarted && result.lobby.currentBattle) { console.log('Tournament started, sending battle data'); // First send state change io.to(result.lobbyId).emit('tournament_started', { state: 'VOTING' }); // Then send battle data with a slight delay to ensure clients process in the right order setTimeout(() => { io.to(result.lobbyId).emit('new_battle', { battle: result.lobby.currentBattle }); }, 300); } // If game finished (edge case where only one song was submitted) if (result.lobby.state === 'FINISHED') { io.to(result.lobbyId).emit('game_finished', { winner: result.lobby.finalWinner, battles: result.lobby.battles }); } // Send response to client if (callback) callback(result); } catch (error) { console.error('Error setting player ready:', error); if (callback) callback({ error: 'Failed to set player status' }); } }); // Submit a vote in a battle socket.on('submit_vote', ({ songId }, callback) => { try { const result = gameManager.submitVote(socket.id, songId); if (result.error) { if (callback) callback(result); return; } // Notify all players about vote count io.to(result.lobbyId).emit('vote_submitted', { lobby: result.lobby }); // If battle is finished, notify about new battle if (result.lobby.currentBattle && result.lobby.currentBattle !== result.lobby.battles[result.lobby.battles.length - 1]) { io.to(result.lobbyId).emit('new_battle', { battle: result.lobby.currentBattle }); } // If game is finished, notify about the winner if (result.lobby.state === 'FINISHED') { io.to(result.lobbyId).emit('game_finished', { winner: result.lobby.finalWinner, battles: result.lobby.battles }); } // Send response to client if (callback) callback(result); } catch (error) { console.error('Error submitting vote:', error); if (callback) callback({ error: 'Failed to submit vote' }); } }); // Handle player disconnection socket.on('disconnect', () => { try { const result = gameManager.handleDisconnect(socket.id); if (result) { // Notify remaining players about the disconnection io.to(result.lobbyId).emit('player_disconnected', { playerId: socket.id, lobby: result.lobby }); console.log(`Player ${socket.id} disconnected from lobby ${result.lobbyId}`); } } catch (error) { console.error('Error handling disconnect:', error); } }); // Simple ping handler to help with connection testing socket.on('ping', (callback) => { if (callback && typeof callback === 'function') { callback({ success: true, timestamp: Date.now() }); } }); }); server.on('error', (error) => { console.error('Server error:', error); }); const PORT = process.env.PORT || 5237; server.listen(PORT, () => { console.log(`Server running on port ${PORT}`); }); process.on('SIGINT', () => { console.log('Server shutting down...'); server.close(() => { console.log('Server closed'); process.exit(0); }); });