const Logger = require('./logger.js'); const WebServer = require('./webserver.js'); const Game = require('./game.js'); const Error = require('./error.js'); const Dist = require('./letter-distributions.js'); let io = {}; /** * All socket communication follows a standard call/response & event * flow, subsiquent communication call event names will be layered * with '-', for example identify-error is sent AFTER identify IF error * * Once in a game, the communiation will be less back and fourth and * more event/response based * * Again, networking code will be seperated from domain logic with sockets * * Sockets will error with HTTP error codes because it's a pretty decent * standard for standard errors that may occur. Then general errors will * be 500 errors with a description * * Clients connect to identify with an 'intent' to be placed in a game or to * be part of the lobbying system, the intent of the client defines what * sort of communication they will be recieving */ // TODO: stop trusting the users UID, lookup with their connection ID function init() { io = require('socket.io')(WebServer.Server); io.on('connection', (socket) => { Logger.info(`NEW SOCKET CIENT ID ${socket.id}`); // Pass socket onto router Router(socket); }) Logger.info('SOCKET SERVER LISTENING'); } module.exports = { init: init } async function Router(socket) { // First, ask socket to identify it's self // everything else should be event / intent // based socket.emit('identify'); // identify functions socket.on('identify', args => ClientIdentify(socket, args)); socket.on('identify-update-intent', args => UpdateIntent(socket, args)); // lobby functions socket.on('lobby-create', args => LobbyCreate(socket, args)); socket.on('lobby-join', args => LobbyJoin(socket, args)); socket.on('lobby-leave', args => LobbyLeave(socket, args)); socket.on('lobby-user-ready', args => LobbyUserReady(socket, args)); socket.on('lobby-user-unready', args => LobbyUserUnReady(socket, args)); // game functions // socket will emit game begin with play order and starting tiles // once all clients have connected with identify socket.on('lobby-game-begin', args => LobbyGameBegin(socket, args)); socket.on('game-play-turn', args => GamePlayTurn(socket, args)) socket.on('game-skip-turn', args => GamePlayTurn(socket, {skip: true})); socket.on('game-exchange-tiles', args => GameExchangeTiles(socket, args)); socket.on('disconnect', args => HandleDisconnect(socket, ...args)); } function ClientIdentify(socket, args) { const err = new Error; const user = Game.Registrar.GetUserByUID(args.userid); const intent = args.intent; if (!user) { err.addError(400, 'Bad Request', 'error-unknown-uid'); socket.emit('identify-error', err.toError); return; } if (!intent) { err.addError(400, 'Bad Request', 'error-bad-intent'); socket.emit('identify-error', err.toError); return; } const oldIntent = user.intent; Game.Registrar.ChangeUserIntent(user.uid, intent); const status = Game.Registrar.UserConnect(user.uid, socket.id, intent); // User reconnecting to game after disconnect (Not sure what this entails, mainly for debugging) if (intent === 'GAME' && oldIntent === 'GAME') { // TODO: lobby is left when user disconnects, do this properly you lazy shit const lobby = Game.Lobbies.GetLobbyByUserUID(user.uid); const game = Game.Logic.GetGameByUserUID(user.uid); if (!game) { err.addError(500, 'Internal Server Error', 'error-illegal-intent'); socket.emit('identify-error', err.toError); return; } socket.emit('identify-success', {connected: true, user: user}); socket.join(lobby.uid); Logger.game(`USER ${user.uid} (${user.username}) IS RECONNECTING TO GAME ${game.uid}`); EmitGameReconnect(user, game); return; } // If the user enters a game without transitioning, no bueno if (intent === 'GAME' && oldIntent !== 'GAMETRANSITION') { err.addError(500, 'Internal Server Error', 'error-illegal-intent'); socket.emit('identify-error', err.toError); return; } // User intends to enter a game if (intent === 'GAME' && oldIntent === 'GAMETRANSITION') { // Make sure the user is actually in this game const lobby = Game.Lobbies.GetLobbyByUserUID(user.uid); if (lobby.uid !== args.lobbyuid) { err.addError(500, 'Internal Server Error', 'error-illegal-intent'); socket.emit('identify-error', err.toError); return; } Game.Lobbies.UserConnectGame(user.uid); // Users at this point don't go through lobbying code // so make sure that they are joined into a lobby for // the networking socket.join(lobby.uid); console.log(io.sockets.adapter.rooms); // If this user was the last player in the lobby to connect // start the game and tell every connected user if (Game.Lobbies.IsLobbyReadyForGame(lobby.uid)) { Logger.debug(`ALL PLAYERS IN LOBBY ${lobby.uid} ARE CONNECTED TO GAME`); // Make sure the last user to start the game is in the correct // state to recieve the game-begin packet socket.emit('identify-success', {connected: true, user: user}); const game = Game.Logic.BeginGame(lobby); Logger.game(`GAME ${lobby.uid} IS BEGINNING`); EmitGameBegin(game); return; } } if (status === true) { socket.emit('identify-success', {connected: true, user: user}); } else if (status === 'error-taken-user-connection') { err.addError(500, 'Internal Server Error', 'error-taken-user-connection'); socket.emit('identify-error', err.toError); } else { err.addError(500, 'Internal Server Error', 'error-illegal-user'); socket.emit('identify-error', err.toError); } } // TODO: add checks to all these functions function UpdateIntent(socket, args) { const user = Game.Registrar.GetUserbyConnection(socket.id); const intent = args.intent; Game.Registrar.ChangeUserIntent(user.uid, intent); } // if i use a database in the future i need to consider that the lobby // name is not yet sanatised function LobbyCreate(socket, args) { const err = new Error; const useruid = args.user.uid; if (!useruid) { err.addError(400, 'Bad Request', 'Unknown uid'); socket.emit('lobby-create-error', err.toError); return; } if (!args.lobbyName || args.lobbyPrivate === undefined || args.lobbySpectators === undefined) { err.addError(400, 'Bad Request', 'Lobby malformed'); socket.emit('lobby-create-error', err.toError); return; } // Make sure user is who they say they are const user = Game.Registrar.GetUserbyConnection(socket.id); if (!user || user.uid != useruid) { err.addError(403, 'Forbidden', 'error-illegal-user'); socket.emit('lobby-create-error', err.toError); return; } // Make sure user isn't already in a lobby or owns one if (!Game.Lobbies.CheckUserAvailability(useruid)) { err.addError(400, 'Bad Request', 'error-taken-lobby-ownership'); socket.emit('lobby-create-error', err.toError); return; } const lobby = Game.Lobbies.RegisterLobby(useruid, args.lobbyName, args.lobbyPrivate, args.lobbySpectators); if (!lobby) { err.addError(500, 'Internal Server Error', 'error-illegal-lobby'); socket.emit('lobby-create-error', err.toError); return; } // Lobby created socket.emit('lobby-create-success', {created: true, lobby: lobby}); const lobbyJoined = Game.Lobbies.UserJoinLobby(lobby.uid, useruid, LobbyUpdateCallback); if (!lobbyJoined) { err.addError(403, 'Forbidden', 'error-cannot-join-lobby'); socket.emit('lobby-create-error', err.toError); return; } if (lobbyJoined.uid !== lobby.uid) { err.addError(500, 'Internal Server Error', 'error-illegal-lobby'); socket.emit('lobby-create-error', err.toError); return; } socket.join(lobby.uid); socket.emit('lobby-join-success', lobby); } function LobbyJoin(socket, args) { const err = new Error; const useruid = args.user.uid; if (!useruid) { err.addError(400, 'Bad Request', 'error-unknown-uid'); socket.emit('lobby-join-error', err.toError); return; } if (!args.lobbyuid || args.joinAsSpectator === undefined) { err.addError(400, 'Bad Request', 'error-malformed-lobby'); socket.emit('lobby-join-error', err.toError); return; } // Make sure user is who they say they are const user = Game.Registrar.GetUserbyConnection(socket.id); if (!user || user.uid != useruid) { err.addError(403, 'Forbidden', 'error-illegal-user'); socket.emit('lobby-join-error', err.toError); return; } // Make sure user isn't already in a lobby if (!Game.Lobbies.CheckUserAvailability(useruid)) { err.addError(400, 'Bad Request', 'error-taken-lobby-ownership'); socket.emit('lobby-join-error', err.toError); return; } const lobby = Game.Lobbies.GetLobbyByUID(args.lobbyuid); if (!lobby) { err.addError(400, 'Bad Request', 'error-lobby-not-exist'); socket.emit('lobby-join-error', err.toError); return; } if (args.joinAsSpectator) { // TODO: this lol } else { const status = Game.Lobbies.UserJoinLobby(lobby.uid, useruid, LobbyUpdateCallback); if (!status) { err.addError(403, 'Forbidden', 'error-cannot-join-lobby'); socket.emit('lobby-join-error', err.toError); return; } socket.join(lobby.uid); socket.emit('lobby-join-success', lobby); } } function LobbyLeave(socket, args) { const user = Game.Registrar.GetUserbyConnection(socket.id); const lobby = Game.Lobbies.GetLobbyByUserUID(user.uid); Logger.debug(`USER ${user.uid} (${Game.Registrar.GetUserByUID(user.uid).username}) ATTEMPTING TO LEAVE LOBBY`); socket.leave(lobby.uid); Game.Lobbies.UserLeaveLobby(user.uid, LobbyUpdateCallback); } function LobbyUserReady(socket, args) { const user = Game.Registrar.GetUserbyConnection(socket.id); const lobby = Game.Lobbies.GetLobbyByUserUID(user.uid); if (!Game.Lobbies.UserReady(user.uid, LobbyUpdateCallback)) return; Logger.debug(`USER ${user.uid} (${Game.Registrar.GetUserByUID(user.uid).username}) READY`); if (Game.Lobbies.IsLobbyReady(lobby.uid)) LobbyUpdateCallback(user, lobby, 'game-ready'); } function LobbyUserUnReady(socket, args) { const user = Game.Registrar.GetUserbyConnection(socket.id); const lobby = Game.Lobbies.GetLobbyByUserUID(user.uid); if (!Game.Lobbies.UserUnReady(user.uid, LobbyUpdateCallback)) return; Logger.debug(`USER ${user.uid} (${Game.Registrar.GetUserByUID(user.uid).username}) UNREADY`); if (!Game.Lobbies.IsLobbyReady(lobby.uid)) LobbyUpdateCallback(user, lobby, 'game-unready'); } function LobbyGameBegin(socket, args) { const user = Game.Registrar.GetUserbyConnection(socket.id); const lobby = Game.Lobbies.GetLobbyByUserUID(user.uid); // TODO: Maybe only the owner of the lobby should be able to begin the game // Tells all other clients in the lobby to change intent to transition // the clients don't need to request the server change their intent // except the host that started the transition for (const user of lobby.players) { Game.Registrar.ChangeUserIntent(user.uid, 'GAMETRANSITION'); } for (const user of lobby.spectators) { Game.Registrar.ChangeUserIntent(user.uid, 'GAMETRANSITION'); } io.to(lobby.uid).emit('request-intent-change', { intent: 'GAMETRANSITION', lobby: lobby }); } function GamePlayTurn(socket, args) { const user = Game.Registrar.GetUserbyConnection(socket.id); const game = Game.Logic.GetGameByUserUID(user.uid); if (!user || !game) { // do something bad return; } Logger.game(`USER ${user.uid} (${user.username}) IS ATTEMPTING TO PLAY A TURN IN GAME ${game.uid}`); if (args.skip === true) { const [outcome, turninfo] = Game.Logic.SkipTurn(game.uid, user.uid); io.to(game.uid).emit('game-turn-processed', { outcome: outcome }); const nextuser = Game.Registrar.GetConnectionByUser(turninfo.turnplayer.uid); io.to(game.uid).emit('game-turn-start', { turninfo: turninfo }); io.to(nextuser).emit('game-your-turn'); } else { // TODO: validate args const [err, outcome, turninfo, newuserpieces] = Game.Logic.PlayTurn(game.uid, user.uid, args) // process errors if (err) { socket.emit('game-turn-error', err); return; } io.to(game.uid).emit('game-turn-processed', { outcome: outcome }); const nextuser = Game.Registrar.GetConnectionByUser(turninfo.turnplayer.uid); io.to(game.uid).emit('game-turn-start', { turninfo: turninfo }); io.to(nextuser).emit('game-your-turn'); } require('fs').appendFileSync('../turns-debug.json', JSON.stringify(Game.Logic.GetGameByUserUID(user.uid), null, 4)); } function GameExchangeTiles(socket, args) { } function HandleDisconnect(socket, args) { const user = Game.Registrar.GetUserbyConnection(socket.id); if (!user) return; // if user is in a game, notify the game logic // if the user is the last user in a game - delete it // if the user is leaving, change their status so reconnect is allowed // TODO: VERY IMPORTANTTTT THAT^^^ // if user is in a lobby, leave and if user own's a lobby, destruct // leave lobby before user is disconnected if (user.intent !== 'GAMETRANSITION') { LobbyLeave(socket); } Game.Registrar.UserDisconnect(user.uid); Logger.info(`SOCKET ${socket.id} DISCONNECTED`); } /** * Possible states * * lobby-deregister * lobby-join * lobby-leave * user-ready * user-unready * game-ready * game-unready */ function LobbyUpdateCallback(user, lobby, state) { // Just send updated lobby object for now io.to(lobby.uid).emit('lobby-update', { state: state, updateuser: Game.Registrar.GetSafeUserByUID(user.uid), lobby: lobby }); Game.Lobbies.IsLobbyReady(lobby.uid) } // send the client their user as well as the rest of the game // works at any point during the game as the client will always // setup a game not assuming begining - also fresh games have a // populated gamestates array function EmitGameBegin(game) { // Instead of using io.to(room), i'm sending an individual packet to everyone // in the game so that i can customise the game object that they recieve for (const user of game.players) { const gameuser = game.players.filter(i => i.uid === user.uid)[0]; const gameuserconnection = Game.Registrar.GetConnectionByUser(gameuser.uid); // TODO: consider not sending all users the entire game state // due to cheating - a few more considerations and maybe a // getsafegame function is needed io.to(gameuserconnection).emit('game-begin', { game: game, tileset: Dist.GetDist(game.locale).dist, gameuser: gameuser }); } // Let starting player know it's their turn const userturnstart = Game.Logic.GetTurnUser(game.uid).uid; const userturnstartconnection = Game.Registrar.GetConnectionByUser(userturnstart); io.to(userturnstartconnection).emit('game-your-turn'); } function EmitGameReconnect(user, game) { const gameuser = game.players.filter(i => i.uid === user.uid)[0]; const gameuserconnection = Game.Registrar.GetConnectionByUser(gameuser.uid); io.to(gameuserconnection).emit('game-begin', { game: game, tileset: Dist.GetDist(game.locale).dist, gameuser: gameuser }); // If it's their turn, pass it to them // NOTE it shouldn't ever be their turn on a reconnect // as the game logic should pass control to next player // as the game order is changed const userturnstart = Game.Logic.GetTurnUser(game.uid).uid; if (userturnstart === user.uid) { const userturnstartconnection = Game.Registrar.GetConnectionByUser(userturnstart); io.to(userturnstartconnection).emit('game-your-turn'); } }