GameLoader

Copyright(c) 2015 Stefano Balietti MIT Licensed

Loads nodeGame games from file system

http://nodegame.org

"use strict";

module.exports = GameLoader;


Global scope


var path = require('path');
var fs = require('fs');

var GameRouter = require('./GameRouter');

var J = require('JSUS').JSUS;
var ngc = require('nodegame-client');
var GameStage = ngc.GameStage;

var serverVersionObj;


GameLoader constructor

Constructs a new instance of GameLoader

function GameLoader(servernode) {
    this.servernode = servernode;

    this.gameRouter = new GameRouter(servernode);


Cannot keep a reference to servernode.logger because it gets overwritten later on.



Transform server version string to object (used to check dependencies).

    serverVersionObj = version2GameStage(this.servernode.version);
}



GameLoader methods



GameLoader.updateGameInfo

Updates information about an existing game

Params
gameName string The name of the game
update object An object containing properties to mixin
GameLoader.prototype.updateGameInfo = function(gameName, update) {
    var gameInfo;
    if ('string' !== typeof gameName) {
        throw new TypeError('GameLoader.updateGameInfo: gameName must ' +
                            'be string.');
    }
    if ('object' !== typeof update) {
        throw new TypeError('GameLoader.updateGameInfo: update must ' +
                            'be object.');
    }

    gameInfo = this.servernode.info.games[gameName];
    if (!gameInfo) {
        throw new TypeError('GameLoader.updateGameInfo: game not found: ' +
                            gameName + '.');
    }

    J.mixin(gameInfo, update);
    return gameInfo;
};


GameLoader.addGame

Adds a new game to the global collection info.games

The version of ServerNode is compared against the information in gameInfo.engines.nodegame, if found.

Aliases will be created, if needed.

Params
gameInfo object A game descriptor, usually from package.json
gameDir string The root directory of the game (with trailing slash)
See
ServerNode.addGameDir
GameLoader.prototype.addGame = function(gameInfo, gameDir) {
    var gamePath, reqFail;
    var gameLogicPath, gameLogic, gameClientPath, gameClient;
    var clientTypes, langObject;
    var gameType, gameGame, game, gameName;
    var channel;

    if ('object' !== typeof gameInfo) {
        throw new TypeError('GameLoader.addGame: gameInfo must be object.');
    }
    if ('string' !== typeof gameDir) {
        throw new TypeError('GameLoader.addGame: gameDir must be string.');
    }

    gameName = gameInfo.name;


Check name.

    if ('string' !== typeof gameName) {
        throw new Error('GameLoader.addGame: missing or invalid game name: ' +
                        gameDir + '.');
    }


Checking nodeGame server version requirement.


    if (gameInfo.engines) {
        reqFail = gameRequirementsFail(gameInfo.engines.nodegame);
        if (reqFail) {
            throw new Error('GameLoader.addGame: game ' + gameName + ' ' +
                            'requires nodeGame version ' +
                            gameInfo.engines.nodegame  + ' ' +
                            'but found ' + this.servernode.version + '.');
        }
    }



Loading settings, setup, and stages.

    gameGame = this.loadGameGameDir(gameDir);


Loading client types.

    clientTypes = this.loadClientTypes(gameDir);


Building language object.

    langObject = this.buildLanguagesObject(gameDir);


Adding game info to global info object.

    this.servernode.info.games[gameName] = {
        dir: gameDir,
        info: gameInfo,
        clientTypes: clientTypes,
        settings: gameGame.settings,
        setup: gameGame.setup,
        stager: gameGame.stager,
        languages: langObject,
        channel: {},
        alias: []
    };


Load server dir first (channels, etc.).

    channel = this.loadChannelDir(gameDir, gameName);


Creates a waiting room.

    this.loadWaitRoomDir(gameDir, channel);


Loading Auth dir, if any.

    this.loadAuthDir(gameDir, channel);


Loading Requirements dir, if any.

    this.loadRequirementsDir(gameDir, channel);


Tries to load Views file from game directory. this.loadViewsFile(gameDir, gameInfo);



Add routes (considering other loaded options: auth, monitor, etc.).

    this.gameRouter.addRoutes(gameDir, gameName,
                              this.servernode.info.games[gameName]);


Done.

    this.servernode.logger.info('GameLoader.addGame: added ' +
                                gameName + ': ' + gameDir);
};


