581a567c17
Il controller invia un comando 'speak' via WebSocket. Il server inoltra il messaggio solo ai client display, che eseguono speechSynthesis con preferenza per voce italiana.
200 lines
5.7 KiB
JavaScript
200 lines
5.7 KiB
JavaScript
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)
|
|
}
|
|
|
|
if (msg.type === 'speak') {
|
|
return handleSpeak(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()
|
|
}
|
|
|
|
/**
|
|
* Gestisce una richiesta di sintesi vocale proveniente dal controller.
|
|
* Il messaggio viene inoltrato solo ai client registrati come display.
|
|
*/
|
|
function handleSpeak(ws, msg) {
|
|
const client = clients.get(ws)
|
|
if (!client || client.role !== 'controller') {
|
|
sendError(ws, 'Only controllers can request speech')
|
|
return
|
|
}
|
|
|
|
if (typeof msg.text !== 'string' || msg.text.trim() === '') {
|
|
sendError(ws, 'Invalid speak payload')
|
|
return
|
|
}
|
|
|
|
const speakMsg = JSON.stringify({ type: 'speak', text: msg.text.trim() })
|
|
clients.forEach((meta, clientWs) => {
|
|
if (meta.role === 'display' && clientWs.readyState === 1) {
|
|
clientWs.send(speakMsg)
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
}
|
|
}
|