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 "./enums.js" import * as ev from "./class/Event.js" import { Client, ClientPlayer } from "./class/Client.js" import { GameSession } from "./class/GameSession.js" import { shortId } from "./utils.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 ? // TODO: spectator/player distinction with msg.role ? // msg.role is probably not a good idea. // something like a different route could be better // "/pong" to play, "/ID_OF_A_GAMESESSION" to spectate const player = clientsMap.get(this.id) as ClientPlayer; player.matchOptions = msg.matchOptions; this.send(JSON.stringify( new ev.EventAssignId(this.id) )); this.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchmakingInProgress) )); matchmaking(player); } 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 = 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) )); } 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, key, map) => { if (!client.isAlive) { clientTerminate(client, key, map); deleteLog += ` ${shortId(key)} |`; } 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, key: string, map: Map) { client.socket.terminate(); if (client.gameSession) { client.gameSession.playersMap.delete(key); if (client.gameSession.playersMap.size === 0) { clearInterval(client.gameSession.clientsUpdateInterval); clearInterval(client.gameSession.gameLoopInterval); gameSessionsMap.delete(client.gameSession.id); } } map.delete(key); if (matchmakingPlayersMap.has(key)) { matchmakingPlayersMap.delete(key); } } function closeListener() { clearInterval(pingInterval); } function errorListener(error: Error) { console.log("Error: " + JSON.stringify(error)); }