GameLoader.loadGameGameDir

Loads the game dir of the game

Params
directory string The path of the game directory
Returns
object An object containing three properties: 'settings', 'stager', and 'setup'.
See
GameLoader.addGame
See
parseGameSettings
GameLoader.prototype.loadGameGameDir = function(directory) {
    var gameSetupPath, gameSetup;
    var gameSettingsPath, gameSettings;
    var gameStagesPath, gameStages;
    var stager;

    if ('string' !== typeof directory) {
        throw new TypeError('GameLoader.loadGameGameDir: directory must be ' +
                            'string.');
    }


Settings.

    gameSettingsPath = directory + 'game/game.settings.js';

    try {
        gameSettings = require(gameSettingsPath);
    }
    catch(e) {
        throw new Error('GameLoader.loadGameGameDir: cannot read ' +
                        gameSettingsPath + ': ' + e.stack);
    }

    if ('object' !== typeof gameSettings) {
        throw new Error('SeverNode.loadGameGameDir: invalid settings file: ' +
                        gameSettingsPath + '.');
    }


Stages.

    gameStagesPath = directory + 'game/game.stages.js';

    try {
        gameStages = require(gameStagesPath);
    }
    catch(e) {
        throw new Error('GameLoader.loadGameGameDir: cannot read ' +
                        gameStagesPath + ': ' + e.stack);
    }

    if ('function' !== typeof gameStages) {
        throw new Error('SeverNode.loadGameGameDir: stages file did not ' +
                        'export a valid function: ' + gameStagesPath + '.');
    }

    stager = ngc.getStager();
    gameStages = gameStages(stager, gameSettings);


Setup.

    gameSetupPath = directory + 'game/game.setup.js';

    try {
        gameSetup = require(gameSetupPath);
    }
    catch(e) {
        throw new Error('GameLoader.loadGameGameDir: cannot read ' +
                        gameSetupPath + ': ' + e.stack);
    }

    if ('function' !== typeof gameSetup) {
        throw new Error('SeverNode.loadGameGameDir: setup file did not ' +
                        'export a valid function: ' + gameSetupPath + '.');
    }

    gameSetup = gameSetup(gameSettings, gameStages);

    if ('object' !== typeof gameSetup) {
        throw new Error('SeverNode.loadGameGameDir: setup function did  ' +
                        'not return a valid object: ' + gameSetupPath + '.');
    }


Parsing the settings object before returning.

    gameSettings = parseGameSettings(gameSettings);

    if ('object' !== typeof gameSettings) {
        throw new Error('SeverNode.loadGameGameDir: error parsing ' +
                        'treatment object: ' + gameSettingsPath + '.');
    }

    return {
        setup: gameSetup,
        settings: gameSettings,
        stager: stager
    };
};


GameLoader.loadClientTypes

Loads client types from game/client_types directory

Params
directory string The path of the game directory
See
GameLoader.addGame
GameLoader.prototype.loadClientTypes = function(directory) {
    var clientTypesDir, fileNames, fileName;
    var clientType, clientTypes, clientTypePath;
    var i, len;
    var logger;

    if ('string' !== typeof directory) {
        throw new TypeError('GameLoader.loadClientTypes: directory must be ' +
                            'string.');
    }

    logger = this.servernode.logger;

    clientTypesDir = directory + 'game/client_types/';

    if (!J.existsSync(clientTypesDir)) {
        logger.error('GameLoader.loadClientTypes: game directory not ' +
                     'found: ' + directory);
        return;
    }

    fileNames = fs.readdirSync(clientTypesDir);

    clientTypes = {};

    i = -1, len = fileNames.length;
    for ( ; ++i < len ; ) {
        fileName = fileNames[i];

Ignore non-js files, and temporary and hidden files (begin with '.').

        if (path.extname(fileName) === '.js' && fileName.charAt(0) !== '.') {
            clientTypePath = clientTypesDir + fileName;
            if (!fs.statSync(clientTypePath).isDirectory()) {
                try {
                    clientType = require(clientTypePath);
                    clientTypes[fileName.slice(0,-3)] = clientType;
                }
                catch(e) {
                    logger.error('GameLoader.loadClientTypes: cannot ' +
                                 'read ' + clientTypePath + ': ' + e.stack);
                    throw e;
                }
            }
        }
    }


Checking if mandatory types are found.

    if (!clientTypes.logic) {
        throw new Error('GameLoader.loadClientTypes: logic type not found.');
    }
    if (!clientTypes.player) {
        throw new Error('GameLoader.loadClientTypes: player type not found.');
    }

    return clientTypes;
};


