import { createInitialState, applyAction } from './gameState.js' /** * Crea e configura il server WebSocket per la gestione dello stato di gioco. * @param {WebSocketServer} wss - Istanza del server WebSocket. * @returns {Object} Oggetto con metodi di gestione dello stato. */ export function setupWebSocketHandler(wss) { // Stato globale della partita. let gameState = createInitialState() // Mappa dei ruoli associati ai client connessi. const clients = new Map() // ws -> { role: 'display' | 'controller' } /** * Gestisce i messaggi in arrivo dal client */ function handleMessage(ws, data) { try { // Converte il payload in stringa in modo sicuro, anche se arriva come Buffer. const dataStr = typeof data === 'string' ? data : data.toString('utf8') const msg = JSON.parse(dataStr) if (msg.type === 'register') { return handleRegister(ws, msg) } if (msg.type === 'action') { return handleAction(ws, msg) } } catch (err) { console.error('Error processing message:', err, 'data:', data) // Invia l'errore solo se la connessione e ancora aperta. if (ws.readyState === 1) { // Stato WebSocket.OPEN try { sendError(ws, 'Invalid message format') } catch (sendErr) { console.error('Error sending error message:', sendErr) } } } } /** * Gestisce la registrazione di un client (display o controller) */ function handleRegister(ws, msg) { const role = msg.role || 'display' // Valida il ruolo dichiarato dal client. if (!['display', 'controller'].includes(role)) { sendError(ws, 'Invalid role') return } clients.set(ws, { role }) console.log(`[WebSocket] Client registered as: ${role} (total clients: ${clients.size})`) // Invia subito lo stato corrente, se la connessione e aperta. if (ws.readyState === 1) { // Stato WebSocket.OPEN try { ws.send(JSON.stringify({ type: 'state', state: gameState })) } catch (err) { console.error('Error sending initial state:', err) } } } /** * Gestisce un'azione di gioco dal controller */ function handleAction(ws, msg) { // Solo i client controller possono inviare azioni. const client = clients.get(ws) if (!client || client.role !== 'controller') { sendError(ws, 'Only controllers can send actions') return } // Verifica il formato dell'azione ricevuta. if (!msg.action || !msg.action.type) { sendError(ws, 'Invalid action format') return } // Applica l'azione allo stato della partita. const previousState = gameState try { gameState = applyAction(gameState, msg.action) } catch (err) { console.error('Error applying action:', err) sendError(ws, 'Failed to apply action') gameState = previousState return } // Propaga il nuovo stato a tutti i client connessi. broadcastState() } /** * Invia un messaggio di errore al client */ function sendError(ws, message) { if (ws.readyState === 1) { // Stato WebSocket.OPEN try { ws.send(JSON.stringify({ type: 'error', message })) } catch (err) { console.error('Failed to send error message:', err) } } } /** * Invia lo stato corrente a tutti i client connessi. */ function broadcastState() { const stateMsg = JSON.stringify({ type: 'state', state: gameState }) wss.clients.forEach((client) => { if (client.readyState === 1) { // Stato WebSocket.OPEN client.send(stateMsg) } }) } /** * Gestisce la chiusura della connessione */ function handleClose(ws) { const client = clients.get(ws) const role = client?.role || 'unknown' console.log(`[WebSocket] Client disconnected (role: ${role})`) clients.delete(ws) } /** * Gestisce gli errori WebSocket */ function handleError(err, ws) { console.error('WebSocket error:', err) // In caso di frame non validi, chiude forzatamente la connessione. if (err.code === 'WS_ERR_INVALID_CLOSE_CODE' || err.code === 'WS_ERR_INVALID_UTF8') { try { if (ws && ws.readyState === 1) { // Stato WebSocket.OPEN ws.terminate() // Chiusura forzata senza handshake di chiusura. } } catch (closeErr) { console.error('Error closing connection:', closeErr) } } } // Registra gli handler per ogni nuova connessione. wss.on('connection', (ws) => { // Imposta il tipo binario per ridurre i problemi di codifica. ws.binaryType = 'arraybuffer' ws.on('message', (data) => handleMessage(ws, data)) ws.on('close', () => handleClose(ws)) ws.on('error', (err) => handleError(err, ws)) }) // Espone un'API pubblica per controllo esterno, se necessario. return { getState: () => gameState, setState: (newState) => { gameState = newState }, broadcastState, getClients: () => clients, } }