From 148cfb63db16056fbb9ce129f23582efccd139a4 Mon Sep 17 00:00:00 2001 From: AlecM33 Date: Sat, 14 Jan 2023 00:18:35 -0500 Subject: [PATCH] part 4 of redis effort --- client/src/config/globals.js | 1 + .../front_end_components/HTMLFragments.js | 70 +++++++- .../modules/game_state/states/InProgress.js | 6 + .../states/shared/SharedStateUtil.js | 25 +-- client/src/modules/timer/GameTimerManager.js | 20 +-- client/src/styles/GLOBAL.css | 7 + client/src/styles/game.css | 11 +- index.js | 1 - server/api/GamesAPI.js | 1 + server/config/globals.js | 19 ++- server/modules/Events.js | 157 +++++++++++++++--- server/modules/singletons/ActiveGameRunner.js | 41 +++-- server/modules/singletons/GameManager.js | 36 ++-- server/modules/singletons/SocketManager.js | 46 ++--- 14 files changed, 324 insertions(+), 117 deletions(-) diff --git a/client/src/config/globals.js b/client/src/config/globals.js index 0a487f4..80fb263 100644 --- a/client/src/config/globals.js +++ b/client/src/config/globals.js @@ -54,6 +54,7 @@ export const globals = { UPDATE_SPECTATORS: 'updateSpectators', RESTART_GAME: 'restartGame', ASSIGN_DEDICATED_MOD: 'assignDedicatedMod' + }, USER_TYPES: { MODERATOR: 'moderator', diff --git a/client/src/modules/front_end_components/HTMLFragments.js b/client/src/modules/front_end_components/HTMLFragments.js index 04aa487..ab0b68d 100644 --- a/client/src/modules/front_end_components/HTMLFragments.js +++ b/client/src/modules/front_end_components/HTMLFragments.js @@ -52,7 +52,22 @@ export const HTMLFragments = { `
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -76,7 +91,22 @@ export const HTMLFragments = { `
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -101,7 +131,22 @@ export const HTMLFragments = {
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -131,7 +176,22 @@ export const HTMLFragments = {
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -180,7 +240,7 @@ export const HTMLFragments = {
`, // via https://loading.io/css/ SPINNER: - `
+ `
diff --git a/client/src/modules/game_state/states/InProgress.js b/client/src/modules/game_state/states/InProgress.js index 76b76cb..acb0f1c 100644 --- a/client/src/modules/game_state/states/InProgress.js +++ b/client/src/modules/game_state/states/InProgress.js @@ -51,6 +51,12 @@ export class InProgress { globals.EVENT_IDS.GET_TIME_REMAINING, this.stateBucket.currentGameState.accessCode ); + setTimeout(() => { + if (this.socket.hasListeners(globals.EVENT_IDS.GET_TIME_REMAINING)) { + document.getElementById('game-timer').innerText = 'could not retrieve'; + document.getElementById('game-timer').classList.add('timer-error'); + } + }, 15000); } else { document.querySelector('#game-timer')?.remove(); document.querySelector('#timer-container-moderator')?.remove(); diff --git a/client/src/modules/game_state/states/shared/SharedStateUtil.js b/client/src/modules/game_state/states/shared/SharedStateUtil.js index 2ac7d92..8fa742f 100644 --- a/client/src/modules/game_state/states/shared/SharedStateUtil.js +++ b/client/src/modules/game_state/states/shared/SharedStateUtil.js @@ -118,20 +118,21 @@ export const SharedStateUtil = { const accessCode = splitUrl[1]; if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH) { socket.emit(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.FETCH_GAME_STATE, accessCode, { personId: cookie }, function (gameState) { - if (gameState === null) { - window.location = '/not-found?reason=' + encodeURIComponent('game-not-found'); - } else { - stateBucket.currentGameState = gameState; - document.querySelector('.spinner-container')?.remove(); - document.querySelector('.spinner-background')?.remove(); - document.getElementById('game-content').innerHTML = HTMLFragments.INITIAL_GAME_DOM; - toast('You are connected.', 'success', true, true, 'short'); - processGameState(stateBucket.currentGameState, cookie, socket, true, true); - } + // if (gameState === null) { + // window.location = '/not-found?reason=' + encodeURIComponent('game-not-found'); + // } else { + stateBucket.currentGameState = gameState; + document.querySelector('.spinner-container')?.remove(); + document.querySelector('.spinner-background')?.remove(); + document.getElementById('game-content').innerHTML = HTMLFragments.INITIAL_GAME_DOM; + toast('You are connected.', 'success', true, true, 'short'); + processGameState(stateBucket.currentGameState, cookie, socket, true, true); + // } }); - } else { - window.location = '/not-found?reason=' + encodeURIComponent('invalid-access-code'); } + // else { + // window.location = '/not-found?reason=' + encodeURIComponent('invalid-access-code'); + // } }, buildSpectatorList (people) { diff --git a/client/src/modules/timer/GameTimerManager.js b/client/src/modules/timer/GameTimerManager.js index e44d8cd..0e1b6be 100644 --- a/client/src/modules/timer/GameTimerManager.js +++ b/client/src/modules/timer/GameTimerManager.js @@ -123,17 +123,15 @@ export class GameTimerManager { }); } - if (!socket.hasListeners(globals.COMMANDS.GET_TIME_REMAINING)) { - socket.on(globals.COMMANDS.GET_TIME_REMAINING, (timeRemaining, paused) => { - if (paused) { - this.displayPausedTime(timeRemaining); - } else if (timeRemaining === 0) { - this.displayExpiredTime(); - } else { - this.resumeGameTimer(timeRemaining, globals.CLOCK_TICK_INTERVAL_MILLIS, null, timerWorker); - } - }); - } + socket.once(globals.COMMANDS.GET_TIME_REMAINING, (timeRemaining, paused) => { + if (paused) { + this.displayPausedTime(timeRemaining); + } else if (timeRemaining === 0) { + this.displayExpiredTime(); + } else { + this.resumeGameTimer(timeRemaining, globals.CLOCK_TICK_INTERVAL_MILLIS, null, timerWorker); + } + }); if (!socket.hasListeners(globals.COMMANDS.END_TIMER)) { socket.on(globals.COMMANDS.END_TIMER, () => { diff --git a/client/src/styles/GLOBAL.css b/client/src/styles/GLOBAL.css index b634323..1086231 100644 --- a/client/src/styles/GLOBAL.css +++ b/client/src/styles/GLOBAL.css @@ -710,6 +710,13 @@ input { display: inline-block; width: 80px; } + +.lds-spinner-clock { + position: absolute; + transform: scale(0.3); + top: -23px; +} + .lds-spinner div { transform-origin: 40px 40px; animation: lds-spinner 1.2s linear infinite; diff --git a/client/src/styles/game.css b/client/src/styles/game.css index b588c67..903f025 100644 --- a/client/src/styles/game.css +++ b/client/src/styles/game.css @@ -348,12 +348,18 @@ h1 { font-size: 35px; text-shadow: 0 3px 4px rgb(0 0 0 / 85%); border: 1px solid #747474; - min-width: 5em; + width: 204px; display: flex; justify-content: center; align-items: center; height: 43px; - margin-bottom: 0.5em; + margin-bottom: 15px; + position: relative; +} + +.timer-error { + color: #c51212 !important; + font-size: 20px !important; } #game-timer.low { @@ -861,6 +867,7 @@ canvas { padding: 5px; font-size: 30px; height: 30px; + width: 167px; } #role-info-button, #mod-transfer-button { diff --git a/index.js b/index.js index bdfc480..dcfb3f1 100644 --- a/index.js +++ b/index.js @@ -37,7 +37,6 @@ const main = async () => { console.log('Root Redis client connected'); await singletons.activeGameRunner.createGameSyncSubscriber(singletons.gameManager, singletons.socketManager); await singletons.socketManager.createRedisPublisher(); - await singletons.gameManager.createRedisPublisher(); const socketServer = singletons.socketManager.createSocketServer(webServer, app, port); singletons.gameManager.setGameSocketNamespace(singletons.socketManager.createGameSocketNamespace(socketServer, logger, singletons.gameManager)); diff --git a/server/api/GamesAPI.js b/server/api/GamesAPI.js index f7b6b78..c645c6b 100644 --- a/server/api/GamesAPI.js +++ b/server/api/GamesAPI.js @@ -83,6 +83,7 @@ router.patch('/:code/players', async function (req, res) { gameManager.joinGame(game, req.body.playerName, inUseCookie, req.body.joinAsSpectator).then((data) => { res.status(200).send({ cookie: data, environment: gameManager.environment }); }).catch((data) => { + console.log(data); res.status(data.status).send(data.reason); }); } else { diff --git a/server/config/globals.js b/server/config/globals.js index 9e03373..6618cdc 100644 --- a/server/config/globals.js +++ b/server/config/globals.js @@ -40,7 +40,10 @@ const globals = { START_GAME: 'startGame', PAUSE_TIMER: 'pauseTimer', RESUME_TIMER: 'resumeTimer', + END_TIMER: 'endTimer', GET_TIME_REMAINING: 'getTimeRemaining', + SOURCE_TIME_REMAINING: 'sourceTimeRemaining', + SHARE_TIME_REMAINING: 'shareTimeRemaining', KILL_PLAYER: 'killPlayer', REVEAL_PLAYER: 'revealPlayer', TRANSFER_MODERATOR: 'transferModerator', @@ -58,9 +61,6 @@ const globals = { 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, @@ -68,11 +68,20 @@ const globals = { this.EVENT_IDS.RESTART_GAME, this.EVENT_IDS.PLAYER_JOINED, this.EVENT_IDS.ADD_SPECTATOR, - this.EVENT_IDS.REMOVE_SPECTATOR, this.EVENT_IDS.SYNC_GAME_STATE, this.EVENT_IDS.UPDATE_SOCKET, this.EVENT_IDS.FETCH_GAME_STATE, - this.EVENT_IDS.ASSIGN_DEDICATED_MOD + this.EVENT_IDS.ASSIGN_DEDICATED_MOD, + this.EVENT_IDS.RESUME_TIMER, + this.EVENT_IDS.PAUSE_TIMER, + this.EVENT_IDS.END_TIMER + ]; + }, + TIMER_EVENTS: function () { + return [ + this.EVENT_IDS.RESUME_TIMER, + this.EVENT_IDS.PAUSE_TIMER, + this.EVENT_IDS.END_TIMER ]; }, MESSAGES: { diff --git a/server/modules/Events.js b/server/modules/Events.js index 65cf6a6..b0f7ea6 100644 --- a/server/modules/Events.js +++ b/server/modules/Events.js @@ -5,7 +5,7 @@ const EVENT_IDS = globals.EVENT_IDS; const Events = [ { id: EVENT_IDS.PLAYER_JOINED, - stateChange: (game, socketArgs, vars) => { + stateChange: async (game, socketArgs, vars) => { const toBeAssignedIndex = game.people.findIndex( (person) => person.id === socketArgs.id && person.assigned === false ); @@ -14,7 +14,7 @@ const Events = [ game.isFull = vars.gameManager.isGameFull(game); } }, - communicate: (game, socketArgs, vars) => { + communicate: async (game, socketArgs, vars) => { vars.gameManager.namespace.in(game.accessCode).emit( globals.EVENTS.PLAYER_JOINED, GameStateCurator.mapPerson(socketArgs), @@ -24,10 +24,10 @@ const Events = [ }, { id: EVENT_IDS.ADD_SPECTATOR, - stateChange: (game, socketArgs, vars) => { + stateChange: async (game, socketArgs, vars) => { game.people.push(socketArgs); }, - communicate: (game, socketArgs, vars) => { + communicate: async (game, socketArgs, vars) => { vars.gameManager.namespace.in(game.accessCode).emit( globals.EVENT_IDS.ADD_SPECTATOR, GameStateCurator.mapPerson(socketArgs) @@ -36,14 +36,14 @@ const Events = [ }, { id: EVENT_IDS.FETCH_GAME_STATE, - stateChange: (game, socketArgs, vars) => { + stateChange: async (game, socketArgs, vars) => { const matchingPerson = vars.gameManager.findPersonByField(game, 'cookie', socketArgs.personId); if (matchingPerson && matchingPerson.socketId !== vars.socketId) { matchingPerson.socketId = vars.socketId; vars.gameManager.namespace.sockets.get(vars.socketId)?.join(game.accessCode); } }, - communicate: (game, socketArgs, vars) => { + communicate: async (game, socketArgs, vars) => { if (!vars.ackFn) return; const matchingPerson = vars.gameManager.findPersonByField(game, 'cookie', socketArgs.personId); if (matchingPerson && vars.gameManager.namespace.sockets.get(matchingPerson.socketId)) { @@ -64,8 +64,8 @@ const Events = [ // } { id: EVENT_IDS.SYNC_GAME_STATE, - stateChange: (game, socketArgs, vars) => {}, - communicate: (game, socketArgs, vars) => { + stateChange: async (game, socketArgs, vars) => {}, + communicate: async (game, socketArgs, vars) => { const matchingPerson = vars.gameManager.findPersonByField(game, 'id', socketArgs.personId); if (matchingPerson && vars.gameManager.namespace.sockets.get(matchingPerson.socketId)) { vars.gameManager.namespace.to(matchingPerson.socketId).emit(globals.EVENTS.SYNC_GAME_STATE); @@ -74,16 +74,16 @@ const Events = [ }, { id: EVENT_IDS.START_GAME, - stateChange: (game, socketArgs, vars) => { + stateChange: async (game, socketArgs, vars) => { if (game.isFull) { game.status = globals.STATUS.IN_PROGRESS; if (game.hasTimer) { game.timerParams.paused = true; - // this.activeGameRunner.runGame(game, namespace); + await vars.activeGameRunner.runGame(game, vars.gameManager.namespace, vars.socketManager, vars.gameManager); } } }, - communicate: (game, socketArgs, vars) => { + communicate: async (game, socketArgs, vars) => { if (vars.ackFn) { vars.ackFn(); } @@ -92,7 +92,7 @@ const Events = [ }, { id: EVENT_IDS.KILL_PLAYER, - stateChange: (game, socketArgs, vars) => { + stateChange: async (game, socketArgs, vars) => { const person = game.people.find((person) => person.id === socketArgs.personId); if (person && !person.out) { person.userType = globals.USER_TYPES.KILLED_PLAYER; @@ -100,7 +100,7 @@ const Events = [ person.killed = true; } }, - communicate: (game, socketArgs, vars) => { + communicate: async (game, socketArgs, vars) => { const person = game.people.find((person) => person.id === socketArgs.personId); if (person) { vars.gameManager.namespace.in(game.accessCode).emit(globals.EVENT_IDS.KILL_PLAYER, person.id); @@ -109,13 +109,13 @@ const Events = [ }, { id: EVENT_IDS.REVEAL_PLAYER, - stateChange: (game, socketArgs, vars) => { + stateChange: async (game, socketArgs, vars) => { const person = game.people.find((person) => person.id === socketArgs.personId); if (person && !person.revealed) { person.revealed = true; } }, - communicate: (game, socketArgs, vars) => { + communicate: async (game, socketArgs, vars) => { const person = game.people.find((person) => person.id === socketArgs.personId); if (person) { vars.gameManager.namespace.in(game.accessCode).emit( @@ -131,7 +131,7 @@ const Events = [ }, { id: EVENT_IDS.END_GAME, - stateChange: (game, socketArgs, vars) => { + stateChange: async (game, socketArgs, vars) => { game.status = globals.STATUS.ENDED; // if (this.activeGameRunner.timerThreads[game.accessCode]) { // this.logger.trace('KILLING TIMER PROCESS FOR ENDED GAME ' + game.accessCode); @@ -141,7 +141,7 @@ const Events = [ person.revealed = true; } }, - communicate: (game, socketArgs, vars) => { + communicate: async (game, socketArgs, vars) => { vars.gameManager.namespace.in(game.accessCode) .emit(globals.EVENT_IDS.END_GAME, GameStateCurator.mapPeopleForModerator(game.people)); if (vars.ackFn) { @@ -151,7 +151,7 @@ const Events = [ }, { id: EVENT_IDS.TRANSFER_MODERATOR, - stateChange: (game, socketArgs, vars) => { + stateChange: async (game, socketArgs, vars) => { const currentModerator = vars.gameManager.findPersonByField(game, 'id', game.currentModeratorId); const toTransferTo = vars.gameManager.findPersonByField(game, 'id', socketArgs.personId); if (currentModerator) { @@ -167,7 +167,7 @@ const Events = [ game.currentModeratorId = toTransferTo.id; } }, - communicate: (game, socketArgs, vars) => { + communicate: async (game, socketArgs, vars) => { if (vars.ackFn) { vars.ackFn(); } @@ -176,7 +176,7 @@ const Events = [ }, { id: EVENT_IDS.ASSIGN_DEDICATED_MOD, - stateChange: (game, socketArgs, vars) => { + stateChange: async (game, socketArgs, vars) => { const currentModerator = vars.gameManager.findPersonByField(game, 'id', game.currentModeratorId); const toTransferTo = vars.gameManager.findPersonByField(game, 'id', socketArgs.personId); if (currentModerator && toTransferTo) { @@ -191,7 +191,7 @@ const Events = [ game.currentModeratorId = toTransferTo.id; } }, - communicate: (game, socketArgs, vars) => { + communicate: async (game, socketArgs, vars) => { const moderator = vars.gameManager.findPersonByField(game, 'id', game.currentModeratorId); const moderatorSocket = vars.gameManager.namespace.sockets.get(moderator?.socketId); if (moderator && moderatorSocket) { @@ -208,13 +208,124 @@ const Events = [ }, { id: EVENT_IDS.RESTART_GAME, - stateChange: (game, socketArgs, vars) => {}, - communicate: (game, socketArgs, vars) => { + stateChange: async (game, socketArgs, vars) => {}, + communicate: async (game, socketArgs, vars) => { if (vars.ackFn) { vars.ackFn(); } vars.gameManager.namespace.in(game.accessCode).emit(globals.EVENT_IDS.RESTART_GAME); } + }, + { + id: EVENT_IDS.GET_TIME_REMAINING, + stateChange: async (game, socketArgs, vars) => {}, + communicate: async (game, socketArgs, vars) => { + const thread = vars.activeGameRunner.timerThreads[game.accessCode]; + if (thread && (!thread.killed && thread.exitCode === null)) { + thread.send({ + command: globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, + accessCode: game.accessCode, + socketId: vars.socketId, + logLevel: vars.logger.logLevel + }); + } else if (thread) { + console.log(game.timerParams); + if (game.timerParams && game.timerParams.timeRemaining === 0) { + vars.gameManager.namespace.to(vars.socketId) + .emit(globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused); + await vars.socketManager.publisher.publish( + globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, + game.accessCode + ';' + globals.EVENT_IDS.SHARE_TIME_REMAINING + ';' + + JSON.stringify({ + socketId: vars.socketId, + timeRemaining: game.timerParams.timeRemaining, + paused: game.timerParams.paused + }) + + ';' + vars.instanceId + ); + } + } else { // we need to consult another container for the timer data + await vars.socketManager.publisher?.publish( + globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, + game.accessCode + ';' + globals.EVENT_IDS.SOURCE_TIME_REMAINING + ';' + JSON.stringify({ socketId: vars.socketId }) + ';' + vars.instanceId + ); + } + } + }, + { + /* unlike the GET_TIME_REMAINING event, this event is a request from another instance for timer data. In response + * to this event, this instance will check if it is home to a particular timer thread. */ + id: EVENT_IDS.SOURCE_TIME_REMAINING, + stateChange: async (game, socketArgs, vars) => {}, + communicate: async (game, socketArgs, vars) => { + const thread = vars.activeGameRunner.timerThreads[game.accessCode]; + if (thread && (!thread.killed && thread.exitCode === null)) { + thread.send({ + command: globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, + accessCode: game.accessCode, + socketId: socketArgs.socketId, + logLevel: vars.logger.logLevel + }); + } else if (thread) { + if (game.timerParams && game.timerParams.timeRemaining === 0) { + vars.gameManager.namespace.to(vars.socketId) + .emit(globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused); + } + await vars.socketManager.publisher.publish( + globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, + game.accessCode + ';' + globals.EVENT_IDS.SHARE_TIME_REMAINING + ';' + + JSON.stringify({ + socketId: socketArgs.socketId, + timeRemaining: game.timerParams.timeRemaining, + paused: game.timerParams.paused + }) + + ';' + vars.instanceId + ); + } + } + }, + { + /* This is an event fired when an instance receives timer data from another instance. In this case, we should check if the socket id + * given in the message is connected to this namespace. If it is, emit the time remaining to them. */ + id: EVENT_IDS.SHARE_TIME_REMAINING, + stateChange: async (game, socketArgs, vars) => {}, + communicate: async (game, socketArgs, vars) => { + const socket = vars.gameManager.namespace.sockets.get(socketArgs.socketId); + if (socket) { + vars.gameManager.namespace.to(socket.id) + .emit(globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, socketArgs.timeRemaining, socketArgs.paused); + } + } + }, + { + id: EVENT_IDS.END_TIMER, + stateChange: async (game, socketArgs, vars) => { + game.timerParams.paused = false; + game.timerParams.timeRemaining = 0; + }, + communicate: async (game, socketArgs, vars) => { + vars.gameManager.namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.END_TIMER); + } + }, + { + id: EVENT_IDS.PAUSE_TIMER, + stateChange: async (game, socketArgs, vars) => { + game.timerParams.paused = true; + game.timerParams.timeRemaining = socketArgs.timeRemaining; + }, + communicate: async (game, socketArgs, vars) => { + vars.gameManager.namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER, game.timerParams.timeRemaining); + } + }, + { + id: EVENT_IDS.RESUME_TIMER, + stateChange: async (game, socketArgs, vars) => { + game.timerParams.paused = false; + game.timerParams.timeRemaining = socketArgs.timeRemaining; + }, + communicate: async (game, socketArgs, vars) => { + vars.gameManager.namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.RESUME_TIMER, game.timerParams.timeRemaining); + } } ]; diff --git a/server/modules/singletons/ActiveGameRunner.js b/server/modules/singletons/ActiveGameRunner.js index bf46d7b..77b2351 100644 --- a/server/modules/singletons/ActiveGameRunner.js +++ b/server/modules/singletons/ActiveGameRunner.js @@ -12,7 +12,6 @@ class ActiveGameRunner { this.timerThreads = {}; this.logger = logger; this.client = redis.createClient(); - this.publisher = null; this.subscriber = null; this.instanceId = instanceId; ActiveGameRunner.instance = this; @@ -56,40 +55,48 @@ class ActiveGameRunner { /* We're only going to fork a child process for games with a timer. They will report back to the parent process whenever the timer is up. */ - runGame = (game, namespace) => { + runGame = async (game, namespace, socketManager, gameManager) => { this.logger.debug('running game ' + game.accessCode); const gameProcess = fork(path.join(__dirname, '../GameProcess.js')); this.timerThreads[game.accessCode] = gameProcess; + console.log(this.timerThreads); this.logger.debug('game ' + game.accessCode + ' now associated with subProcess ' + gameProcess.pid); - gameProcess.on('message', (msg) => { + gameProcess.on('message', async (msg) => { + game = await this.getActiveGame(game.accessCode); switch (msg.command) { case globals.GAME_PROCESS_COMMANDS.END_TIMER: - game.timerParams.paused = false; - game.timerParams.timeRemaining = 0; - namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.END_TIMER); + await socketManager.handleEventById(globals.EVENT_IDS.END_TIMER, game, msg.socketId, game.accessCode, msg, null, false); this.logger.trace('PARENT: END TIMER'); break; case globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER: - game.timerParams.paused = true; - this.logger.trace(msg); - game.timerParams.timeRemaining = msg.timeRemaining; - this.logger.trace('PARENT: PAUSE TIMER'); - namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER, game.timerParams.timeRemaining); + await socketManager.handleEventById(globals.EVENT_IDS.PAUSE_TIMER, game, msg.socketId, game.accessCode, msg, null, false); break; case globals.GAME_PROCESS_COMMANDS.RESUME_TIMER: - game.timerParams.paused = false; - this.logger.trace(msg); - game.timerParams.timeRemaining = msg.timeRemaining; - this.logger.trace('PARENT: RESUME TIMER'); - namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.RESUME_TIMER, game.timerParams.timeRemaining); + await socketManager.handleEventById(globals.EVENT_IDS.RESUME_TIMER, game, msg.socketId, game.accessCode, msg, null, false); break; case globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING: this.logger.trace(msg); game.timerParams.timeRemaining = msg.timeRemaining; this.logger.trace('PARENT: GET TIME REMAINING'); - namespace.to(msg.socketId).emit(globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused); + msg.paused = game.timerParams.paused; + await socketManager.publisher.publish( + globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, + game.accessCode + ';' + globals.EVENT_IDS.SHARE_TIME_REMAINING + ';' + JSON.stringify(msg) + ';' + this.instanceId + ); + const socket = namespace.sockets.get(msg.socketId); + if (socket) { + namespace.to(socket.id).emit(globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused); + } break; } + + if (globals.SYNCABLE_EVENTS().includes(msg.command)) { + await gameManager.refreshGame(game); + await socketManager.publisher.publish( + globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, + game.accessCode + ';' + msg.command + ';' + JSON.stringify(msg) + ';' + this.instanceId + ); + } }); gameProcess.on('exit', (code, signal) => { diff --git a/server/modules/singletons/GameManager.js b/server/modules/singletons/GameManager.js index 3b2045c..3157c63 100644 --- a/server/modules/singletons/GameManager.js +++ b/server/modules/singletons/GameManager.js @@ -4,7 +4,6 @@ const Person = require('../../model/Person'); const GameStateCurator = require('../GameStateCurator'); const UsernameGenerator = require('../UsernameGenerator'); const GameCreationRequest = require('../../model/GameCreationRequest'); -const redis = require('redis'); class GameManager { constructor (logger, environment, instanceId) { @@ -17,17 +16,10 @@ class GameManager { this.activeGameRunner = null; this.socketManager = null; this.namespace = null; - this.publisher = null; this.instanceId = instanceId; GameManager.instance = this; } - createRedisPublisher = async () => { - this.publisher = redis.createClient(); - await this.publisher.connect(); - this.logger.info('GAME MANAGER - CREATED GAME SYNC PUBLISHER'); - } - setGameSocketNamespace = (namespace) => { this.namespace = namespace; }; @@ -164,7 +156,7 @@ class GameManager { ) { return Promise.reject({ status: 400, reason: 'There are too many people already spectating.' }); } else if (joinAsSpectator) { - return await addSpectator(game, name, this.logger, this.namespace, this.publisher, this.instanceId, this.refreshGame); + return await addSpectator(game, name, this.logger, this.namespace, this.socketManager.publisher, this.instanceId, this.refreshGame); } const unassignedPerson = this.findPersonByField(game, 'id', game.currentModeratorId).assigned === false ? this.findPersonByField(game, 'id', game.currentModeratorId) @@ -180,7 +172,7 @@ class GameManager { GameStateCurator.mapPerson(unassignedPerson), game.isFull ); - await this.publisher?.publish( + await this.activeGameRunner.publisher?.publish( globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, game.accessCode + ';' + globals.EVENT_IDS.PLAYER_JOINED + ';' + JSON.stringify(unassignedPerson) + ';' + this.instanceId ); @@ -189,21 +181,21 @@ class GameManager { if (game.people.filter(person => person.userType === globals.USER_TYPES.SPECTATOR).length === globals.MAX_SPECTATORS) { return Promise.reject({ status: 400, reason: 'This game has reached the maximum number of players and spectators.' }); } - return await addSpectator(game, name, this.logger, this.namespace, this.publisher, this.instanceId, this.refreshGame); + return await addSpectator(game, name, this.logger, this.namespace, this.socketManager.publisher, this.instanceId, this.refreshGame); } }; restartGame = async (game, namespace) => { // kill any outstanding timer threads - // const subProcess = this.activeGameRunner.timerThreads[game.accessCode]; - // if (subProcess) { - // if (!subProcess.killed) { - // this.logger.info('Killing timer process ' + subProcess.pid + ' for: ' + game.accessCode); - // this.activeGameRunner.timerThreads[game.accessCode].kill(); - // } - // this.logger.debug('Deleting reference to subprocess ' + subProcess.pid); - // delete this.activeGameRunner.timerThreads[game.accessCode]; - // } + const subProcess = this.activeGameRunner.timerThreads[game.accessCode]; + if (subProcess) { + if (!subProcess.killed) { + this.logger.info('Killing timer process ' + subProcess.pid + ' for: ' + game.accessCode); + this.activeGameRunner.timerThreads[game.accessCode].kill(); + } + this.logger.debug('Deleting reference to subprocess ' + subProcess.pid); + delete this.activeGameRunner.timerThreads[game.accessCode]; + } // re-shuffle the deck const cards = []; @@ -238,11 +230,11 @@ class GameManager { game.status = globals.STATUS.IN_PROGRESS; if (game.hasTimer) { game.timerParams.paused = true; - this.activeGameRunner.runGame(game, namespace); + await this.activeGameRunner.runGame(game, namespace, this.socketManager, this); } await this.refreshGame(game); - await this.publisher?.publish( + await this.socketManager.publisher?.publish( globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, game.accessCode + ';' + globals.EVENT_IDS.RESTART_GAME + ';' + JSON.stringify({}) + ';' + this.instanceId ); diff --git a/server/modules/singletons/SocketManager.js b/server/modules/singletons/SocketManager.js index 0584f54..4ec3ea9 100644 --- a/server/modules/singletons/SocketManager.js +++ b/server/modules/singletons/SocketManager.js @@ -65,14 +65,18 @@ class SocketManager { socket.on(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, async (eventId, accessCode, args = null, ackFn = null) => { const game = await this.activeGameRunner.getActiveGame(accessCode); if (game) { - await this.handleAndSyncEvent(eventId, game, socket, args, ackFn); + if (globals.TIMER_EVENTS().includes(eventId)) { + await this.handleAndSyncTimerEvent(eventId, game, socket, args, ackFn, false); + } else { + await this.handleAndSyncSocketEvent(eventId, game, socket, args, ackFn, false); + } } else { ackFn(null); } }); }; - handleAndSyncEvent = async (eventId, game, socket, socketArgs, ackFn) => { + handleAndSyncSocketEvent = async (eventId, game, socket, socketArgs, ackFn) => { await this.handleEventById(eventId, game, socket?.id, game.accessCode, socketArgs, 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)) { @@ -84,20 +88,7 @@ class SocketManager { } } - handleEventById = async (eventId, game, socketId, accessCode, socketArgs, ackFn, syncOnly) => { - this.logger.trace('ARGS TO HANDLER: ' + JSON.stringify(socketArgs)); - const event = Events.find((event) => event.id === eventId); - const additionalVars = { - gameManager: this.gameManager, - socketId: socketId, - ackFn: ackFn - }; - if (event) { - if (!syncOnly) { - event.stateChange(game, socketArgs, additionalVars); - } - event.communicate(game, socketArgs, additionalVars); - } + handleAndSyncTimerEvent = async (eventId, game, socketId, accessCode, socketArgs, ackFn, syncOnly) => { switch (eventId) { case EVENT_IDS.PAUSE_TIMER: await this.gameManager.pauseTimer(game, this.logger); @@ -105,13 +96,30 @@ class SocketManager { case EVENT_IDS.RESUME_TIMER: await this.gameManager.resumeTimer(game, this.logger); break; - case EVENT_IDS.GET_TIME_REMAINING: - await this.gameManager.getTimeRemaining(game, socketId); - break; default: break; } } + + handleEventById = async (eventId, game, socketId, accessCode, socketArgs, ackFn, syncOnly) => { + this.logger.trace('ARGS TO HANDLER: ' + JSON.stringify(socketArgs)); + const event = Events.find((event) => event.id === eventId); + const additionalVars = { + gameManager: this.gameManager, + activeGameRunner: this.activeGameRunner, + socketManager: this, + socketId: socketId, + ackFn: ackFn, + logger: this.logger, + instanceId: this.instanceId + }; + if (event) { + if (!syncOnly) { + await event.stateChange(game, socketArgs, additionalVars); + } + await event.communicate(game, socketArgs, additionalVars); + } + } } function registerRateLimiter (server, logger) {