GameLoader.buildLanguagesObject

Builds an object containing language objects for a given game directory

Params
directory string The directory of the game.
Returns
object languages Object of language objects.
GameLoader.prototype.buildLanguagesObject = function(directory) {
    var ctxPath, langPaths, languages, languageObject;
    var i, len, langFile;

    ctxPath = directory + '/views/contexts/';
    if (!fs.existsSync(ctxPath)) return;

    langPaths = fs.readdirSync(ctxPath);
    languages = {};

    i = -1, len = langPaths.length;
    for ( ; ++i < len ; ) {
        languageObject = {};

        langFile = ctxPath + langPaths[i] + '/languageInfo.json';
        if (fs.existsSync(langFile)) languageObject = require(langFile);

        languageObject.shortName = langPaths[i];
        languages[languageObject.shortName] = languageObject;
    }
    return languages;
};


GameLoader.loadChannelDir

Loads several files from channel/ dir

Read: channel.settings.js, channel.credentials.js, _channel.secret.js

Params
directory string The directory of the game.
gameName string The name of the game.
Returns
ServerChannel The created channel.
See
GameLoader.loadChannelFile
See
ServerChannel
GameLoader.prototype.loadChannelDir = function(directory, gameName) {
    var settings, pwdFile, jwtFile;
    var channel;


  1. Channel settings.
    settings = this.loadChannelFile(directory, gameName);


Save ref. to newly created channel.

    channel = this.servernode.channels[gameName];


  1. Credentials.
    pwdFile = directory + 'channel/channel.credentials.js';

    loadSyncAsync(pwdFile, settings, 'loadChannelDir', function(credentials) {

TODO: checkings. Add method.

        channel.credentials = credentials;
    });


  1. JWT secret.
    jwtFile = directory + 'channel/channel.secret.js';

    loadSyncAsync(jwtFile, settings, 'loadChannelDir', function(secret) {

TODO: checkings. Add method.

        channel.secret = secret;
    });

    return channel;
};



GameLoader.loadChannelFile

Loads channel.settings.js from file system and adds channels accordingly

Synchronously looks for a file called channel.settings.js at the top level of the specified directory.

The file channel.settings.js must export one channel object in the format specified by the ServerChannel constructor. Every channel configuration object can optionally have a waitingRoom object to automatically add a waiting room to the channel.

