Files
scrabble-online/server/src/socketserver.js

555 lines
17 KiB
JavaScript

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');
}
}