555 lines
17 KiB
JavaScript
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');
|
|
}
|
|
}
|
|
|