Params
directory string The directory of the game
gameName string The name of the game
See
ServerChannel
See
ServerChannel.createWaitingRoom
See
ServerNode.addChannel
GameLoader.prototype.loadChannelFile = function(directory, gameName) {
    var channelsFile;
    var channel, waitRoom;
    var i, len;
    var channelConf, waitRoomConf;

    if ('string' !== typeof directory) {
        throw new TypeError('GameLoader.loadChannelFile: directory must be ' +
                            'string.');
    }

    channelsFile = directory + '/channel/channel.settings.js';

    if (!fs.existsSync(channelsFile)) return;

    channelConf = require(channelsFile);


Validate


    if ('object' !== typeof channelConf) {
        throw new TypeError('GameLoader.loadChannelFile:' +
                            'channels must be object. Directory: ' +
                            directory);
    }
    if (channelConf.waitingRoom &&
        'object' !== typeof channelConf.waitingRoom) {

        throw new TypeError('GameLoader.loadChannelFile: waitingRoom ' +
                            'in channel configuration must be object. ' +
                            'Directory: ' + directory);
    }
    waitRoomConf = channelConf.waitingRoom;
    delete channelConf.waitingRoom;

    if (!channelConf.name) channelConf.name = gameName;
    channelConf.gameName = gameName;

    channel = this.servernode.addChannel(channelConf);

    if (channel) {


Add the list of channels created by the game.

        this.servernode.info.games[gameName].channel = {
            player: channel.playerServer ?
                channel.playerServer.endpoint : null,
            admin: channel.adminServer ?
                channel.adminServer.endpoint : null
        };

        if (waitRoomConf) {


We prepend the baseDir parameter, if any. If logicPath is not string we let the WaitingRoom constructor throw an error.

            if ('string' === typeof waitRoomConf.logicPath) {
                waitRoomConf.logicPath = checkLocalPath(
                    directory, 'channel/', waitRoomConf.logicPath);
            }

            waitRoom = channel.createWaitingRoom(waitRoomConf);

            if (!waitRoom) {
                throw new Error('GameLoader.loadChannelFile: could ' +
                                'not add waiting room to channel ' +
                                channel.name);
            }
        }


Adding channel alias.

        if (channelConf.alias) {
            this.createGameAlias(channelConf.alias, gameName);
        }

    }

Errors in channel creation are already logged.


    return channelConf;
};


GameLoader.loadWaitRoomDir

Loads waitroom/room.settings.js from file system and acts accordingly

Synchronously looks for a file called waitroom/room.settings.js at the top level of the specified directory.

Params
directory string The directory of the game
channel ServerChannel The channel object
Returns
WaitingRoom waitRoom The created waiting room
See
ServerChannel.createWaitingRoom
GameLoader.prototype.loadWaitRoomDir = function(directory, channel) {
    var waitRoomSettingsFile, waitRoomFile, conf, waitRoom;
    var logger, gameName;

    if ('string' !== typeof directory) {
        throw new TypeError('GameLoader.loadWaitRoomDir: directory must be ' +
                            'string.');
    }
    logger = this.servernode.logger;
    gameName = channel.gameName;

    waitRoomSettingsFile = directory + 'waitroom/waitroom.settings.js';

    if (!fs.existsSync(waitRoomSettingsFile)) {
        logger.error('GameLoader.loadWaitRoomDir: waitroom.settings.js ' +
                     'not found. Game: ' + gameName + '.');
        return;
    }

    try {
        conf = require(waitRoomSettingsFile);
    }
    catch(e) {
        logger.error('GameLoader.loadWaitRoomDir: error reading ' +
                     'waitroom.settings file. Game: ' + gameName + ': ' +
                    e.stack);
        throw e;
    }

    if ('object' !== typeof conf) {
        throw new TypeError('GameLoader.loadWaitRoomDir: room.settings file ' +
                            'must return a configuration object.');
    }

    if (conf.logicPath) {
        if ('string' === typeof conf.logicPath) {
            throw new TypeError('GameLoader.loadWaitRoomDir: configuration ' +
                                'loaded, but "logicPath" must be undefined ' +
                                'or string.');
        }
        conf.logicPath = checkLocalPath(directory, 'waitroom/', conf.logicPath);
    }
    else {
        waitRoomFile = directory + 'waitroom/waitroom.js';
        if (fs.existsSync(waitRoomFile)) {
            conf.logicPath = waitRoomFile;
        }
    }


Add waiting room information to global game object.

    this.updateGameInfo(gameName, { waitroom: conf });

    waitRoom = channel.createWaitingRoom(conf);

    if (!waitRoom) {
        throw new Error('GameLoader.loadWaitRoomDir: could not create ' +
                        'waiting room. Game: ' + gameName + '.');
    }

    return waitRoom;
};


GameLoader.loadAuthDir

Reads the auth/ directory and imports settings from it

If a function is found in auth/auth.js it calls it with an object containing a sandboxed environment for authorization, clientId generation and clientObject decoration.

