From a0607d2c9a094956465b30056cb71991832de400 Mon Sep 17 00:00:00 2001 From: AlecM33 Date: Tue, 10 Jan 2023 21:33:24 -0500 Subject: [PATCH] further incomplete redis draft --- index.js | 8 +- server/api/AdminAPI.js | 8 +- server/api/GamesAPI.js | 6 +- server/config/globals.js | 22 ++- server/modules/Events.js | 68 +++++++++ server/modules/ServerBootstrapper.js | 6 +- server/modules/singletons/ActiveGameRunner.js | 23 +-- server/modules/singletons/GameManager.js | 144 +++++++++--------- server/modules/singletons/SocketManager.js | 110 ++++++------- 9 files changed, 251 insertions(+), 144 deletions(-) create mode 100644 server/modules/Events.js diff --git a/index.js b/index.js index b95f598..bdfc480 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,9 @@ const main = async () => { const express = require('express'); const app = express(); const ServerBootstrapper = require('./server/modules/ServerBootstrapper'); + const ActiveGameRunner = require('./server/modules/singletons/ActiveGameRunner'); + const GameManager = require('./server/modules/singletons/GameManager'); + const SocketManager = require('./server/modules/singletons/SocketManager'); const globals = require('./server/config/globals'); app.use(express.json({limit: '10kb'})); @@ -25,10 +28,13 @@ const main = async () => { } return id; })()); + singletons.gameManager.activeGameRunner = ActiveGameRunner.instance; + singletons.gameManager.socketManager = SocketManager.instance; + singletons.socketManager.activeGameRunner = ActiveGameRunner.instance; + singletons.socketManager.gameManager = GameManager.instance; await singletons.activeGameRunner.client.connect(); console.log('Root Redis client connected'); - await singletons.activeGameRunner.refreshActiveGames(); await singletons.activeGameRunner.createGameSyncSubscriber(singletons.gameManager, singletons.socketManager); await singletons.socketManager.createRedisPublisher(); await singletons.gameManager.createRedisPublisher(); diff --git a/server/api/AdminAPI.js b/server/api/AdminAPI.js index ecb44fb..ebf5c09 100644 --- a/server/api/AdminAPI.js +++ b/server/api/AdminAPI.js @@ -20,11 +20,11 @@ router.post('/sockets/broadcast', function (req, res) { res.status(201).send('Broadcasted message to all connected sockets: ' + req.body?.message); }); -router.get('/games/state', function (req, res) { +router.get('/games/state', async (req, res) => { const gamesArray = []; - for (const key of gameManager.activeGameRunner.activeGames.keys()) { - gamesArray.push(gameManager.activeGameRunner.activeGames.get(key)); - } + await this.client.hGetAll('activeGames').then(async (r) => { + Object.values(r).forEach((v) => gamesArray.push(v)); + }); res.status(200).send(gamesArray); }); diff --git a/server/api/GamesAPI.js b/server/api/GamesAPI.js index a5c181a..89147e3 100644 --- a/server/api/GamesAPI.js +++ b/server/api/GamesAPI.js @@ -66,7 +66,7 @@ router.get('/:code/availability', function (req, res) { }); }); -router.patch('/:code/players', function (req, res) { +router.patch('/:code/players', async function (req, res) { if ( req.body === null || !validateAccessCode(req.body.accessCode) @@ -77,7 +77,7 @@ router.patch('/:code/players', function (req, res) { ) { res.status(400).send(); } else { - const game = gameManager.activeGameRunner.activeGames.get(req.body.accessCode); + const game = await gameManager.activeGameRunner.getActiveGame(req.body.accessCode); if (game) { const inUseCookie = gameManager.environment === globals.ENVIRONMENT.PRODUCTION ? req.body.localCookie : req.body.sessionCookie; gameManager.joinGame(game, req.body.playerName, inUseCookie, req.body.joinAsSpectator).then((data) => { @@ -101,7 +101,7 @@ router.patch('/:code/restart', function (req, res) { ) { res.status(400).send(); } else { - const game = gameManager.activeGameRunner.activeGames.get(req.body.accessCode); + const game = gameManager.activeGameRunner.getActiveGame(req.body.accessCode); if (game) { gameManager.restartGame(game, gameManager.namespace).then((data) => { res.status(200).send(); diff --git a/server/config/globals.js b/server/config/globals.js index cfbeb9c..0a2917c 100644 --- a/server/config/globals.js +++ b/server/config/globals.js @@ -48,7 +48,27 @@ const globals = { END_GAME: 'endGame', RESTART_GAME: 'restartGame', PLAYER_JOINED: 'playerJoined', - SPECTATOR_JOINED: 'spectatorJoined' + UPDATE_SPECTATORS: 'updateSpectators', + SYNC_GAME_STATE: 'syncGameState', + UPDATE_SOCKET: 'updateSocket' + }, + SYNCABLE_EVENTS: function () { + return [ + this.EVENT_IDS.NEW_GAME, + this.EVENT_IDS.START_GAME, + this.EVENT_IDS.PAUSE_TIMER, + this.EVENT_IDS.RESUME_TIMER, + this.EVENT_IDS.GET_TIME_REMAINING, + this.EVENT_IDS.KILL_PLAYER, + this.EVENT_IDS.REVEAL_PLAYER, + this.EVENT_IDS.TRANSFER_MODERATOR, + this.EVENT_IDS.END_GAME, + this.EVENT_IDS.RESTART_GAME, + this.EVENT_IDS.PLAYER_JOINED, + this.EVENT_IDS.UPDATE_SPECTATORS, + this.EVENT_IDS.SYNC_GAME_STATE, + this.EVENT_IDS.UPDATE_SOCKET + ] }, MESSAGES: { ENTER_NAME: 'Client must enter name.' diff --git a/server/modules/Events.js b/server/modules/Events.js new file mode 100644 index 0000000..e3373c3 --- /dev/null +++ b/server/modules/Events.js @@ -0,0 +1,68 @@ +const globals = require('../config/globals'); +const GameStateCurator = require("./GameStateCurator"); +const EVENT_IDS = globals.EVENT_IDS; + +const Events = [ + { + id: EVENT_IDS.PLAYER_JOINED, + stateChange: (game, args, gameManager) => { + let toBeAssignedIndex = game.people.findIndex( + (person) => person.id === args.id && person.assigned === false + ); + if (toBeAssignedIndex >= 0) { + game.people[toBeAssignedIndex] = args; + game.isFull = gameManager.isGameFull(game); + } + }, + communicate: (game, args, gameManager) => { + gameManager.namespace.in(game.accessCode).emit( + globals.EVENTS.PLAYER_JOINED, + GameStateCurator.mapPerson(args), + game.isFull + ); + } + }, + { + id: EVENT_IDS.UPDATE_SPECTATORS, + stateChange: (game, args, gameManager) => { + game.spectators = args; + }, + communicate: (game, args, gameManager) => { + gameManager.namespace.in(game.accessCode).emit( + globals.EVENTS.UPDATE_SPECTATORS, + game.spectators.map((spectator) => { return GameStateCurator.mapPerson(spectator); }) + ); + } + }, + { + id: EVENT_IDS.FETCH_GAME_STATE, + stateChange: (game, args, gameManager) => { + const matchingPerson = gameManager.findPersonByField(game, 'cookie', args.personId); + if (matchingPerson) { + if (matchingPerson.socketId === socketId) { + logger.debug('matching person found with an established connection to the room: ' + matchingPerson.name); + if (ackFn) { + ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson)); + } + } else { + logger.debug('matching person found with a new connection to the room: ' + matchingPerson.name); + this.namespace.sockets.get(socketId).join(accessCode); + matchingPerson.socketId = socketId; + await this.publisher.publish( + globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, + game.accessCode + ';' + globals.EVENT_IDS.UPDATE_SOCKET + ';' + JSON.stringify({ personId: matchingPerson.id, socketId: socketId }) + ';' + this.instanceId + ); + if (ackFn) { + ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson)); + } + } + } else { + if (ackFn) { + rejectClientRequestForGameState(ackFn); + } + } + } + } +]; + +module.exports = Events; diff --git a/server/modules/ServerBootstrapper.js b/server/modules/ServerBootstrapper.js index 01f90ac..082f76a 100644 --- a/server/modules/ServerBootstrapper.js +++ b/server/modules/ServerBootstrapper.js @@ -15,10 +15,10 @@ const ServerBootstrapper = { singletons: (logger, instanceId) => { return { activeGameRunner: new ActiveGameRunner(logger, instanceId), - socketManager: new SocketManager(logger, ActiveGameRunner.instance, instanceId), + socketManager: new SocketManager(logger, instanceId), gameManager: process.env.NODE_ENV.trim() === 'development' - ? new GameManager(logger, ENVIRONMENT.LOCAL, ActiveGameRunner.instance, instanceId) - : new GameManager(logger, ENVIRONMENT.PRODUCTION, ActiveGameRunner.instance, instanceId) + ? new GameManager(logger, ENVIRONMENT.LOCAL, instanceId) + : new GameManager(logger, ENVIRONMENT.PRODUCTION, instanceId) }; }, diff --git a/server/modules/singletons/ActiveGameRunner.js b/server/modules/singletons/ActiveGameRunner.js index b4e0305..65f535e 100644 --- a/server/modules/singletons/ActiveGameRunner.js +++ b/server/modules/singletons/ActiveGameRunner.js @@ -18,12 +18,9 @@ class ActiveGameRunner { ActiveGameRunner.instance = this; } - refreshActiveGames = async () => { - await this.client.hGetAll('activeGames').then(async (r) => { - this.activeGames = new Map(Object.entries(r).map(([k, v]) => { - return [k, JSON.parse(v)]; - })); - }); + getActiveGame = async (accessCode) => { + const r = await this.client.hGet('activeGames', accessCode); + return JSON.parse(r); } createGameSyncSubscriber = async (gameManager, socketManager) => { @@ -36,13 +33,21 @@ class ActiveGameRunner { this.logger.trace('Disregarding self-authored message'); return; } - const game = this.activeGames.get(messageComponents[0]); + const game = await this.getActiveGame(messageComponents[0]); let args; if (messageComponents[2]) { args = JSON.parse(messageComponents[2]); } - if (game || messageComponents[1] === globals.EVENT_IDS.NEW_GAME) { - await socketManager.handleEventById(messageComponents[1], game, null, gameManager, game?.accessCode || messageComponents[0], args ? args : null, null) + if (game) { + await socketManager.handleEventById( + messageComponents[1], + game, + null, + game?.accessCode || messageComponents[0], + args ? args : null, + null, + true + ) } }); this.logger.info('ACTIVE GAME RUNNER - CREATED GAME SYNC SUBSCRIBER'); diff --git a/server/modules/singletons/GameManager.js b/server/modules/singletons/GameManager.js index de1ae7d..cf5f438 100644 --- a/server/modules/singletons/GameManager.js +++ b/server/modules/singletons/GameManager.js @@ -8,14 +8,15 @@ const redis = require('redis'); class GameManager { - constructor (logger, environment, activeGameRunner, instanceId) { + constructor (logger, environment, instanceId) { if (GameManager.instance) { throw new Error('The server tried to instantiate more than one GameManager'); } logger.info('CREATING SINGLETON GAME MANAGER'); this.logger = logger; this.environment = environment; - this.activeGameRunner = activeGameRunner; + this.activeGameRunner = null; + this.socketManager = null; this.namespace = null; this.publisher = null; this.instanceId = instanceId; @@ -47,7 +48,7 @@ class GameManager { gameParams.moderatorName, gameParams.hasDedicatedModerator ); - await this.pruneStaleGames(); + //await this.pruneStaleGames(); const newAccessCode = await this.generateAccessCode(globals.ACCESS_CODE_CHAR_POOL); if (newAccessCode === null) { return Promise.reject(globals.ERROR_MESSAGE.NO_UNIQUE_ACCESS_CODE); @@ -69,15 +70,7 @@ class GameManager { new Date().toJSON(), req.timerParams ); - - this.activeGameRunner.activeGames.set(newAccessCode, newGame); await this.activeGameRunner.client.hSet('activeGames', newAccessCode, JSON.stringify(newGame)); - - this.publisher?.publish( - globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, - newGame.accessCode + ';' + globals.EVENT_IDS.NEW_GAME + ';' + JSON.stringify({ newGame: newGame }) + ';' + this.instanceId - ); - return Promise.resolve({ accessCode: newAccessCode, cookie: moderator.cookie, environment: this.environment }); }).catch((message) => { console.log(message); @@ -93,7 +86,6 @@ class GameManager { game.timerParams.paused = true; this.activeGameRunner.runGame(game, namespace); } - await this.refreshGame(game); namespace.in(game.accessCode).emit(globals.EVENT_IDS.START_GAME); } }; @@ -111,7 +103,6 @@ class GameManager { }; resumeTimer = async (game, logger) => { - await this.activeGameRunner.refreshActiveGames(); const thread = this.activeGameRunner.timerThreads[game.accessCode]; if (thread && !thread.killed) { this.logger.debug('Timer thread found for game ' + game.accessCode); @@ -125,7 +116,6 @@ class GameManager { getTimeRemaining = async (game, socketId) => { if (socketId) { - await this.activeGameRunner.refreshActiveGames(); const thread = this.activeGameRunner.timerThreads[game.accessCode]; if (thread && (!thread.killed && thread.exitCode === null)) { thread.send({ @@ -147,7 +137,6 @@ class GameManager { if (person && !person.revealed) { this.logger.debug('game ' + game.accessCode + ': revealing player ' + person.name); person.revealed = true; - await this.refreshGame(game); this.namespace.in(game.accessCode).emit( globals.EVENT_IDS.REVEAL_PLAYER, { @@ -168,12 +157,11 @@ class GameManager { for (const person of game.people) { person.revealed = true; } - await this.refreshGame(game); this.namespace.in(game.accessCode).emit(globals.EVENT_IDS.END_GAME, GameStateCurator.mapPeopleForModerator(game.people)); }; checkAvailability = async (code) => { - const game = this.activeGameRunner.activeGames.get(code.toUpperCase().trim()); + const game = await this.activeGameRunner.getActiveGame(code.toUpperCase().trim()); if (game) { return Promise.resolve({ accessCode: code, playerCount: getGameSize(game.deck), timerParams: game.timerParams }); } else { @@ -185,7 +173,8 @@ class GameManager { const charCount = charPool.length; let codeDigits, accessCode; let attempts = 0; - while (!accessCode || (this.activeGameRunner.activeGames.get(accessCode) && attempts < globals.ACCESS_CODE_GENERATION_ATTEMPTS)) { + while (!accessCode || ((await this.activeGameRunner.client.hKeys('activeGames')).includes(accessCode) + && attempts < globals.ACCESS_CODE_GENERATION_ATTEMPTS)) { codeDigits = []; let iterations = globals.ACCESS_CODE_LENGTH; while (iterations > 0) { @@ -195,7 +184,7 @@ class GameManager { accessCode = codeDigits.join(''); attempts ++; } - return this.activeGameRunner.activeGames.get(accessCode) + return (await this.activeGameRunner.client.hKeys('activeGames')).includes(accessCode) ? null : accessCode; }; @@ -210,9 +199,9 @@ class GameManager { logger.debug('game ' + game.accessCode + ': transferring mod powers to ' + person.name); if (game.moderator === person) { person.userType = globals.USER_TYPES.MODERATOR; - this.namespace.to(person.socketId).emit(globals.EVENTS.SYNC_GAME_STATE); - if (socketId) { - this.namespace.sockets.get(socketId).to(game.accessCode).emit(globals.EVENT_IDS.KILL_PLAYER, person.id); + const socket = this.namespace.sockets.get(socketId); + if (socket) { + this.namespace.to(socketId).emit(globals.EVENTS.SYNC_GAME_STATE); // they are guaranteed to be connected to this instance. } } else { const oldModerator = game.moderator; @@ -233,10 +222,9 @@ class GameManager { game.spectators.map((spectator) => GameStateCurator.mapPerson(spectator)) ); } - this.namespace.to(person.socketId).emit(globals.EVENTS.SYNC_GAME_STATE); - this.namespace.to(oldModerator.socketId).emit(globals.EVENTS.SYNC_GAME_STATE); + await notifyPlayerInvolvedInModTransfer(game, this.namespace, person); + await notifyPlayerInvolvedInModTransfer(game, this.namespace, oldModerator); } - await this.refreshGame(game); } }; @@ -247,18 +235,27 @@ class GameManager { person.userType = globals.USER_TYPES.KILLED_PLAYER; } person.out = true; + const socket = namespace.sockets.get(socketId); + if (socket && game.moderator.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { + socket.to(game.accessCode).emit(globals.EVENT_IDS.KILL_PLAYER, person.id); + } else { + namespace.in(game.accessCode).emit(globals.EVENT_IDS.KILL_PLAYER, person.id); + } // temporary moderators will transfer their powers automatically to the first person they kill. if (game.moderator.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { - await this.transferModeratorPowers(socketId, game, person, namespace, logger); - } else { - await this.refreshGame(game); - namespace.in(game.accessCode).emit(globals.EVENT_IDS.KILL_PLAYER, person.id); + await this.socketManager.handleAndSyncEvent( + globals.EVENT_IDS.TRANSFER_MODERATOR, + game, + socket, + { personId: person.id }, + null + ); } } }; joinGame = async (game, name, cookie, joinAsSpectator) => { - const matchingPerson = findPersonByField(game, 'cookie', cookie); + const matchingPerson = this.findPersonByField(game, 'cookie', cookie); if (matchingPerson) { return Promise.resolve(matchingPerson.cookie); } @@ -279,15 +276,15 @@ class GameManager { unassignedPerson.name = name; game.isFull = this.isGameFull(game); await this.refreshGame(game); - await this.publisher?.publish( - globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, - game.accessCode + ';' + globals.EVENT_IDS.PLAYER_JOINED + ';' + JSON.stringify(unassignedPerson) + ';' + this.instanceId - ); this.namespace.in(game.accessCode).emit( globals.EVENTS.PLAYER_JOINED, GameStateCurator.mapPerson(unassignedPerson), game.isFull ); + await this.publisher?.publish( + globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, + game.accessCode + ';' + globals.EVENT_IDS.PLAYER_JOINED + ';' + JSON.stringify(unassignedPerson) + ';' + this.instanceId + ); return Promise.resolve(unassignedPerson.cookie); } else { if (game.spectators.length === globals.MAX_SPECTATORS) { @@ -352,7 +349,7 @@ class GameManager { }; handleRequestForGameState = async (game, namespace, logger, gameRunner, accessCode, personCookie, ackFn, socketId) => { - const matchingPerson = findPersonByField(game, 'cookie', personCookie); + const matchingPerson = this.findPersonByField(game, 'cookie', personCookie); if (matchingPerson) { if (matchingPerson.socketId === socketId) { logger.debug('matching person found with an established connection to the room: ' + matchingPerson.name); @@ -363,6 +360,10 @@ class GameManager { logger.debug('matching person found with a new connection to the room: ' + matchingPerson.name); this.namespace.sockets.get(socketId).join(accessCode); matchingPerson.socketId = socketId; + await this.publisher.publish( + globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, + game.accessCode + ';' + globals.EVENT_IDS.UPDATE_SOCKET + ';' + JSON.stringify({ personId: matchingPerson.id, socketId: socketId }) + ';' + this.instanceId + ); if (ackFn) { ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson)); } @@ -391,27 +392,40 @@ class GameManager { return array; }; - pruneStaleGames = async () => { - await this.activeGameRunner.refreshActiveGames(); - this.activeGameRunner.activeGames.forEach((key, value) => { - if (value.createTime) { - const createDate = new Date(value.createTime); - if (createDate.setHours(createDate.getHours() + globals.STALE_GAME_HOURS) < Date.now()) { - this.logger.info('PRUNING STALE GAME ' + key); - this.activeGameRunner.activeGames.delete(key); - if (this.activeGameRunner.timerThreads[key]) { - this.logger.info('KILLING STALE TIMER PROCESS FOR ' + key); - this.activeGameRunner.timerThreads[key].kill(); - delete this.activeGameRunner.timerThreads[key]; - } - } - } - }); - }; + // pruneStaleGames = async () => { + // this.activeGameRunner.activeGames.forEach((key, value) => { + // if (value.createTime) { + // const createDate = new Date(value.createTime); + // if (createDate.setHours(createDate.getHours() + globals.STALE_GAME_HOURS) < Date.now()) { + // this.logger.info('PRUNING STALE GAME ' + key); + // this.activeGameRunner.activeGames.delete(key); + // if (this.activeGameRunner.timerThreads[key]) { + // this.logger.info('KILLING STALE TIMER PROCESS FOR ' + key); + // this.activeGameRunner.timerThreads[key].kill(); + // delete this.activeGameRunner.timerThreads[key]; + // } + // } + // } + // }); + // }; isGameFull = (game) => { return game.moderator.assigned === true && !game.people.find((person) => person.assigned === false); } + + findPersonByField = (game, fieldName, value) => { + let person; + if (value === game.moderator[fieldName]) { + person = game.moderator; + } + if (!person) { + person = game.people.find((person) => person[fieldName] === value); + } + if (!person) { + person = game.spectators.find((spectator) => spectator[fieldName] === value); + } + return person; + } } function getRandomInt (max) { @@ -483,20 +497,6 @@ function findPlayerBySocketId (people, socketId) { return people.find((person) => person.socketId === socketId && person.userType === globals.USER_TYPES.PLAYER); } -function findPersonByField (game, fieldName, value) { - let person; - if (value === game.moderator[fieldName]) { - person = game.moderator; - } - if (!person) { - person = game.people.find((person) => person[fieldName] === value); - } - if (!person) { - person = game.spectators.find((spectator) => spectator[fieldName] === value); - } - return person; -} - function isNameTaken (game, name) { const processedName = name.toLowerCase().trim(); return (game.people.find((person) => person.name.toLowerCase().trim() === processedName)) @@ -513,6 +513,12 @@ function getGameSize (cards) { return quantity; } +async function notifyPlayerInvolvedInModTransfer(game, namespace, person) { + if (namespace.sockets.get(person.socketId)) { + namespace.to(person.socketId).emit(globals.EVENTS.SYNC_GAME_STATE); + } +} + async function addSpectator (game, name, logger, namespace, publisher, instanceId, refreshGame) { const spectator = new Person( createRandomId(), @@ -527,10 +533,10 @@ async function addSpectator (game, name, logger, namespace, publisher, instanceI globals.EVENTS.UPDATE_SPECTATORS, game.spectators.map((spectator) => { return GameStateCurator.mapPerson(spectator); }) ); - publisher.publish( + await publisher.publish( globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, - game.accessCode + ';' + globals.EVENT_IDS.SPECTATOR_JOINED + ';' + JSON.stringify(spectator) + ';' + instanceId -); + game.accessCode + ';' + globals.EVENT_IDS.UPDATE_SPECTATORS + ';' + JSON.stringify(game.spectators) + ';' + instanceId + ); return Promise.resolve(spectator.cookie); } diff --git a/server/modules/singletons/SocketManager.js b/server/modules/singletons/SocketManager.js index 5fc5f97..449238c 100644 --- a/server/modules/singletons/SocketManager.js +++ b/server/modules/singletons/SocketManager.js @@ -3,9 +3,10 @@ const EVENT_IDS = globals.EVENT_IDS; const { RateLimiterMemory } = require('rate-limiter-flexible'); const redis = require('redis'); const GameStateCurator = require("../GameStateCurator"); +const Events = require("../Events"); class SocketManager { - constructor (logger, activeGameRunner, instanceId) { + constructor (logger, instanceId) { if (SocketManager.instance) { throw new Error('The server attempted to instantiate more than one SocketManager.'); } @@ -13,7 +14,8 @@ class SocketManager { this.logger = logger; this.io = null; this.publisher = null; - this.activeGameRunner = activeGameRunner; + this.activeGameRunner = null; + this.gameManager = null; this.instanceId = instanceId; SocketManager.instance = this; } @@ -60,95 +62,95 @@ class SocketManager { return server.of('/in-game'); }; - registerHandlers = (namespace, socket, gameManager) => { + registerHandlers = (namespace, socket) => { socket.on(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, async (eventId, accessCode, args = null, ackFn = null) => { - const game = gameManager.activeGameRunner.activeGames.get(accessCode); + const game = await this.activeGameRunner.getActiveGame(accessCode); if (game) { - await this.handleEventById(eventId, game, socket.id, gameManager, accessCode, args, ackFn); - /* This server should publish events initiated by a connected socket to Redis for consumption by other instances. */ - if (Object.values(EVENT_IDS).includes(eventId)) { - await gameManager.refreshGame(game); - this.publisher?.publish( - globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, - accessCode + ';' + eventId + ';' + JSON.stringify(args) + ';' + this.instanceId - ); - } + await this.handleAndSyncEvent(eventId, game, socket, args, ackFn); } else { ackFn(null); } }); }; - handleEventById = async (eventId, game, socketId, gameManager, accessCode, args, ackFn) => { - this.logger.debug('ARGS TO HANDLER: ' + JSON.stringify(args)); + handleAndSyncEvent = async (eventId, game, socket, args, ackFn) => { + await this.handleEventById(eventId, game, socket?.id, game.accessCode, args, ackFn, false); + /* This server should publish events initiated by a connected socket to Redis for consumption by other instances. */ + if (globals.SYNCABLE_EVENTS().includes(eventId)) { + await this.gameManager.refreshGame(game); + this.publisher?.publish( + globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, + game.accessCode + ';' + eventId + ';' + JSON.stringify(args) + ';' + this.instanceId + ); + } + } + + handleEventById = async (eventId, game, socketId, accessCode, args, ackFn, syncOnly) => { + this.logger.trace('ARGS TO HANDLER: ' + JSON.stringify(args)); + const event = Events.find((event) => event.id === eventId); + if (event) { + if (!syncOnly) { + event.stateChange(game, args, this.gameManager); + } + event.communicate(game, args, this.gameManager); + } switch (eventId) { - case EVENT_IDS.NEW_GAME: - this.activeGameRunner.activeGames.set(accessCode, args.newGame); - break; - case EVENT_IDS.PLAYER_JOINED: - let toBeAssignedIndex = game.people.findIndex((person) => person.id === args.id && person.assigned === false); - if (toBeAssignedIndex >= 0) { - game.people[toBeAssignedIndex] = args; - game.isFull = gameManager.isGameFull(game); - gameManager.namespace.in(game.accessCode).emit( - globals.EVENTS.PLAYER_JOINED, - GameStateCurator.mapPerson(args), - game.isFull - ); - } - break; - case EVENT_IDS.SPECTATOR_JOINED: - if (!game.spectators.find((spectator) => spectator.id === args.id)) { - game.spectators.push(args); - } - gameManager.namespace.in(game.accessCode).emit( - globals.EVENTS.UPDATE_SPECTATORS, - game.spectators.map((spectator) => { return GameStateCurator.mapPerson(spectator); }) - ); - break; case EVENT_IDS.FETCH_GAME_STATE: - if (!socketId) break; - await gameManager.handleRequestForGameState( + await this.gameManager.handleRequestForGameState( game, this.namespace, this.logger, - gameManager.activeGameRunner, + this.activeGameRunner, accessCode, args.personId, ackFn, socketId ); break; + case EVENT_IDS.UPDATE_SOCKET: + const matchingPerson = this.gameManager.findPersonByField(game, 'id', args.personId); + if (matchingPerson) { + matchingPerson.socketId = args.socketId; + } + break; + case EVENT_IDS.SYNC_GAME_STATE: + const personToSync = this.gameManager.findPersonByField(game, 'id', args.personId); + if (personToSync) { + this.gameManager.namespace.to(personToSync.socketId).emit(globals.EVENTS.SYNC_GAME_STATE); + } + break; case EVENT_IDS.START_GAME: - await gameManager.startGame(game, gameManager.namespace); + await this.gameManager.startGame(game, this.gameManager.namespace); if (ackFn) { ackFn(); } break; case EVENT_IDS.PAUSE_TIMER: - await gameManager.pauseTimer(game, this.logger); + await this.gameManager.pauseTimer(game, this.logger); break; case EVENT_IDS.RESUME_TIMER: - await gameManager.resumeTimer(game, this.logger); + await this.gameManager.resumeTimer(game, this.logger); break; case EVENT_IDS.GET_TIME_REMAINING: - await gameManager.getTimeRemaining(game, socketId); + await this.gameManager.getTimeRemaining(game, socketId); break; case EVENT_IDS.KILL_PLAYER: - await gameManager.killPlayer(socketId, game, game.people.find((person) => person.id === args.personId), gameManager.namespace, this.logger); + await this.gameManager.killPlayer(socketId, game, game.people.find((person) => person.id === args.personId), this.gameManager.namespace, this.logger); break; case EVENT_IDS.REVEAL_PLAYER: - await gameManager.revealPlayer(game, args.personId); + await this.gameManager.revealPlayer(game, args.personId); break; case EVENT_IDS.TRANSFER_MODERATOR: - let person = game.people.find((person) => person.id === args.personId); - if (!person) { - person = game.spectators.find((spectator) => spectator.id === args.personId); - } - await gameManager.transferModeratorPowers(socketId, game, person, gameManager.namespace, this.logger); + await this.gameManager.transferModeratorPowers( + socketId, + game, + this.gameManager?.findPersonByField(game, 'id', args.personId), + this.gameManager.namespace, + this.logger + ); break; case EVENT_IDS.END_GAME: - await gameManager.endGame(game); + await this.gameManager.endGame(game); if (ackFn) { ackFn(); }