Liedkampf/server/index.js
Mathias Wagner 301e08b6e6
All checks were successful
Publish Docker image / Push Docker image to Docker Hub (push) Successful in 2m20s
Add connection status component and improve socket reconnection handling
2025-05-14 20:10:46 +02:00

382 lines
12 KiB
JavaScript

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);
});
});