Params
file string The path to the configuration file
channel ServerChannel The channel object
See
GameLoader.getChannelSandbox
GameLoader.prototype.loadAuthDir = function(directory, channel) {
    var authFile, authSettingsFile, authCodesFile;
    var settings, codes;
    var authObj, sandboxAuth, sandboxIdGen, sandboxDecorate;
    var gameName;
    var logger;

    if ('string' !== typeof directory) {
        throw new TypeError('GameLoader.loadAuthDir: directory must be ' +
                            'string.');
    }

    gameName = channel.gameName;
    logger = this.servernode.logger;


Check if directory exists.

    if (!fs.existsSync(directory + 'auth/')) {
        logger.warn('GameLoader.loadRequirementsDir: channel ' + gameName +
                    ': no auth directory.');1


Add information to global game object.

        this.updateGameInfo(gameName, { auth: { enabled: false } });
        return;
    }


Auth settings.


    authSettingsFile = directory + 'auth/auth.settings.js';

    if (!fs.existsSync(authSettingsFile)) {
        settings = {
            enabled: true,
            mode: 'auto',
            codes: 'auth.codes'
        };
        logger.warn('GameLoader.loadAuthDir: channel' + gameName +
                    ': no auth settings. Default settings used.');
    }
    else {
        try {
            settings = require(authSettingsFile);
        }
        catch(e) {
            throw new Error('GameLoader.loadAuthDir: cannot read ' +
                            authSettingsFile + ': ' + e.stack);
        }

        if ('object' !== typeof settings) {
            throw new Error('GameLoader.loadAuthDir: invalid settings file: ' +
                            authSettingsFile + ': ' + e.stack);
        }
    }


Enable authorization by default.

    if ('undefined' === typeof settings.enabled) settings.enabled = true;


Add waiting room information to global game object.

    this.updateGameInfo(gameName, { auth: settings });



Exit here if auth is disabled.

    if (!settings.enabled) {
        logger.warn('GameLoader.loadAuthDir: channel ' + gameName +
                    ': authorization disabled in configuration file.');
        return;
    }


Auth codes. Can be synchronous or asynchronous.


    authCodesFile = directory + 'auth/' + (settings.codes || 'auth.codes.js');

    if (fs.existsSync(authCodesFile)) {
        try {
            codes = require(authCodesFile)(settings, function(err, codes) {
                if (err) {
                    throw new Error('GameLoader.loadAuthDir: an error ' +
                                    'occurred: ' + err);
                }
                importAuthCodes(channel, codes, authCodesFile);
            });
        }
        catch(e) {
            throw new Error('GameLoader.loadAuthDir: cannot read ' +
                            authCodesFile + ": \n" + e.stack);
        }

        if (codes) importAuthCodes(channel, codes, authCodesFile);
    }


Auth file.


    authFile = directory + 'auth/auth.js';

    if (fs.existsSync(authCodesFile)) {
        sandboxAuth = this.getChannelSandbox(channel, 'authorization');
        sandboxIdGen = this.getChannelSandbox(channel, 'clientIdGenerator');
        sandboxDecorate = this.getChannelSandbox(channel, 'clientObjDecorator');

        authObj = {
            authorization: sandboxAuth,
            clientIdGenerator: sandboxIdGen,
            clientObjDecorator: sandboxDecorate
        };

        try {
            require(authFile)(authObj, settings);
        }
        catch(e) {
            throw new Error('GameLoader.loadAuthDir: cannot read ' +
                            authCodesFile + '. ' + e.stack);
        }
    }
};


GameLoader.loadRequirementsDir

Reads the requirements/ directory and imports settings from it

