import { WebSocketServer, WebSocket as BaseLibWebSocket } from "ws"; export class WebSocket extends BaseLibWebSocket { id?: string; } import { IncomingMessage } from "http"; import { v4 as uuidv4 } from 'uuid'; import * as en from "../shared_js/enums.js" import * as ev from "../shared_js/class/Event.js" import { Client, ClientPlayer, ClientSpectator } from "./class/Client.js" import { GameSession } from "./class/GameSession.js" import { shortId } from "./utils.js"; import { gameSessionIdPLACEHOLDER } from "./constants.js"; // pas indispensable d'avoir un autre port si le WebSocket est relié à un serveur http préexistant ? const wsPort = 8042; export const wsServer = new WebSocketServer({port: wsPort, path: "/pong"}); const clientsMap: Map = new Map; // socket.id/Client const matchmakingPlayersMap: Map = new Map; // socket.id/ClientPlayer (duplicates with clientsMap) const gameSessionsMap: Map = new Map; // GameSession.id(url)/GameSession wsServer.on("connection", connectionListener); wsServer.on("error", errorListener); wsServer.on("close", closeListener); function connectionListener(socket: WebSocket, request: IncomingMessage) { const id = uuidv4(); const client = new Client(socket, id); clientsMap.set(id, client); socket.id = id; socket.on("pong", function heartbeat() { client.isAlive = true; // console.log(`client ${shortId(client.id)} is alive`); }); socket.on("message", function log(data: string) { try { const event: ev.ClientEvent = JSON.parse(data); if (event.type === en.EventTypes.clientInput) { return; } } catch (e) {} console.log("data: " + data); }); socket.once("message", clientAnnounceListener); } function clientAnnounceListener(this: WebSocket, data: string) { try { const msg : ev.ClientAnnounce = JSON.parse(data); if (msg.type === en.EventTypes.clientAnnounce) { // TODO: reconnection with msg.clientId ? // "/pong" to play, "/pong?ID_OF_A_GAMESESSION" to spectate (or something like that) if (msg.role === en.ClientRole.player) { const announce: ev.ClientAnnouncePlayer = msg; const player = clientsMap.get(this.id) as ClientPlayer; player.matchOptions = announce.matchOptions; this.send(JSON.stringify( new ev.EventAssignId(this.id) )); this.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchmakingInProgress) )); matchmaking(player); } else if (msg.role === en.ClientRole.spectator) { const announce: ev.ClientAnnounceSpectator = msg; const gameSession = gameSessionsMap.get(announce.gameSessionId); if (!gameSession) { // WIP: send "invalid game session" return; } const spectator = clientsMap.get(this.id) as ClientSpectator; spectator.gameSession = gameSession; gameSession.spectatorsMap.set(spectator.id, spectator); this.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchStart) )); } } else { console.log("Invalid ClientAnnounce"); } return; } catch (e) { console.log("Invalid JSON (clientAnnounceListener)"); } this.once("message", clientAnnounceListener); } function matchmaking(player: ClientPlayer) { const minPlayersNumber = 2; const maxPlayersNumber = 2; const matchOptions = player.matchOptions; matchmakingPlayersMap.set(player.id, player); const compatiblePlayers: ClientPlayer[] = []; for (const [id, client] of matchmakingPlayersMap) { if (client.matchOptions === matchOptions) { compatiblePlayers.push(client); if (compatiblePlayers.length === maxPlayersNumber) { break; } } } if (compatiblePlayers.length < minPlayersNumber) { return; } // const id = gameSessionIdPLACEHOLDER; // Force ID, TESTING SPECTATOR const id = uuidv4(); const gameSession = new GameSession(id, matchOptions); gameSessionsMap.set(id, gameSession); compatiblePlayers.forEach((client) => { matchmakingPlayersMap.delete(client.id); client.gameSession = gameSession; gameSession.playersMap.set(client.id, client); gameSession.unreadyPlayersMap.set(client.id, client); }); // WIP: Not pretty, hardcoded two players. // Could be done in gameSession maybe ? compatiblePlayers[0].racket = gameSession.components.playerRight; compatiblePlayers[1].racket = gameSession.components.playerLeft; compatiblePlayers.forEach((client) => { client.socket.once("message", playerReadyConfirmationListener); }); compatiblePlayers[0].socket.send(JSON.stringify( new ev.EventMatchmakingComplete(en.PlayerSide.right) )); compatiblePlayers[1].socket.send(JSON.stringify( new ev.EventMatchmakingComplete(en.PlayerSide.left) )); setTimeout(function abortMatch() { if (gameSession.unreadyPlayersMap.size !== 0) { gameSessionsMap.delete(gameSession.id); gameSession.playersMap.forEach((client) => { client.socket.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchAbort) )); client.gameSession = null; clientTerminate(client); }); } }, 5000); } function playerReadyConfirmationListener(this: WebSocket, data: string) { try { const msg : ev.ClientEvent = JSON.parse(data); if (msg.type === en.EventTypes.clientPlayerReady) { const client = clientsMap.get(this.id); const gameSession = client.gameSession; gameSession.unreadyPlayersMap.delete(this.id); if (gameSession.unreadyPlayersMap.size === 0) { gameSession.playersMap.forEach( (client) => { client.socket.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchStart) )); }); gameSession.start(); } } else { console.log("Invalid playerReadyConfirmation"); } return; } catch (e) { console.log("Invalid JSON (playerReadyConfirmationListener)"); } this.once("message", playerReadyConfirmationListener); } export function clientInputListener(this: WebSocket, data: string) { try { // const input: ev.ClientEvent = JSON.parse(data); const input: ev.EventInput = JSON.parse(data); if (input.type === en.EventTypes.clientInput) { const client = clientsMap.get(this.id) as ClientPlayer; client.inputBuffer = input; client.gameSession.instantInputDebug(client); // wip } else { console.log("Invalid clientInput"); } } catch (e) { console.log("Invalid JSON (clientInputListener)"); } } //////////// //////////// const pingInterval = setInterval( () => { let deleteLog = ""; clientsMap.forEach( (client) => { if (!client.isAlive) { clientTerminate(client); deleteLog += ` ${shortId(client.id)} |`; } else { client.isAlive = false; client.socket.ping(); } }); if (deleteLog) { console.log(`Disconnected:${deleteLog}`); } console.log("gameSessionMap size: " + gameSessionsMap.size); console.log("clientsMap size: " + clientsMap.size); console.log("matchmakingPlayersMap size: " + matchmakingPlayersMap.size); console.log(""); }, 4200); function clientTerminate(client: Client) { client.socket.terminate(); if (client.gameSession) { client.gameSession.playersMap.delete(client.id); if (client.gameSession.playersMap.size === 0) { clearInterval(client.gameSession.playersUpdateInterval); clearInterval(client.gameSession.spectatorsUpdateInterval); clearInterval(client.gameSession.gameLoopInterval); gameSessionsMap.delete(client.gameSession.id); } } clientsMap.delete(client.id); if (matchmakingPlayersMap.has(client.id)) { matchmakingPlayersMap.delete(client.id); } } function closeListener() { clearInterval(pingInterval); } function errorListener(error: Error) { console.log("Error: " + JSON.stringify(error)); }