Params
file string The path to the configuration file
channel ServerChannel The channel object
Returns
RequirementsRoom The requirements room (if one is created)
GameLoader.prototype.loadRequirementsDir = function(directory, channel) {
    var file, settingsFile, settings;
    var logger, gameName;
    var reqFunc, reqObj;

    if ('string' !== typeof directory) {
        throw new TypeError('GameLoader.loadRequirementsDir: directory ' +
                            'must be string.');
    }

    gameName = channel.gameName;
    logger = this.servernode.logger;

    if (!fs.existsSync(directory + 'requirements/')) {
        logger.warn('GameLoader.loadRequirementsDir: channel ' + gameName +
                    ': no requirements directory.');1


Add information to global game object.

        this.updateGameInfo(gameName, { requirements: { enabled: false } });
        return;
    }


Requirements settings.

    settingsFile = directory + 'requirements/requirements.settings.js';

    if (!fs.existsSync(settingsFile)) {
        settings = { enabled: true };
        logger.warn('GameLoader.loadRequirementsDir: channel' + gameName +
                    ': no settings file found. Default settings used.');
    }

    else {
        try {
            settings = require(settingsFile);
        }
        catch(e) {
            throw new Error('GameLoader.loadRequirementsDir: cannot read ' +
                            settingsFile + ': ' + e.stack);
        }

        if ('object' !== typeof settings) {
            throw new Error('GameLoader.loadRequirementsDir: invalid ' +
                            'settings file: ' + settingsFile + '.');
        }
    }


We prepend the baseDir parameter, if any. If logicPath is not string RequirementsRoom constructor throws an error.

    if ('string' === typeof settings.logicPath) {
        settings.logicPath = checkLocalPath(directory, 'requirements/',
                                            settings.logicPath);
    }


Enable requirements by default.

    if ('undefined' === typeof settings.enabled) settings.enabled = true;


Exit here if requirements is disabled.

    if (!settings.enabled) {
        logger.warn('GameLoader.loadRequirementsDir: channel ' + gameName +
                    ': requirements checking disabled in configuration file.');


Add information to global game object.

        this.updateGameInfo(gameName, { requirements: settings });
        return;
    }


Requirements file.


    file = directory + 'requirements/requirements.js';

    if (fs.existsSync(file)) {
        reqObj = {};
        reqFunc = new RequirementsSandbox(gameName, reqObj);

        try {
            require(file)(reqFunc, settings);
            reqObj.enabled = true;
        }
        catch(e) {
            throw new Error('GameLoader.loadRequirementsDir: channel ' +
                            gameName + ' cannot read ' + file +
                            '. \n' + e.stack);
        }
    }


Add information to global game object.

    this.updateGameInfo(gameName, {
        requirements: J.merge(settings, reqObj)
    });


Create requirements room in channel.

    return channel.createRequirementsRoom(J.merge(reqObj, settings));
};


TODO: see if we want to add it to prototype. Else remove getChannelSandbox also (maybe).


function RequirementsSandbox(gameName, requirementsObj) {
    var errBegin = 'GameLoader.loadRequirementsDir: ' + gameName + ': ';
    this.add = function(cb, params) {
        if ('function' !== typeof cb) {
            throw new TypeError(errBegin + 'requirement must be function.');
        }
        if (params && 'object' !== typeof params) {
            throw new TypeError(errBegin + 'params must be object or ' +
                                'undefined: ', params);
        }
        if (!requirementsObj.requirements) requirementsObj.requirements = [];
        requirementsObj.requirements.push({ cb: cb, params: params });
    };
    this.onSuccess = function(cb) {
        if ('function' !== typeof cb) {
            throw new TypeError(errBegin + 'onSuccess callback must be ' +
                                'function.');
        }
        requirementsObj.onSuccess = cb;
    };
    this.onFailure = function(cb) {
        if ('function' !== typeof cb) {
            throw new TypeError(errBegin + 'onFailure callback must be ' +
                                'function.');
        }
        requirementsObj.onFailure = cb;
    };
    this.setMaxExecutionTime = function(maxTime) {
        if ('number' !== typeof maxTime) {
            throw new TypeError(errBegin + 'max execution time must be ' +
                                'number:' + maxTime + '.');
        }
        requirementsObj.maxExecTime = maxTime;
    };
}


GameLoader.getChannelSandbox

Returns a sandbox function to configure a channel

Params
channelName string The name of the game
method string The name of the method in ServerChannel
methodName string Optional The name of the method as diplayed in error messages.
Returns
function The sandbox
See
GameLoader.loadAuthDir
See
GameLoader.getChannelSandbox
GameLoader.prototype.getChannelSandbox = function(channel, method, methodName) {

    var that, channelName;

    that = this;
    channelName = channel.name;
    methodName = methodName || method;

    return function() {
        var len;
        var callback;
        var errorBegin;
        var servernode;

        servernode = that.servernode;

        errorBegin = 'GameLoader.loadAuthDir: ' + methodName + ' callback: ';

        len = arguments.length;

        if (len > 2) {
            throw new Error(errorBegin +
                            'accepts maximum 2 input parameters, ' +
                            len + ' given. Game: ' + channelName + '.');
        }

        callback = arguments[len-1];

        if ('function' !== typeof callback) {
            throw new TypeError(errorBegin + 'last parameter must be ' +
                                'function. Game: ' + channelName + '.');
        }


Channels defined by a game with the channels.js file. Auth callback can modify the authorization only of those.



1 Auth for both admin and player servers.

        if (len === 1) {
            channel.adminServer[method](callback);
            channel.playerServer[method](callback);
        }


1 Auth for one server: admin or player.

        else {

            if (arguments[0] !== 'admin' && arguments[0] !== 'player') {
                throw new TypeError(errorBegin + 'server parameter must ' +
                                    'be either "player" or "admin". ' +
                                    'Given: ' + arguments[0] + '. ' +
                                    'Game: ' + channelName + '.');
            }
            channel[arguments[0] + 'Server'][method](callback);
        }
    };
};


GameLoader.createGameAlias

Adds one or multiple game alias

Adds references into the info.games and resourceManager.games objects.

Params
alias string The name of the alias
gameName string The name of the game
See
GameLoader.addGame
See
ServerChannel.resourceManager
GameLoader.prototype.createGameAlias = function(alias, gameName) {
    var servernode, gameInfo;
    var i, len;

    if ('string' === typeof alias) {
        alias = [alias];
    }
    if (!J.isArray(alias)) {
        throw new TypeError('GameLoader.createGameAlias: alias' +
                            'must be either string or array.');
    }
    if ('string' !== typeof gameName) {
        throw new TypeError('GameLoader.createGameAlias: gameName must be ' +
                            'string.');
    }

    servernode = this.servernode;
    gameInfo = servernode.getGamesInfo(gameName);
    if (!gameInfo) {
        throw new Error('GameLoader.createGameAlias: game not found: ' +
                        gameName);
    }

    i = -1;
    len = alias.length;
    for ( ; ++i < len ; ) {
        if ('string' !== typeof alias[i]) {
            throw new TypeError(
                'GameLoader.createGameAlias: alias must be string.');
        }
        if (servernode.info.games[alias[i]]) {
            throw new Error('GameLoader.createGameAlias: ' +
                            'alias must be unique: ' + alias[i] +
                            ' (' + gameName + ').');
        }


TODO: do not add aliases to the info.games object. (we need to make sure aliases do not clash).

        servernode.info.games[alias[i]] = servernode.info.games[gameName];
        servernode.resourceManager.createAlias(alias[i], gameName);
        gameInfo.alias.push(alias[i]);
    }
};


Helper methods



checkLocalPath

Helper function to specify full path in a game subdir

Checks if the file starts with './' and, if so, adds the absolute path to the game directory in front.

Params
gameDir string The absolute path of the game directory
subDir string The name of the game sub directory
filePath string The path of the file in the sub directory
Returns
string The updated file path (if local)
function checkLocalPath(gameDir, subDir, filePath) {
    if (filePath.substring(0,2) === './') {
        return path.resolve(gameDir, subDir, filePath);
    }
    return filePath;
}


version2GameStage

Helper function to parse a version string into a GameStage

Cannot use GameStage constructor because it does not accept stage = 0

Params
str string The version number to parse
See
GameStage constructor
function version2GameStage(str) {
    var tokens, stage, step, round;
    var out;
    tokens = str.trim().split('.');
    stage = parseInt(tokens[0], 10);
    if (isNaN(stage)) return false;
    step  = parseInt(tokens[1], 10);
    if (isNaN(step)) return false;
    round = parseInt(tokens[2], 10);
    if (isNaN(round)) return false;
    return {
        stage: stage, step: step, round: round
    };
}


gameRequirementsFail

Helper function that checks if game requirements are fullfilled

Params
req mixed The game requirements from package.json
Returns
boolean TRUE, if check fails
See
GameStage constructor
function gameRequirementsFail(req) {
    var reqFail;


  • skips the check.
    if (!req || req.trim() === '*') return false;


Trick: we compare version numbers using the GameStage class.

    if (req.indexOf(">=") !== -1) {
        req = req.split(">=")[1];
        req = version2GameStage(req);
        reqFail = !req || GameStage.compare(req, serverVersionObj) > 0;
    }
    else if (req.indexOf(">") !== -1) {
        req = req.split(">")[1];
        req = version2GameStage(req);
        reqFail = !req || GameStage.compare(req, serverVersionObj) > -1;
    }
    else {
        req = version2GameStage(req);
        reqFail = !req || GameStage.compare(req, serverVersionObj) !== 0;
    }

    return reqFail;
}



parseGameSettings

Parses a game settings object and builds the treatments object

If no treatments property is defined, one is created with one entry named 'standard'.

All properties of the 'standard' treatment are shared with the other settings.

Params
game object The path to a package.json file, or its content as loaded from require
Returns
object out The parsed settings object containing treatments
See
GameLoader.addGame
function parseGameSettings(settingsObj) {
    var standard, t, out;
    var treatments;
    if ('object' !== typeof settingsObj) {
        throw new TypeError('parseGameSettings: settingsObj must be object.');
    }

    out = {};
    standard = J.clone(settingsObj);
    if (!settingsObj.treatments) {
        out.standard = standard;
        out.standard.treatmentName = 'standard';
        out.standard.name = 'standard';

    }
    else {
        treatments = settingsObj.treatments;
        delete standard.treatments;
        for (t in treatments) {
            if (treatments.hasOwnProperty(t)) {
                out[t] = J.merge(standard, treatments[t]);
                out[t].treatmentName = t;
                out[t].name = t;
            }
        }
    }

    return out;
}


importAuthCodes

Imports an array of authorization codes into the channel registry

Params
channel ServerChannel
codes array The array of codes
file string The name of the file that returned the codes (used in case of errors)
function importAuthCodes(channel, codes, file) {
    if (!J.isArray(codes)) {
        throw new Error('GameLoader.loadAuthDir: codes file must ' +
                        'return an array of codes, or undefined ' +
                        '(if asynchronous): ' + file + '.');
    }
    channel.registry.importClients(codes);
}

function loadSyncAsync(filepath, settings, method, cb) {
    var result;

    if (fs.existsSync(filepath)) {
        try {
            result = require(filepath)(settings, function(err, result) {
                if (err) {
                    throw new Error('GameLoader.' + method + ': an error ' +
                                    'occurred: ' + err);
                }
                cb(result);
            });
        }
        catch(e) {
            throw new Error('GameLoader.' + method + ': cannot read ' +
                            pwdFile + ": \n" + e.stack);
        }

        if (result) cb(result);
    }
}


TODO: see if we need it in the future:



GameLoader.loadViewsFile

Loads views.js from file system and adds channels accordingly

Asynchronously looks for a file called channels.js at the top level of the specified directory.

The file channels.js must export an array containing channels object in the format specified by the ServerChannel constructor. Every channel configuration object can optionally have a waitingRoom object to automatically add a waiting room to the channel.

Params
directory string The path in which views.js will be looked for TODO: views.js file is not loaded anymore because all context are dynamics.
See
PageManager

GameLoader.prototype.loadViewsFile = function(directory, gameInfo) { var viewsFile, that, gameName;

if ('string' !== typeof directory) {
    throw new TypeError('GameLoader.loadViewsFile: directory must be ' +
                        'string.');
}
gameName = gameInfo.name;

// TODO: see if still needed (broken now). // // that = this; // viewsFile = directory + 'views/views.js'; // fs.exists(viewsFile, function(exists) { // var cb, sb; // if (!exists) return; // cb = require(viewsFile); // if ('function' !== typeof cb) { // throw new TypeError('GameLoader.loadViewsFile: ' + // 'views.js did not ' + // 'return a valid function. Dir: ' + directory); // } // // Execute views function in a sandboxed enviroment. // // A game cannot modify other games settings. // sb = that.servernode.resourceManager // .getSandBox(gameName, directory); // cb(sb, gameInfo.settings); // }); };